├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── publish.yml │ └── site.yml ├── .gitignore ├── .run-tool.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── app ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── components │ ├── base.templ │ ├── index.templ │ ├── login.templ │ ├── new_paste.templ │ ├── not_found.templ │ ├── paste.templ │ ├── pastes.templ │ └── signup.templ ├── core │ ├── auth.go │ ├── config.go │ ├── db.go │ ├── helpers.go │ ├── renderers.go │ └── types.go ├── go.mod ├── go.sum ├── handlers │ ├── auth.go │ ├── aux.go │ ├── home.go │ ├── user.go │ └── utils.go ├── main.go ├── main.js ├── middleware │ ├── auth.go │ ├── config.go │ ├── session.go │ └── vite.go ├── migrations │ ├── 20241129_init.up.sql │ └── utils.go ├── package.json ├── pnpm-lock.yaml ├── public │ └── icon.svg ├── query.sql ├── services │ ├── auth.go │ ├── aux.go │ ├── home.go │ ├── user.go │ └── utils.go ├── sqlc.yaml ├── storage │ └── storage.go ├── style.css └── vite.config.js └── docs ├── .gitignore ├── LICENSE.txt ├── archetypes └── default.md ├── assets └── sass │ └── mysti-guides │ └── variables │ └── _custom.scss ├── content ├── _index.md ├── about.md └── docs │ ├── _index.md │ ├── devs │ ├── _index.md │ └── setup.md │ └── setup │ ├── _index.md │ ├── backup.md │ ├── configuration.md │ └── install.md ├── go.mod ├── go.sum ├── hugo.toml └── static ├── icon.svg └── showcase.webp /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | target-branch: main 9 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | schedule: 7 | - cron: '0 12 1 * *' 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: [ 'go' ] 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Install go build deps 28 | run: | 29 | go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 30 | go install github.com/a-h/templ/cmd/templ@latest 31 | 32 | - name: Run codegen for go 33 | working-directory: app 34 | run: | 35 | sqlc generate 36 | templ generate 37 | 38 | - name: Initialize CodeQL 39 | uses: github/codeql-action/init@v3 40 | with: 41 | languages: ${{ matrix.language }} 42 | 43 | - name: Autobuild 44 | uses: github/codeql-action/autobuild@v3 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v3 48 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish App 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | publish-images: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | id-token: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Docker meta 19 | id: meta 20 | uses: docker/metadata-action@v5 21 | with: 22 | images: | 23 | ghcr.io/${{ github.repository }} 24 | tags: | 25 | type=raw,value=latest,enable={{is_default_branch}} 26 | type=semver,pattern={{major}} 27 | type=semver,pattern={{major}}.{{minor}} 28 | type=semver,pattern={{version}} 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v3 31 | - name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v3 33 | - name: Login to GitHub Container Registry 34 | uses: docker/login-action@v3 35 | with: 36 | registry: ghcr.io 37 | username: ${{ github.repository_owner }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | - name: Build and push 40 | uses: docker/build-push-action@v6 41 | with: 42 | context: "./app" 43 | file: "./app/Dockerfile" 44 | platforms: linux/amd64,linux/arm64 45 | push: ${{ github.event_name != 'pull_request' }} 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | cache-from: type=gha 49 | cache-to: type=gha,mode=max 50 | -------------------------------------------------------------------------------- /.github/workflows/site.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Site 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "docs/**" 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | concurrency: 17 | group: "pages" 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Setup Hugo 26 | uses: peaceiris/actions-hugo@v3 27 | with: 28 | hugo-version: 'latest' 29 | extended: true 30 | - name: Setup Pages 31 | id: pages 32 | uses: actions/configure-pages@v5 33 | - name: Build With Hugo 34 | working-directory: ./docs 35 | run: hugo --minify 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | path: ./docs/public 40 | 41 | deploy: 42 | environment: 43 | name: github-pages 44 | url: ${{ steps.deployment.outputs.page_url }} 45 | runs-on: ubuntu-latest 46 | needs: build 47 | steps: 48 | - name: Deploy to GitHub Pages 49 | id: deployment 50 | uses: actions/deploy-pages@v4 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Tt]humbs.db 2 | *.DS_Store 3 | ~$* 4 | 5 | *.env 6 | *db.sqlite 7 | data/ 8 | -------------------------------------------------------------------------------- /.run-tool.yml: -------------------------------------------------------------------------------- 1 | targets: 2 | gen-db: 3 | program: sqlc 4 | args: 5 | - generate 6 | cwd: app 7 | gen-templates: 8 | program: templ 9 | args: 10 | - generate 11 | cwd: app 12 | dev-app: 13 | program: go 14 | args: 15 | - run 16 | - . 17 | cwd: app 18 | env_file: .env 19 | before_hooks: 20 | - gen-templates 21 | dev-app-vite: 22 | program: pnpm 23 | args: 24 | - run 25 | - dev 26 | cwd: app 27 | dev-app-health-check: 28 | program: go 29 | args: 30 | - run 31 | - . 32 | - health-check 33 | cwd: app 34 | env_file: .env 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.1.0] - 2025-04-24 8 | ### Added 9 | - #121; ability to disable anonymous paste creation 10 | - #122; paste size limit & ability to disable attachments 11 | - #123; ability to change random slug length 12 | - #124; docker health check 13 | - #129; migrate lexer alias code to official method 14 | ### Changed 15 | - show paste link on paste page 16 | - bump deps 17 | - update to go1.24 18 | 19 | ## [2.0.0] - 2025-02-27 20 | ### Added 21 | - V2 app 22 | ### Removed 23 | - V1 app 24 | 25 | ## [1.10.0] - 2024-02-17 26 | ### Changed 27 | - Bump deps & migrate some code 28 | - Use hatch for python management 29 | 30 | ## [1.9.0] - 2023-02-16 31 | ### Added 32 | - #27; :construction: Experimental S3 storage support 33 | - Add ability to hide ascii art boot message 34 | ### Changed 35 | - #70; Long ID only adjustable through config value 36 | - Bump deps 37 | 38 | ## [1.8.0] - 2022-12-21 39 | ### Added 40 | - Ensure Redis can be reached on app launch 41 | - Add ASCII art for launch :) 42 | - #67; log loaded configs on launch 43 | - #66; custom 404 page 44 | - Support installing using pip through setuptools pyproject 45 | - Added optional json accelerator (installed by default in Docker image) 46 | ### Changed 47 | - #68; make Docker image rootless (increase security) 48 | - Reduce number of final Docker image layers 49 | - Remove ability to use "Copy Share Link" when not under secure connection 50 | - Improved launch scripts 51 | - Bump pip versions 52 | ### Fixed 53 | - Fix long id 54 | 55 | ## [1.7.0] - 2022-10-30 56 | ### Added 57 | - Show expiry on paste screen 58 | - Configurable multi-tiered caching 59 | - CLI can remove empty cache folders 60 | - Strict paste id checking in URLs 61 | - Better general exception handling 62 | - Add human padding for paste id 63 | - Some more unit tests 64 | ### Changed 65 | - Major code refactoring (pastes are no longer dependant on storage types, for future s3 object support) 66 | - Tidy REST API routes 67 | ### Fixed 68 | - Fixed #53 Expiry set in UI always interpreted as UTC, by having a configurable timezone 69 | ### Removed 70 | - Removed deprecated features, see #50 71 | 72 | ## [1.6.1] - 2022-10-09 73 | ### Fixed 74 | - #51, When override lexer is given in paste url incorrectly uses cached content 75 | 76 | ## [1.6.0] - 2022-10-08 77 | ### Added 78 | - Simple paste creation through the api 79 | - Ability to hide version number 80 | - Optional caching with internal or redis 81 | ### Changed 82 | - Code refactoring 83 | - Update requirements 84 | - Dependency updates 85 | 86 | ## [1.5.0] - 2022-09-23 87 | ### Added 88 | - #39, brand customisation 89 | ### Changed 90 | - Only generate paste id's with A-Za-z0-9 91 | - Put config into groups 92 | - Update pip requirements 93 | - quart-schema to ~=0.13.0 94 | - pydantic[dotenv] to ~=1.10.2 95 | - mkdocs-material to ~=8.5.3 96 | - Split routes into separate modules 97 | 98 | ## [1.4.0] - 2022-08-27 99 | ### Added 100 | - #21, Experimental management CLI 101 | ### Changed 102 | - #19, Use --link for dockerfile, improving build speed 103 | ### Fixed 104 | - #26, Backwards compatibility for copy share link button 105 | 106 | ## [1.3.0] - 2022-08-19 107 | ### Added 108 | - #18, add ability to clone a paste content into new paste 109 | - #20, add optional paste title 110 | - #22, override lexer name with `.` of paste URL 111 | ### Fixed 112 | - Fixed not being able to select "No Highlighting" option 113 | 114 | ## [1.2.1] - 2022-08-14 115 | ### Fixed 116 | - Fixed not being able to select "No Highlighting" option 117 | 118 | ## [1.2.0] - 2022-08-13 119 | ### Added 120 | - #8, add filtered highlighter syntax dropdown 121 | - #6, config option to turn index page into new paste page 122 | ### Changed 123 | - add ability to hide long id form checkbox 124 | - #10, centred horizontal positioned options in new paste form 125 | - use shorter names for syntax highlighting 126 | ### Fixed 127 | - #9, turn off auto correction features 128 | 129 | ## [1.1.0] - 2022-08-04 130 | ### Added 131 | - Configurable default expiry for UI (disabled by default) 132 | - Optional syntax highlighting 133 | - Optional public list api route (disabled by default) 134 | 135 | ## [1.0.0] - 2022-07-31 136 | ### Added 137 | - Initial release 138 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hasty Paste II 2 | Paste it all, with haste. 3 | 4 | Docs available at: [hastypaste.docs.enchantedcode.co.uk](https://hastypaste.docs.enchantedcode.co.uk/) 5 | 6 | ## Showcase 7 | ![Showcase image, showing several app pages](./docs/static/showcase.webp) 8 | 9 | ## Features 10 | - Paste visibility 11 | - Private 12 | - Unlisted 13 | - Public 14 | - Paste assets 15 | - Paste content rendering (plain, markdown, code) 16 | - Customisable paste slug (or random) 17 | - Paste expiry 18 | - User accounts with "anonymous" mode 19 | - SSO via OpenID/OAuth2 20 | - Dark/Light theme 21 | 22 | ## Support Me 23 | Like this project? Consider supporting me financially so I can continue development. 24 | 25 | Buy Me A Coffee 26 | 27 | ## License 28 | This project is Copyright (c) 2025 Leo Spratt, licences shown below: 29 | 30 | Code 31 | 32 | AGPL-3 or any later version. Full license found in `LICENSE.txt` 33 | 34 | Documentation 35 | 36 | FDLv1.3 or any later version. Full license found in `docs/LICENSE.txt` 37 | 38 | Icon/Mark 39 | 40 | All Rights Reserved 41 | -------------------------------------------------------------------------------- /app/.dockerignore: -------------------------------------------------------------------------------- 1 | database/ 2 | components/*.go 3 | dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | database/ 2 | components/*.go 3 | dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM node:20-slim AS node-base 4 | ENV PNPM_HOME="/pnpm" 5 | ENV PATH="$PNPM_HOME:$PATH" 6 | 7 | # ensure running latest corepack: https://github.com/nodejs/corepack/issues/612 8 | RUN npm install --global corepack@latest 9 | RUN corepack enable 10 | 11 | FROM golang:1.24-alpine AS go-base 12 | RUN go install github.com/a-h/templ/cmd/templ@latest 13 | RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 14 | 15 | FROM node-base AS node-build 16 | WORKDIR /opt/hasty-paste 17 | 18 | COPY . . 19 | 20 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 21 | RUN pnpm run build 22 | 23 | FROM go-base AS go-build 24 | WORKDIR /opt/hasty-paste 25 | 26 | COPY . . 27 | 28 | RUN sqlc generate 29 | RUN templ generate 30 | RUN CGO_ENABLED=0 GOOS=linux go build -o ./hasty-paste 31 | 32 | FROM gcr.io/distroless/static-debian12 33 | WORKDIR /opt/hasty-paste 34 | 35 | COPY --from=node-build --link /opt/hasty-paste/dist /opt/hasty-paste/dist 36 | COPY --from=go-build --link /opt/hasty-paste/hasty-paste /opt/hasty-paste/hasty-paste 37 | 38 | ENV BIND__HOST=0.0.0.0 39 | ENV BIND__PORT=8080 40 | ENV DB_URI=sqlite:///opt/hasty-paste/data/db.sqlite 41 | ENV ATTACHMENTS_PATH=/opt/hasty-paste/data/attachments 42 | 43 | EXPOSE 8080 44 | VOLUME /opt/hasty-paste/data 45 | 46 | CMD [ "./hasty-paste" ] 47 | HEALTHCHECK --interval=2m --timeout=5s --start-period=20s --start-interval=5s --retries=3 \ 48 | CMD [ "./hasty-paste", "health-check" ] 49 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # The App 2 | -------------------------------------------------------------------------------- /app/components/base.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/enchant97/hasty-paste/app/middleware" 7 | ) 8 | 9 | var viteHeadHandler = templ.NewOnceHandle() 10 | 11 | func getCurrentUsername(ctx context.Context) string { 12 | if currentUsername, ok := ctx.Value("currentUsername").(string); ok { 13 | return currentUsername 14 | } 15 | return "anonymous" 16 | } 17 | 18 | templ flashes() { 19 |
    20 | for _, flash := range middleware.GetFlashes(ctx) { 21 |
  • { flash.Message }
  • 26 | } 27 |
28 | } 29 | 30 | templ base(title string) { 31 | 32 | 33 | 34 | 35 | 36 | 37 | { title } - Hasty Paste II 38 | @viteHeadHandler.Once() { 39 | @templ.Raw(ctx.Value("viteHead").(string)) 40 | } 41 | 42 | 43 | 44 | @flashes() 45 | { children... } 46 | 47 | 48 | 49 | } 50 | 51 | templ header() { 52 | {{ currentUsername := getCurrentUsername(ctx) }} 53 |
54 | Hasty Paste II 55 | Home 56 | New Paste 57 | if currentUsername == "anonymous" { 58 | Public Area 60 | Sign In 61 | } else { 62 | My Area 64 | Sign Out 65 | } 66 |
67 | } 68 | 69 | templ footer() { 70 | 82 | } 83 | -------------------------------------------------------------------------------- /app/components/index.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | "github.com/enchant97/hasty-paste/app/database" 6 | "time" 7 | ) 8 | 9 | templ IndexPage(latestPastes []database.GetLatestPublicPastesRow) { 10 | @base("Home") { 11 | @header() 12 |
13 |

Welcome To Hasty Paste II.

14 |

Paste it all, with haste.

15 |
16 |

Recent Pastes

17 | 37 |
38 |
39 | @footer() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/components/login.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/enchant97/hasty-paste/app/core" 4 | 5 | templ UserLoginPage(internalLoginEnabled bool, internalSignupEnabled bool, oidcConfig core.OIDCConfig) { 6 | @base("Sign In") { 7 | @header() 8 |
9 |

Sign In

10 | if internalLoginEnabled { 11 |
13 | 17 | 21 | 22 | if oidcConfig.Enabled { 23 | Sign In With { oidcConfig.Name } 24 | } 25 | if internalSignupEnabled { 26 | Sign Up Instead? 27 | } 28 |
29 | } else if oidcConfig.Enabled { 30 | Sign In With { oidcConfig.Name } 31 | } 32 |
33 | @footer() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/components/new_paste.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/alecthomas/chroma/v2/lexers" 4 | 5 | var lexerAliasOptionsHandle = templ.NewOnceHandle() 6 | 7 | templ contentFormatList(id string) { 8 | 9 | @lexerAliasOptionsHandle.Once() { 10 | 11 | 12 | for _, lexerAlias := range lexers.Aliases(false) { 13 | if lexerAlias != "markdown" && lexerAlias != "plaintext" { 14 | 15 | } 16 | } 17 | } 18 | 19 | } 20 | 21 | templ NewPastePage(attachmentsEnabled bool) { 22 | @base("Home") { 23 | @header() 24 |
25 |

New Paste

26 |
32 | 36 | 40 | if getCurrentUsername(ctx) == "anonymous" { 41 | 42 | } else { 43 | 51 | } 52 | if attachmentsEnabled { 53 | 57 | } 58 | 72 | 82 | 83 |
84 |
85 | @footer() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/components/not_found.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | templ NotFoundPage() { 4 | @base("404 Not Found") { 5 | @header() 6 |
7 |

Page Not Found

8 |

Your requested page could not be found or you do not have permission.

9 |
10 | @footer() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/components/paste.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "bytes" 7 | "github.com/enchant97/hasty-paste/app/database" 8 | "github.com/enchant97/hasty-paste/app/core" 9 | "github.com/enchant97/hasty-paste/app/middleware" 10 | "github.com/google/uuid" 11 | "context" 12 | "net/url" 13 | ) 14 | 15 | func makeDirectPasteURL(ctx context.Context, pasteID uuid.UUID) string { 16 | p, _ := url.JoinPath(middleware.GetAppConfig(ctx).PublicURL, fmt.Sprintf("~/%s", pasteID.String())) 17 | return p 18 | } 19 | 20 | func makePasteURL(ctx context.Context, username string, pasteSlug string) string { 21 | p, _ := url.JoinPath(middleware.GetAppConfig(ctx).PublicURL, fmt.Sprintf("@/%s/%s", username, pasteSlug)) 22 | return p 23 | } 24 | 25 | templ PastePage(username string, paste database.Paste, attachments []database.Attachment) { 26 | @base("Paste") { 27 | @header() 28 |
29 |

{ paste.Slug }

30 |
31 |

Info

32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 48 | 49 | if paste.ExpiresAt.Valid { 50 | 51 | 52 | 57 | 58 | } 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
Owner{ username }
Created At
Expires At
Visibility{ paste.Visibility }
Direct Link{ makeDirectPasteURL(ctx, paste.ID) }
Paste Link{ makePasteURL(ctx, username, paste.Slug) }
73 |
74 |
75 |

Attachments

76 | if len(attachments) == 0 { 77 |

No attachments were uploaded.

78 | } else { 79 | 94 | } 95 |
96 |
97 |

Content

98 | if paste.ContentFormat == "markdown" { 99 | {{ var buf bytes.Buffer }} 100 | {{ core.RenderMarkdown([]byte(paste.Content), &buf) }} 101 |
@templ.Raw(buf.String())
102 | } 103 | else if paste.ContentFormat == "plaintext" { 104 |
{ paste.Content }
105 | } 106 | else { 107 | {{ var buf bytes.Buffer }} 108 | {{ core.RenderSourceCode(paste.ContentFormat, paste.Content, &buf) }} 109 |
@templ.Raw(buf.String())
110 | } 111 |
112 |
113 | @footer() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/components/pastes.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | "github.com/enchant97/hasty-paste/app/database" 6 | "time" 7 | ) 8 | 9 | templ PastesPage(username string, pastes []database.GetLatestPastesByUserRow) { 10 | @base(fmt.Sprintf("%s's Pastes", username)) { 11 | @header() 12 |
13 |

{ fmt.Sprintf("%s's Pastes", username) }

14 | 42 |
43 | @footer() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/components/signup.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | templ UserSignupPage() { 4 | @base("Signup") { 5 | @header() 6 |
7 |

Sign Up

8 |
9 | 21 | 32 | 43 | 44 | Sign In Instead? 45 |
46 |
47 | @footer() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/core/auth.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "time" 7 | 8 | "github.com/golang-jwt/jwt/v5" 9 | "golang.org/x/crypto/bcrypt" 10 | ) 11 | 12 | var ( 13 | JWTClaimsNotValidError = errors.New("invalid jwt claims") 14 | DefaultJwtSigningMethod = jwt.SigningMethodHS256 15 | ) 16 | 17 | type AuthenticationToken struct { 18 | TokenContent string 19 | ExpiresAt time.Time 20 | } 21 | 22 | // Hash a password for secure storage. 23 | func HashPassword(plainPassword string) []byte { 24 | hashed, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | return hashed 29 | } 30 | 31 | // Check whether the given plain password matches a hashed version. 32 | func IsValidPassword(plainPassword string, hashedPassword []byte) bool { 33 | if err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(plainPassword)); err != nil { 34 | return false 35 | } 36 | return true 37 | } 38 | 39 | func CreateAuthenticationToken(username string, secretKey []byte, durationUntilExpiry time.Duration) (AuthenticationToken, error) { 40 | expiresAt := time.Now().Add(durationUntilExpiry) 41 | token := jwt.NewWithClaims(DefaultJwtSigningMethod, jwt.RegisteredClaims{ 42 | Subject: username, 43 | ExpiresAt: jwt.NewNumericDate(expiresAt), 44 | IssuedAt: jwt.NewNumericDate(time.Now()), 45 | NotBefore: jwt.NewNumericDate(time.Now()), 46 | }) 47 | tokenContent, err := token.SignedString(secretKey) 48 | return AuthenticationToken{ 49 | TokenContent: tokenContent, 50 | ExpiresAt: expiresAt, 51 | }, err 52 | } 53 | 54 | func ParseAuthenticationToken(token string, secretKey []byte) (string, error) { 55 | if token, err := jwt.ParseWithClaims(token, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) { 56 | return secretKey, nil 57 | }, 58 | jwt.WithValidMethods([]string{DefaultJwtSigningMethod.Alg()}), 59 | jwt.WithExpirationRequired(), 60 | jwt.WithIssuedAt(), 61 | jwt.WithLeeway(3*time.Minute), 62 | ); err != nil { 63 | return "", err 64 | } else { 65 | if claims, ok := token.Claims.(*jwt.RegisteredClaims); !ok { 66 | return "", JWTClaimsNotValidError 67 | } else { 68 | return claims.Subject, nil 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/core/config.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/caarlos0/env/v11" 10 | "github.com/labstack/gommon/bytes" 11 | ) 12 | 13 | type Base64Decoded []byte 14 | 15 | func (b *Base64Decoded) UnmarshalText(text []byte) error { 16 | decoded, err := base64.StdEncoding.DecodeString(string(text)) 17 | if err != nil { 18 | return errors.New("cannot decode base64 string") 19 | } 20 | *b = decoded 21 | return nil 22 | } 23 | 24 | type Bytes int64 25 | 26 | func (b *Bytes) UnmarshalText(text []byte) error { 27 | if v, err := bytes.Parse(string(text)); err != nil { 28 | return err 29 | } else { 30 | *b = Bytes(v) 31 | return nil 32 | } 33 | } 34 | 35 | type BindConfig struct { 36 | Host string `env:"HOST" envDefault:"127.0.0.1"` 37 | Port uint `env:"PORT" envDefault:"8080"` 38 | } 39 | 40 | func (c *BindConfig) AsAddress() string { 41 | return fmt.Sprintf("%s:%d", c.Host, c.Port) 42 | } 43 | 44 | type DevConfig struct { 45 | Enabled bool `env:"ENABLED" envDefault:"false"` 46 | ViteDevHost string `env:"VITE_DEV_HOST,notEmpty" envDefault:"localhost:5173"` 47 | ViteDistPath string `env:"VITE_DIST_PATH,notEmpty" envDefault:"./dist"` 48 | } 49 | 50 | type OIDCConfig struct { 51 | Enabled bool `env:"ENABLED" envDefault:"false"` 52 | Name string `env:"NAME"` 53 | IssuerUrl string `env:"ISSUER_URL"` 54 | ClientID string `env:"CLIENT_ID"` 55 | ClientSecret string `env:"CLIENT_SECRET"` 56 | } 57 | 58 | type AppConfig struct { 59 | Dev DevConfig `envPrefix:"DEV__"` 60 | Bind BindConfig `envPrefix:"BIND__"` 61 | OIDC OIDCConfig `envPrefix:"OIDC__"` 62 | PublicURL string `env:"PUBLIC_URL,notEmpty"` 63 | BehindProxy bool `env:"BEHIND_PROXY" envDefault:"false"` 64 | DbUri string `env:"DB_URI,notEmpty"` 65 | AttachmentsPath string `env:"ATTACHMENTS_PATH,notEmpty"` 66 | TokenSecret Base64Decoded `env:"AUTH_TOKEN_SECRET,notEmpty"` 67 | TokenExpiry int64 `env:"AUTH_TOKEN_EXPIRY" envDefault:"604800"` 68 | SessionSecret Base64Decoded `env:"SESSION_SECRET,notEmpty"` 69 | SignupEnabled bool `env:"SIGNUP_ENABLED" envDefault:"true"` 70 | InternalAuthEnabled bool `env:"INTERNAL_AUTH_ENABLED" envDefault:"true"` 71 | RandomSlugLength int `env:"RANDOM_SLUG_LENGTH" envDefault:"10"` 72 | AnonymousPastesEnabled bool `env:"ANONYMOUS_PASTES_ENABLED" envDefault:"true"` 73 | MaxPasteSize int64 `env:"MAX_PASTE_SIZE" envDefault:"12582912"` 74 | AttachmentsEnabled bool `env:"ATTACHMENTS_ENABLED" envDefault:"true"` 75 | } 76 | 77 | func (ac *AppConfig) SecureMode() bool { 78 | return strings.HasPrefix(ac.PublicURL, "https://") 79 | } 80 | 81 | func (appConfig *AppConfig) ParseConfig() error { 82 | if err := env.Parse(appConfig); err != nil { 83 | return err 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /app/core/db.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/enchant97/hasty-paste/app/database" 7 | ) 8 | 9 | type DAO struct { 10 | DB *sql.DB 11 | Queries *database.Queries 12 | } 13 | 14 | func (dao DAO) New(db *sql.DB, queries *database.Queries) DAO { 15 | return DAO{ 16 | DB: db, 17 | Queries: queries, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/core/helpers.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "hash/crc32" 7 | "io" 8 | "math/big" 9 | "strings" 10 | 11 | "github.com/google/uuid" 12 | ) 13 | 14 | const RandomSlugCharacters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 15 | 16 | func GenerateRandomSlug(n int) string { 17 | var out strings.Builder 18 | maxIndex := big.NewInt(int64(len(RandomSlugCharacters))) 19 | for i := 0; i < n; i++ { 20 | charIndex, _ := rand.Int(rand.Reader, maxIndex) 21 | char := RandomSlugCharacters[charIndex.Int64()] 22 | out.Grow(1) 23 | out.WriteByte(char) 24 | } 25 | return out.String() 26 | } 27 | 28 | func MakeChecksum(r io.Reader) (string, error) { 29 | h := crc32.New(crc32.MakeTable(crc32.IEEE)) 30 | buf := make([]byte, 1024) 31 | for { 32 | n, err := r.Read(buf) 33 | if err != nil && err != io.EOF { 34 | return "", err 35 | } 36 | if n == 0 { 37 | break 38 | } 39 | if _, err := h.Write(buf); err != nil { 40 | return "", err 41 | } 42 | } 43 | return fmt.Sprintf("crc32-%x", h.Sum32()), nil 44 | } 45 | 46 | // Create a UUID suitable for insertion into the database. 47 | func NewUUID() uuid.UUID { 48 | id, err := uuid.NewV7() 49 | if err != nil { 50 | panic(err) 51 | } 52 | return id 53 | } 54 | -------------------------------------------------------------------------------- /app/core/renderers.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/alecthomas/chroma/v2" 7 | "github.com/alecthomas/chroma/v2/formatters/html" 8 | "github.com/alecthomas/chroma/v2/lexers" 9 | "github.com/alecthomas/chroma/v2/styles" 10 | "github.com/yuin/goldmark" 11 | "github.com/yuin/goldmark/extension" 12 | gm_html "github.com/yuin/goldmark/renderer/html" 13 | ) 14 | 15 | func RenderSourceCode(lexerAlias string, content string, w io.Writer) error { 16 | lexer := chroma.Coalesce(lexers.Get(lexerAlias)) 17 | style := styles.Get("github-dark") 18 | formatter := html.New(html.InlineCode(true)) 19 | tokens, err := lexer.Tokenise(nil, content) 20 | if err != nil { 21 | return err 22 | } 23 | return formatter.Format(w, style, tokens) 24 | } 25 | 26 | func RenderMarkdown(content []byte, w io.Writer) error { 27 | md := goldmark.New( 28 | goldmark.WithExtensions(extension.GFM), 29 | goldmark.WithParserOptions(), 30 | goldmark.WithRendererOptions( 31 | gm_html.WithHardWraps(), 32 | ), 33 | ) 34 | return md.Convert(content, w) 35 | } 36 | -------------------------------------------------------------------------------- /app/core/types.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "mime/multipart" 5 | "time" 6 | ) 7 | 8 | type NewPasteFormAttachment struct { 9 | Slug string `validate:"printascii,required"` 10 | Type string `validate:"required"` 11 | Size int64 `validate:"required"` 12 | Open func() (multipart.File, error) 13 | } 14 | 15 | type NewPasteForm struct { 16 | Slug string `validate:"printascii,required"` 17 | Content string `validate:"required"` 18 | ContentFormat string `validate:"required,lowercase,max=30"` 19 | Visibility string `validate:"required,oneof=public unlisted private"` 20 | Expiry *time.Time 21 | Attachments []NewPasteFormAttachment 22 | } 23 | 24 | type NewUserForm struct { 25 | Username string `validate:"alphanum,lowercase,min=3,max=30,required"` 26 | Password string `validate:"min=8,required"` 27 | PasswordConfirm string `validate:"eqcsfield=Password,required"` 28 | } 29 | 30 | type OIDCUser struct { 31 | ClientID string `validate:"required"` 32 | Subject string `validate:"required"` 33 | } 34 | 35 | type OIDCUserWithUsername struct { 36 | OIDCUser 37 | Username string `validate:"alphanum,lowercase,min=3,max=30,required"` 38 | } 39 | 40 | type LoginUserForm struct { 41 | Username string `validate:"required"` 42 | Password string `validate:"required"` 43 | } 44 | -------------------------------------------------------------------------------- /app/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/enchant97/hasty-paste/app 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/a-h/templ v0.3.857 9 | github.com/alecthomas/chroma/v2 v2.17.0 10 | github.com/caarlos0/env/v11 v11.3.1 11 | github.com/coreos/go-oidc/v3 v3.14.1 12 | github.com/go-chi/chi/v5 v5.2.1 13 | github.com/go-playground/validator/v10 v10.26.0 14 | github.com/golang-jwt/jwt/v5 v5.2.2 15 | github.com/golang-migrate/migrate/v4 v4.18.2 16 | github.com/google/uuid v1.6.0 17 | github.com/gorilla/sessions v1.4.0 18 | github.com/labstack/gommon v0.4.2 19 | github.com/yuin/goldmark v1.7.10 20 | golang.org/x/crypto v0.37.0 21 | golang.org/x/oauth2 v0.29.0 22 | modernc.org/sqlite v1.37.0 23 | ) 24 | 25 | require ( 26 | github.com/dlclark/regexp2 v1.11.5 // indirect 27 | github.com/dustin/go-humanize v1.0.1 // indirect 28 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 29 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 30 | github.com/go-playground/locales v0.14.1 // indirect 31 | github.com/go-playground/universal-translator v0.18.1 // indirect 32 | github.com/gorilla/securecookie v1.1.2 // indirect 33 | github.com/hashicorp/errwrap v1.1.0 // indirect 34 | github.com/hashicorp/go-multierror v1.1.1 // indirect 35 | github.com/leodido/go-urn v1.4.0 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/ncruces/go-strftime v0.1.9 // indirect 38 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 39 | go.uber.org/atomic v1.11.0 // indirect 40 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect 41 | golang.org/x/net v0.39.0 // indirect 42 | golang.org/x/sys v0.32.0 // indirect 43 | golang.org/x/text v0.24.0 // indirect 44 | modernc.org/libc v1.63.0 // indirect 45 | modernc.org/mathutil v1.7.1 // indirect 46 | modernc.org/memory v1.10.0 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /app/go.sum: -------------------------------------------------------------------------------- 1 | github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU= 2 | github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk= 3 | github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg= 4 | github.com/a-h/templ v0.3.857/go.mod h1:qhrhAkRFubE7khxLZHsBFHfX+gWwVNKbzKeF9GlPV4M= 5 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 6 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 7 | github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= 8 | github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= 9 | github.com/alecthomas/chroma/v2 v2.17.0 h1:3r2Cgk+nXNICMBxIFGnTRTbQFUwMiLisW+9uos0TtUI= 10 | github.com/alecthomas/chroma/v2 v2.17.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 11 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 12 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 13 | github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= 14 | github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= 15 | github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= 16 | github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= 17 | github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= 18 | github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 22 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 23 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 24 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 25 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 26 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 27 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 28 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 29 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 30 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 31 | github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= 32 | github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= 33 | github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= 34 | github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= 35 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 36 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 37 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 38 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 39 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 40 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 41 | github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= 42 | github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= 43 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 44 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 45 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 46 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 47 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 48 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 49 | github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= 50 | github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= 51 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 52 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 53 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 54 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 55 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 56 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= 57 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 58 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 59 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 60 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 61 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 62 | github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 63 | github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 64 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 65 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 66 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 67 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 68 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 69 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 70 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 71 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 72 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 73 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 74 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 75 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 76 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 77 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 78 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 79 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 80 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 81 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 82 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 83 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 84 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 85 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 86 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 87 | github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= 88 | github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 89 | github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= 90 | github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 91 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 92 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 93 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 94 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 95 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 96 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 97 | golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= 98 | golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= 99 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 100 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 101 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 102 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 103 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 104 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 105 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 106 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 107 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 108 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 109 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 110 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 111 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 112 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 113 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 114 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 116 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 117 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 118 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 119 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 120 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 121 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 122 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 123 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 124 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 125 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 126 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 127 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 128 | modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= 129 | modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 130 | modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA= 131 | modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= 132 | modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= 133 | modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc= 134 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 135 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 136 | modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= 137 | modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 138 | modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= 139 | modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= 140 | modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= 141 | modernc.org/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA= 142 | modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E= 143 | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 144 | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 145 | modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= 146 | modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= 147 | modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= 148 | modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 149 | modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= 150 | modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 151 | modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= 152 | modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= 153 | modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8= 154 | modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= 155 | modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= 156 | modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= 157 | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 158 | modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 159 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 160 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 161 | -------------------------------------------------------------------------------- /app/handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "time" 10 | 11 | "github.com/a-h/templ" 12 | "github.com/coreos/go-oidc/v3/oidc" 13 | "github.com/enchant97/hasty-paste/app/components" 14 | "github.com/enchant97/hasty-paste/app/core" 15 | "github.com/enchant97/hasty-paste/app/database" 16 | "github.com/enchant97/hasty-paste/app/middleware" 17 | "github.com/enchant97/hasty-paste/app/services" 18 | "github.com/go-chi/chi/v5" 19 | "github.com/go-playground/validator/v10" 20 | "github.com/google/uuid" 21 | "golang.org/x/oauth2" 22 | ) 23 | 24 | type OIDCUserClaims struct { 25 | Subject string `json:"sub"` 26 | PreferredUsername string `json:"preferred_username"` 27 | Name string `json:"name"` 28 | } 29 | 30 | func setupOIDC(appConfig core.AppConfig) (*oidc.Provider, oauth2.Config, error) { 31 | provider, err := oidc.NewProvider(context.Background(), appConfig.OIDC.IssuerUrl) 32 | if err != nil { 33 | return nil, oauth2.Config{}, err 34 | } 35 | callbackURL, err := url.JoinPath(appConfig.PublicURL, "/sso/oidc/callback") 36 | if err != nil { 37 | return nil, oauth2.Config{}, err 38 | } 39 | config := oauth2.Config{ 40 | ClientID: appConfig.OIDC.ClientID, 41 | ClientSecret: appConfig.OIDC.ClientSecret, 42 | Endpoint: provider.Endpoint(), 43 | RedirectURL: callbackURL, 44 | Scopes: []string{oidc.ScopeOpenID, "profile"}, 45 | } 46 | return provider, config, nil 47 | } 48 | 49 | type AuthHandler struct { 50 | appConfig core.AppConfig 51 | validator *validator.Validate 52 | service services.AuthService 53 | authProvider *middleware.AuthenticationProvider 54 | sessionProvider *middleware.SessionProvider 55 | OIDCProvider *oidc.Provider 56 | OAuth2Config oauth2.Config 57 | } 58 | 59 | func (h AuthHandler) Setup( 60 | r *chi.Mux, 61 | appConfig core.AppConfig, 62 | s services.AuthService, 63 | v *validator.Validate, 64 | ap *middleware.AuthenticationProvider, 65 | sp *middleware.SessionProvider, 66 | ) { 67 | var OIDCProvider *oidc.Provider 68 | var OAuth2Config oauth2.Config 69 | if appConfig.OIDC.Enabled { 70 | var err error 71 | OIDCProvider, OAuth2Config, err = setupOIDC(appConfig) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | } 76 | h = AuthHandler{ 77 | appConfig: appConfig, 78 | validator: v, 79 | service: s, 80 | authProvider: ap, 81 | sessionProvider: sp, 82 | OIDCProvider: OIDCProvider, 83 | OAuth2Config: OAuth2Config, 84 | } 85 | r.Group(func(r chi.Router) { 86 | r.Use(ap.RequireAuthenticationMiddleware) 87 | r.Get("/logout", h.GetLogoutPage) 88 | }) 89 | r.Group(func(r chi.Router) { 90 | r.Use(ap.RequireNoAuthenticationMiddleware) 91 | if appConfig.SignupEnabled { 92 | r.Get("/signup", h.GetUserSignupPage) 93 | r.Post("/signup/_post", h.PostUserSignupPage) 94 | } 95 | r.Get("/login", h.GetUserLoginPage) 96 | if appConfig.InternalAuthEnabled { 97 | r.Post("/login/_post", h.PostUserLoginPage) 98 | } 99 | if appConfig.OIDC.Enabled { 100 | r.Get("/sso/oidc", h.GetOIDCPage) 101 | r.Get("/sso/oidc/callback", h.GetOIDCCallbackPage) 102 | } 103 | }) 104 | } 105 | 106 | func (h *AuthHandler) GetUserSignupPage(w http.ResponseWriter, r *http.Request) { 107 | templ.Handler(components.UserSignupPage()).ServeHTTP(w, r) 108 | } 109 | 110 | func (h *AuthHandler) PostUserSignupPage(w http.ResponseWriter, r *http.Request) { 111 | if err := r.ParseForm(); err != nil { 112 | http.Error(w, "Bad Request", http.StatusBadRequest) 113 | return 114 | } 115 | 116 | form := core.NewUserForm{ 117 | Username: r.PostFormValue("username"), 118 | Password: r.PostFormValue("password"), 119 | PasswordConfirm: r.PostFormValue("passwordConfirm"), 120 | } 121 | 122 | if err := h.validator.Struct(form); err != nil { 123 | s := h.sessionProvider.GetSession(r) 124 | s.AddFlash(middleware.CreateErrorFlash("given details are invalid, do your passwords match?")) 125 | s.Save(r, w) // TODO handle error 126 | http.Redirect(w, r, "/signup", http.StatusFound) 127 | return 128 | } 129 | 130 | if _, err := h.service.CreateNewUser(form); err != nil { 131 | if errors.Is(err, services.ErrConflict) { 132 | s := h.sessionProvider.GetSession(r) 133 | s.AddFlash(middleware.CreateErrorFlash("user with that username already exists")) 134 | s.Save(r, w) // TODO handle error 135 | http.Redirect(w, r, "/signup", http.StatusFound) 136 | } else { 137 | InternalErrorResponse(w, err) 138 | } 139 | return 140 | } 141 | 142 | s := h.sessionProvider.GetSession(r) 143 | s.AddFlash(middleware.CreateOkFlash("user created")) 144 | s.Save(r, w) // TODO handle error 145 | 146 | http.Redirect(w, r, "/login", http.StatusSeeOther) 147 | } 148 | 149 | func (h *AuthHandler) GetUserLoginPage(w http.ResponseWriter, r *http.Request) { 150 | templ.Handler(components.UserLoginPage( 151 | h.appConfig.InternalAuthEnabled, 152 | h.appConfig.SignupEnabled, 153 | h.appConfig.OIDC, 154 | )).ServeHTTP(w, r) 155 | } 156 | 157 | func (h *AuthHandler) PostUserLoginPage(w http.ResponseWriter, r *http.Request) { 158 | if err := r.ParseForm(); err != nil { 159 | http.Error(w, "Bad Request", http.StatusBadRequest) 160 | return 161 | } 162 | 163 | form := core.LoginUserForm{ 164 | Username: r.PostFormValue("username"), 165 | Password: r.PostFormValue("password"), 166 | } 167 | 168 | if err := h.validator.Struct(form); err != nil { 169 | http.Error(w, "Bad Request", http.StatusBadRequest) 170 | return 171 | } 172 | 173 | if isValid, err := h.service.CheckIfValidUser(form); err != nil { 174 | if errors.Is(err, services.ErrNotFound) { 175 | s := h.sessionProvider.GetSession(r) 176 | s.AddFlash(middleware.CreateErrorFlash("username or password invalid")) 177 | s.Save(r, w) // TODO handle error 178 | http.Redirect(w, r, "/login", http.StatusFound) 179 | } else { 180 | InternalErrorResponse(w, err) 181 | } 182 | return 183 | } else if !isValid { 184 | s := h.sessionProvider.GetSession(r) 185 | s.AddFlash(middleware.CreateErrorFlash("username or password invalid")) 186 | s.Save(r, w) // TODO handle error 187 | http.Redirect(w, r, "/login", http.StatusFound) 188 | return 189 | } 190 | 191 | if token, err := core.CreateAuthenticationToken( 192 | form.Username, 193 | h.appConfig.TokenSecret, 194 | time.Duration(int64(time.Second)*h.appConfig.TokenExpiry), 195 | ); err != nil { 196 | InternalErrorResponse(w, err) 197 | } else { 198 | h.authProvider.SetCookieAuthToken(w, token) 199 | http.Redirect(w, r, "/", http.StatusFound) 200 | } 201 | 202 | } 203 | 204 | func (h *AuthHandler) GetLogoutPage(w http.ResponseWriter, r *http.Request) { 205 | h.authProvider.ClearCookieAuthToken(w) 206 | http.Redirect(w, r, "/login", http.StatusSeeOther) 207 | } 208 | 209 | func (h *AuthHandler) GetOIDCPage(w http.ResponseWriter, r *http.Request) { 210 | state := uuid.New().String() 211 | http.SetCookie(w, &http.Cookie{ 212 | Name: "OpenIdState", 213 | Path: "/sso/oidc", 214 | Value: state, 215 | HttpOnly: true, 216 | Secure: h.appConfig.SecureMode(), 217 | }) 218 | http.Redirect(w, r, h.OAuth2Config.AuthCodeURL(state), http.StatusSeeOther) 219 | } 220 | 221 | func (h *AuthHandler) GetOIDCCallbackPage(w http.ResponseWriter, r *http.Request) { 222 | state, err := r.Cookie("OpenIdState") 223 | if err != nil { 224 | http.Error(w, "state cookie not found", http.StatusBadRequest) 225 | return 226 | } 227 | http.SetCookie(w, &http.Cookie{ 228 | Name: "OpenIdState", 229 | Path: "/sso/oidc", 230 | Value: "", 231 | Expires: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), 232 | HttpOnly: true, 233 | Secure: h.appConfig.SecureMode(), 234 | }) 235 | if r.URL.Query().Get("state") != state.Value { 236 | http.Error(w, "state did not match", http.StatusBadRequest) 237 | return 238 | } 239 | 240 | oauth2Token, err := h.OAuth2Config.Exchange(context.Background(), r.URL.Query().Get("code")) 241 | if err != nil { 242 | InternalErrorResponse(w, err) 243 | return 244 | } 245 | userInfo, err := h.OIDCProvider.UserInfo(context.Background(), oauth2.StaticTokenSource(oauth2Token)) 246 | if err != nil { 247 | InternalErrorResponse(w, err) 248 | return 249 | } 250 | var userClaims OIDCUserClaims 251 | if err := userInfo.Claims(&userClaims); err != nil { 252 | InternalErrorResponse(w, err) 253 | return 254 | } 255 | 256 | oidcUser := core.OIDCUserWithUsername{ 257 | Username: userClaims.PreferredUsername, 258 | OIDCUser: core.OIDCUser{ 259 | ClientID: h.appConfig.OIDC.ClientID, 260 | Subject: userClaims.Subject, 261 | }, 262 | } 263 | 264 | if err := h.validator.Struct(oidcUser); err != nil { 265 | s := h.sessionProvider.GetSession(r) 266 | s.AddFlash(middleware.CreateErrorFlash("given details are invalid, maybe the username is not compatible?")) 267 | s.Save(r, w) // TODO handle error 268 | http.Redirect(w, r, "/login", http.StatusSeeOther) 269 | return 270 | } 271 | 272 | var user database.User 273 | if h.appConfig.SignupEnabled { 274 | user, err = h.service.GetOrCreateOIDCUser(oidcUser) 275 | } else { 276 | user, err = h.service.GetOIDCUser(oidcUser.OIDCUser) 277 | } 278 | if err != nil { 279 | if errors.Is(err, services.ErrConflict) { 280 | s := h.sessionProvider.GetSession(r) 281 | s.AddFlash(middleware.CreateErrorFlash("user with that username already exists")) 282 | s.Save(r, w) // TODO handle error 283 | http.Redirect(w, r, "/login", http.StatusSeeOther) 284 | } else if errors.Is(err, services.ErrNotFound) && !h.appConfig.SignupEnabled { 285 | s := h.sessionProvider.GetSession(r) 286 | s.AddFlash(middleware.CreateErrorFlash("user not found and new accounts are disabled")) 287 | s.Save(r, w) // TODO handle error 288 | http.Redirect(w, r, "/login", http.StatusSeeOther) 289 | } else { 290 | InternalErrorResponse(w, err) 291 | } 292 | return 293 | } 294 | 295 | if token, err := core.CreateAuthenticationToken( 296 | user.Username, 297 | h.appConfig.TokenSecret, 298 | time.Duration(int64(time.Second)*h.appConfig.TokenExpiry), 299 | ); err != nil { 300 | InternalErrorResponse(w, err) 301 | } else { 302 | h.authProvider.SetCookieAuthToken(w, token) 303 | http.Redirect(w, r, "/", http.StatusFound) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /app/handlers/aux.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/enchant97/hasty-paste/app/services" 7 | "github.com/go-chi/chi/v5" 8 | ) 9 | 10 | type AuxHandler struct { 11 | service services.AuxService 12 | } 13 | 14 | func (h AuxHandler) Setup( 15 | r *chi.Mux, 16 | service services.AuxService, 17 | ) { 18 | h = AuxHandler{ 19 | service: service, 20 | } 21 | r.Get("/_/is-healthy", h.GetHealthCheck) 22 | } 23 | 24 | func (h *AuxHandler) GetHealthCheck(w http.ResponseWriter, r *http.Request) { 25 | if h.service.IsHealthy() { 26 | w.WriteHeader(http.StatusNoContent) 27 | } else { 28 | w.WriteHeader(http.StatusInternalServerError) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/handlers/home.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "time" 10 | 11 | "github.com/a-h/templ" 12 | "github.com/enchant97/hasty-paste/app/components" 13 | "github.com/enchant97/hasty-paste/app/core" 14 | "github.com/enchant97/hasty-paste/app/middleware" 15 | "github.com/enchant97/hasty-paste/app/services" 16 | "github.com/go-chi/chi/v5" 17 | "github.com/go-playground/validator/v10" 18 | "github.com/google/uuid" 19 | ) 20 | 21 | type HomeHandler struct { 22 | service services.HomeService 23 | validator *validator.Validate 24 | authProvider *middleware.AuthenticationProvider 25 | sessionProvider *middleware.SessionProvider 26 | appConfig *core.AppConfig 27 | } 28 | 29 | func (h HomeHandler) Setup( 30 | r *chi.Mux, 31 | service services.HomeService, 32 | v *validator.Validate, 33 | ap *middleware.AuthenticationProvider, 34 | sp *middleware.SessionProvider, 35 | appConfig *core.AppConfig, 36 | ) { 37 | h = HomeHandler{ 38 | service: service, 39 | validator: v, 40 | authProvider: ap, 41 | sessionProvider: sp, 42 | appConfig: appConfig, 43 | } 44 | r.Get("/", h.GetHomePage) 45 | r.Get("/~/{pasteID}", h.GetPasteIDRedirect) 46 | r.Get("/new", h.GetNewPastePage) 47 | r.Post("/new/_post", h.PostNewPastePage) 48 | } 49 | 50 | func (h *HomeHandler) GetHomePage(w http.ResponseWriter, r *http.Request) { 51 | if latestPastes, err := h.service.GetLatestPublicPastes(); err != nil { 52 | InternalErrorResponse(w, err) 53 | } else { 54 | templ.Handler(components.IndexPage(latestPastes)).ServeHTTP(w, r) 55 | } 56 | } 57 | 58 | func (h *HomeHandler) GetPasteIDRedirect(w http.ResponseWriter, r *http.Request) { 59 | pasteID, err := uuid.Parse(r.PathValue("pasteID")) 60 | if err != nil { 61 | NotFoundErrorResponse(w, r) 62 | return 63 | } 64 | if parts, err := h.service.GetPastePathPartsByPasteID(pasteID); err != nil { 65 | if errors.Is(err, services.ErrNotFound) { 66 | NotFoundErrorResponse(w, r) 67 | } else { 68 | InternalErrorResponse(w, err) 69 | } 70 | } else { 71 | p, err := url.JoinPath("/@/", parts.Username, parts.Slug) 72 | if err != nil { 73 | InternalErrorResponse(w, err) 74 | } else { 75 | http.Redirect(w, r, p, http.StatusTemporaryRedirect) 76 | } 77 | } 78 | } 79 | 80 | func (h *HomeHandler) GetNewPastePage(w http.ResponseWriter, r *http.Request) { 81 | if !h.appConfig.AnonymousPastesEnabled && h.authProvider.IsCurrentUserAnonymous(r) { 82 | http.Redirect(w, r, "/login", http.StatusSeeOther) 83 | return 84 | } 85 | templ.Handler(components.NewPastePage(h.appConfig.AttachmentsEnabled)).ServeHTTP(w, r) 86 | } 87 | 88 | func (h *HomeHandler) PostNewPastePage(w http.ResponseWriter, r *http.Request) { 89 | // ensure current user can create new pastes 90 | if !h.appConfig.AnonymousPastesEnabled && h.authProvider.IsCurrentUserAnonymous(r) { 91 | http.Redirect(w, r, "/login", http.StatusSeeOther) 92 | return 93 | } 94 | // impose max body size limit 95 | r.Body = http.MaxBytesReader(w, r.Body, h.appConfig.MaxPasteSize) 96 | // parse the form 97 | if err := r.ParseMultipartForm(1048576); err != nil { 98 | if _, ok := err.(*http.MaxBytesError); ok { 99 | http.Error(w, "Content Too Large", http.StatusRequestEntityTooLarge) 100 | } else { 101 | http.Error(w, "Bad Request", http.StatusBadRequest) 102 | } 103 | return 104 | } 105 | // Process all given attachments (if enabled) 106 | var attachments []core.NewPasteFormAttachment 107 | if h.appConfig.AttachmentsEnabled { 108 | attachments = make([]core.NewPasteFormAttachment, len(r.MultipartForm.File["pasteAttachmentFile[]"])) 109 | for i, fileHeader := range r.MultipartForm.File["pasteAttachmentFile[]"] { 110 | attachment := core.NewPasteFormAttachment{ 111 | Slug: strings.Trim(fileHeader.Filename, " "), 112 | Size: fileHeader.Size, 113 | Type: fileHeader.Header.Get("Content-Type"), 114 | Open: fileHeader.Open, 115 | } 116 | attachments[i] = attachment 117 | } 118 | } 119 | 120 | pasteSlug := r.PostFormValue("pasteSlug") 121 | if pasteSlug == "" { 122 | pasteSlug = core.GenerateRandomSlug(h.appConfig.RandomSlugLength) 123 | } 124 | 125 | visibility := r.PostFormValue("pasteVisibility") 126 | if h.authProvider.IsCurrentUserAnonymous(r) { 127 | visibility = "public" 128 | } 129 | 130 | var expiry *time.Time 131 | 132 | if v := r.PostFormValue("pasteExpiry"); v != "" { 133 | if t, err := time.Parse("2006-01-02T15:04", v); err != nil { 134 | http.Error(w, "Bad Request", http.StatusBadRequest) 135 | return 136 | } else { 137 | expiry = &t 138 | } 139 | } 140 | 141 | form := core.NewPasteForm{ 142 | Slug: strings.Trim(pasteSlug, " "), 143 | Content: r.PostFormValue("pasteContent"), 144 | ContentFormat: r.PostFormValue("pasteContentFormat"), 145 | Visibility: visibility, 146 | Expiry: expiry, 147 | Attachments: attachments, 148 | } 149 | 150 | if err := h.validator.Struct(form); err != nil { 151 | http.Error(w, "Bad Request", http.StatusBadRequest) 152 | return 153 | } 154 | 155 | if pasteID, err := h.service.NewPaste(h.authProvider.GetCurrentUserID(r), form); err != nil { 156 | if errors.Is(err, services.ErrConflict) { 157 | s := h.sessionProvider.GetSession(r) 158 | s.AddFlash(middleware.CreateErrorFlash("paste with that slug already exists")) 159 | s.Save(r, w) // TODO handle error 160 | http.Redirect(w, r, "/new", http.StatusFound) 161 | } else { 162 | InternalErrorResponse(w, err) 163 | } 164 | return 165 | } else { 166 | http.Redirect(w, r, fmt.Sprintf("/~/%s", pasteID.String()), http.StatusSeeOther) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/handlers/user.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/a-h/templ" 9 | "github.com/enchant97/hasty-paste/app/components" 10 | "github.com/enchant97/hasty-paste/app/middleware" 11 | "github.com/enchant97/hasty-paste/app/services" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/go-playground/validator/v10" 14 | ) 15 | 16 | type UserHandler struct { 17 | service services.UserService 18 | validator *validator.Validate 19 | authProvider *middleware.AuthenticationProvider 20 | sessionProvider *middleware.SessionProvider 21 | } 22 | 23 | func (h UserHandler) Setup( 24 | r *chi.Mux, 25 | service services.UserService, 26 | v *validator.Validate, 27 | ap *middleware.AuthenticationProvider, 28 | sp *middleware.SessionProvider, 29 | ) { 30 | h = UserHandler{ 31 | service: service, 32 | validator: v, 33 | authProvider: ap, 34 | sessionProvider: sp, 35 | } 36 | r.Get("/@/{username}", h.GetPastes) 37 | r.Get("/@/{username}/{pasteSlug}", h.GetPaste) 38 | r.Get("/@/{username}/{pasteSlug}/{attachmentSlug}", h.GetPasteAttachment) 39 | } 40 | 41 | func (h *UserHandler) GetPastes(w http.ResponseWriter, r *http.Request) { 42 | username := r.PathValue("username") 43 | if pastes, err := h.service.GetPastes(h.authProvider.GetCurrentUserID(r), username); err != nil { 44 | InternalErrorResponse(w, err) 45 | } else { 46 | templ.Handler(components.PastesPage(username, pastes)).ServeHTTP(w, r) 47 | } 48 | } 49 | 50 | func (h *UserHandler) GetPaste(w http.ResponseWriter, r *http.Request) { 51 | username := r.PathValue("username") 52 | pasteSlug := r.PathValue("pasteSlug") 53 | paste, err := h.service.GetPaste(h.authProvider.GetCurrentUserID(r), username, pasteSlug) 54 | if err != nil { 55 | if errors.Is(err, services.ErrNotFound) { 56 | NotFoundErrorResponse(w, r) 57 | } else { 58 | InternalErrorResponse(w, err) 59 | } 60 | return 61 | } 62 | attachments, err := h.service.GetPasteAttachments(paste.ID) 63 | if err != nil { 64 | InternalErrorResponse(w, err) 65 | return 66 | } 67 | templ.Handler(components.PastePage(username, paste, attachments)).ServeHTTP(w, r) 68 | } 69 | 70 | func (h *UserHandler) GetPasteAttachment(w http.ResponseWriter, r *http.Request) { 71 | username := r.PathValue("username") 72 | pasteSlug := r.PathValue("pasteSlug") 73 | attachmentSlug := r.PathValue("attachmentSlug") 74 | 75 | attachment, attachmentReader, err := h.service.GetPasteAttachment( 76 | h.authProvider.GetCurrentUserID(r), 77 | username, 78 | pasteSlug, 79 | attachmentSlug, 80 | ) 81 | if err != nil { 82 | if errors.Is(err, services.ErrNotFound) { 83 | NotFoundErrorResponse(w, r) 84 | } else { 85 | InternalErrorResponse(w, err) 86 | } 87 | return 88 | } 89 | defer attachmentReader.Close() 90 | w.Header().Add("Content-Type", attachment.MimeType) 91 | w.Header().Add("ETag", attachment.Checksum) 92 | w.WriteHeader(http.StatusOK) 93 | io.Copy(w, attachmentReader) 94 | } 95 | -------------------------------------------------------------------------------- /app/handlers/utils.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/a-h/templ" 8 | "github.com/enchant97/hasty-paste/app/components" 9 | ) 10 | 11 | func InternalErrorResponse(w http.ResponseWriter, err error) { 12 | log.Println(err) 13 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 14 | } 15 | 16 | func NotFoundErrorResponse(w http.ResponseWriter, r *http.Request) { 17 | w.WriteHeader(http.StatusNotFound) 18 | templ.Handler(components.NotFoundPage()).ServeHTTP(w, r) 19 | } 20 | -------------------------------------------------------------------------------- /app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "strings" 11 | 12 | "github.com/go-chi/chi/v5" 13 | "github.com/go-chi/chi/v5/middleware" 14 | "github.com/go-playground/validator/v10" 15 | _ "modernc.org/sqlite" 16 | 17 | "github.com/enchant97/hasty-paste/app/core" 18 | "github.com/enchant97/hasty-paste/app/database" 19 | "github.com/enchant97/hasty-paste/app/handlers" 20 | app_middleware "github.com/enchant97/hasty-paste/app/middleware" 21 | "github.com/enchant97/hasty-paste/app/migrations" 22 | "github.com/enchant97/hasty-paste/app/services" 23 | "github.com/enchant97/hasty-paste/app/storage" 24 | ) 25 | 26 | func main() { 27 | var appConfig core.AppConfig 28 | if err := appConfig.ParseConfig(); err != nil { 29 | panic(err) 30 | } 31 | if len(os.Args) == 2 && os.Args[1] == "health-check" { 32 | resp, err := http.Get(fmt.Sprintf("http://localhost:%d/_/is-healthy", appConfig.Bind.Port)) 33 | if err != nil || resp.StatusCode >= 400 { 34 | fmt.Println("health-check failed") 35 | os.Exit(1) 36 | } 37 | defer resp.Body.Close() 38 | fmt.Println("health-check successful") 39 | os.Exit(0) 40 | } 41 | sc, err := storage.StorageController{}.New(appConfig.AttachmentsPath) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | if err := migrations.MigrateDB(appConfig.DbUri); err != nil { 47 | panic(err) 48 | } 49 | db, err := sql.Open("sqlite", strings.Split(appConfig.DbUri, "sqlite://")[1]) 50 | if err != nil { 51 | panic(err) 52 | } 53 | dbQueries := database.New(db) 54 | if err := dbQueries.InsertAnonymousUser(context.Background(), core.NewUUID()); err != nil { 55 | panic(err) 56 | } 57 | dao := core.DAO{}.New(db, dbQueries) 58 | validate := validator.New(validator.WithRequiredStructEnabled()) 59 | 60 | devProvider := app_middleware.ViteProvider{}.New(appConfig.Dev) 61 | configProvider := app_middleware.AppConfigProvider{}.New(appConfig) 62 | authenticationProvider := app_middleware.AuthenticationProvider{}.New(appConfig.SecureMode(), appConfig.TokenSecret, &dao) 63 | sessionProvider := app_middleware.SessionProvider{}.New(appConfig.SecureMode(), appConfig.SessionSecret) 64 | 65 | r := chi.NewRouter() 66 | if appConfig.BehindProxy { 67 | r.Use(middleware.RealIP) 68 | } 69 | r.Use(middleware.Logger) 70 | r.Use(middleware.Recoverer) 71 | r.Use(devProvider.ProviderMiddleware) 72 | r.Use(configProvider.ProviderMiddleware) 73 | r.Use(authenticationProvider.ProviderMiddleware) 74 | r.Use(sessionProvider.ProviderMiddleware) 75 | 76 | handlers.AuxHandler{}.Setup(r, services.AuxService{}.New(&dao)) 77 | devProvider.SetupHandlers(r) 78 | handlers.HomeHandler{}.Setup(r, services.HomeService{}.New(&dao, &sc), validate, &authenticationProvider, &sessionProvider, &appConfig) 79 | handlers.UserHandler{}.Setup(r, services.UserService{}.New(&dao, &sc), validate, &authenticationProvider, &sessionProvider) 80 | handlers.AuthHandler{}.Setup(r, appConfig, services.AuthService{}.New(&dao), validate, &authenticationProvider, &sessionProvider) 81 | 82 | fmt.Println(` 83 | ooooo ooooo o oooooooo8 ooooooooooo ooooo oooo 84 | 888 888 888 888 88 888 88 888 88 85 | 888ooo888 8 88 888oooooo 888 888 86 | 888 888 8oooo88 888 888 888 87 | o888o o888o o88o o888o o88oooo888 o888o o888o 88 | 89 | oooooooooo o oooooooo8 ooooooooooo ooooooooooo ooooo ooooo 90 | 888 888 888 888 88 888 88 888 88 888 888 91 | 888oooo88 8 88 888oooooo 888 888ooo8 888 888 92 | 888 8oooo88 888 888 888 oo 888 888 93 | o888o o88o o888o o88oooo888 o888o o888ooo8888 o888o o888o`) 94 | fmt.Println() 95 | log.Printf("listening on: http://%s", appConfig.Bind.AsAddress()) 96 | log.Printf("public access at: %s", appConfig.PublicURL) 97 | http.ListenAndServe(appConfig.Bind.AsAddress(), r) 98 | } 99 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | import "./style.css" 2 | 3 | const flashes = document.getElementById("flashes"); 4 | for (const flash of flashes.children) { 5 | setTimeout(() => { 6 | flash.style.opacity = 0; 7 | }, 4000) 8 | } 9 | -------------------------------------------------------------------------------- /app/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "log" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/enchant97/hasty-paste/app/core" 12 | "github.com/google/uuid" 13 | ) 14 | 15 | const ( 16 | AnonymousUsername = "anonymous" 17 | ContextCurrentUsernameKey = "currentUsername" 18 | ContextCurrentUserIDKey = "currentUserID" 19 | CookieAuthTokenName = "AuthenticatedUser" 20 | ) 21 | 22 | type AuthenticationProvider struct { 23 | secureMode bool 24 | tokenSecret []byte 25 | dao *core.DAO 26 | } 27 | 28 | func (m AuthenticationProvider) New(secureMode bool, tokenSecret []byte, dao *core.DAO) AuthenticationProvider { 29 | return AuthenticationProvider{secureMode: secureMode, tokenSecret: tokenSecret, dao: dao} 30 | } 31 | 32 | func (m *AuthenticationProvider) ProviderMiddleware(next http.Handler) http.Handler { 33 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | currentUsername := AnonymousUsername 35 | cookie, err := r.Cookie(CookieAuthTokenName) 36 | if err == nil { 37 | if username, err := core.ParseAuthenticationToken(cookie.Value, m.tokenSecret); err != nil { 38 | // invalid authentication token 39 | m.ClearCookieAuthToken(w) 40 | http.Redirect(w, r, "/login", http.StatusSeeOther) 41 | return 42 | } else { 43 | currentUsername = username 44 | } 45 | } 46 | 47 | if user, err := m.dao.Queries.GetUserByUsername(context.Background(), currentUsername); err != nil { 48 | if errors.Is(err, sql.ErrNoRows) { 49 | // unknown user 50 | m.ClearCookieAuthToken(w) 51 | http.Redirect(w, r, "/login", http.StatusSeeOther) 52 | 53 | } else { 54 | log.Println(err) 55 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 56 | } 57 | return 58 | } else { 59 | ctx := context.WithValue(r.Context(), ContextCurrentUsernameKey, currentUsername) 60 | ctx = context.WithValue(ctx, ContextCurrentUserIDKey, user.ID) 61 | next.ServeHTTP(w, r.WithContext(ctx)) 62 | } 63 | }) 64 | } 65 | 66 | func (m *AuthenticationProvider) RequireAuthenticationMiddleware(next http.Handler) http.Handler { 67 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 68 | if r.Context().Value(ContextCurrentUsernameKey) == AnonymousUsername { 69 | http.Redirect(w, r, "/login", http.StatusSeeOther) 70 | return 71 | } 72 | next.ServeHTTP(w, r) 73 | }) 74 | } 75 | 76 | func (m *AuthenticationProvider) RequireNoAuthenticationMiddleware(next http.Handler) http.Handler { 77 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 78 | if r.Context().Value(ContextCurrentUsernameKey) != AnonymousUsername { 79 | http.Redirect(w, r, "/", http.StatusSeeOther) 80 | return 81 | } 82 | next.ServeHTTP(w, r) 83 | }) 84 | } 85 | 86 | func (m *AuthenticationProvider) GetCurrentUsername(r *http.Request) string { 87 | return r.Context().Value(ContextCurrentUsernameKey).(string) 88 | } 89 | 90 | func (m *AuthenticationProvider) GetCurrentUserID(r *http.Request) uuid.UUID { 91 | return r.Context().Value(ContextCurrentUserIDKey).(uuid.UUID) 92 | } 93 | 94 | func (m *AuthenticationProvider) IsCurrentUserAnonymous(r *http.Request) bool { 95 | return m.GetCurrentUsername(r) == AnonymousUsername 96 | } 97 | 98 | func (m *AuthenticationProvider) SetCookieAuthToken(w http.ResponseWriter, token core.AuthenticationToken) { 99 | http.SetCookie(w, &http.Cookie{ 100 | Name: CookieAuthTokenName, 101 | Path: "/", 102 | Value: token.TokenContent, 103 | Expires: token.ExpiresAt, 104 | HttpOnly: true, 105 | Secure: m.secureMode, 106 | }) 107 | } 108 | 109 | func (m *AuthenticationProvider) ClearCookieAuthToken(w http.ResponseWriter) { 110 | http.SetCookie(w, &http.Cookie{ 111 | Name: CookieAuthTokenName, 112 | Path: "/", 113 | Value: "", 114 | Expires: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), 115 | HttpOnly: true, 116 | Secure: m.secureMode, 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /app/middleware/config.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/enchant97/hasty-paste/app/core" 8 | ) 9 | 10 | const ( 11 | ContextConfigKey = "appConfig" 12 | ) 13 | 14 | type AppConfigProvider struct { 15 | config core.AppConfig 16 | } 17 | 18 | func (m AppConfigProvider) New(config core.AppConfig) AppConfigProvider { 19 | return AppConfigProvider{ 20 | config: config, 21 | } 22 | } 23 | 24 | func (m *AppConfigProvider) ProviderMiddleware(next http.Handler) http.Handler { 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | ctx := context.WithValue(r.Context(), ContextConfigKey, m.config) 27 | next.ServeHTTP(w, r.WithContext(ctx)) 28 | }) 29 | } 30 | 31 | func GetAppConfig(ctx context.Context) core.AppConfig { 32 | return ctx.Value(ContextConfigKey).(core.AppConfig) 33 | } 34 | -------------------------------------------------------------------------------- /app/middleware/session.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "encoding/gob" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/gorilla/sessions" 10 | ) 11 | 12 | type FlashType string 13 | 14 | const ( 15 | ContextSessionKey = "session" 16 | ContextSessionFlashesKey = "flashes" 17 | FlashTypeOk = "ok" 18 | FlashTypeError = "error" 19 | ) 20 | 21 | type Flash struct { 22 | Type FlashType 23 | Message string 24 | } 25 | 26 | type SessionProvider struct { 27 | store sessions.Store 28 | } 29 | 30 | func (m SessionProvider) New(secureMode bool, sessionSecret []byte) SessionProvider { 31 | gob.Register(Flash{}) 32 | store := sessions.NewCookieStore(sessionSecret) 33 | store.Options.SameSite = http.SameSiteDefaultMode 34 | store.Options.HttpOnly = true 35 | store.Options.Secure = secureMode 36 | return SessionProvider{store: store} 37 | } 38 | 39 | func (m *SessionProvider) ProviderMiddleware(next http.Handler) http.Handler { 40 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 41 | s := m.GetSession(r) 42 | ctx := context.WithValue(r.Context(), ContextSessionKey, s) 43 | ctx = context.WithValue(r.Context(), ContextSessionFlashesKey, s.Flashes()) 44 | if err := s.Save(r, w); err != nil { 45 | log.Println(err) 46 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 47 | return 48 | } 49 | next.ServeHTTP(w, r.WithContext(ctx)) 50 | }) 51 | } 52 | 53 | func (m *SessionProvider) GetSession(r *http.Request) *sessions.Session { 54 | s, _ := m.store.Get(r, "session") 55 | return s 56 | } 57 | 58 | func CreateOkFlash(message string) Flash { 59 | return Flash{ 60 | Type: FlashTypeOk, 61 | Message: message, 62 | } 63 | } 64 | 65 | func CreateErrorFlash(message string) Flash { 66 | return Flash{ 67 | Type: FlashTypeError, 68 | Message: message, 69 | } 70 | } 71 | 72 | func GetFlashes(ctx context.Context) []Flash { 73 | rawValues := ctx.Value(ContextSessionFlashesKey).([]interface{}) 74 | f := make([]Flash, len(rawValues)) 75 | for i, v := range rawValues { 76 | f[i] = v.(Flash) 77 | } 78 | return f 79 | } 80 | -------------------------------------------------------------------------------- /app/middleware/vite.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path" 10 | "strings" 11 | 12 | "github.com/enchant97/hasty-paste/app/core" 13 | "github.com/go-chi/chi/v5" 14 | ) 15 | 16 | const ContextViteHeadKey = "viteHead" 17 | 18 | type ViteManifestSection struct { 19 | File string `json:"file"` 20 | Css []string `json:"css"` 21 | } 22 | 23 | type ViteProvider struct { 24 | devConfig core.DevConfig 25 | scripts []string 26 | css []string 27 | } 28 | 29 | func (m ViteProvider) New(devConfig core.DevConfig) ViteProvider { 30 | scriptFiles := make([]string, 0) 31 | cssFiles := make([]string, 0) 32 | if !devConfig.Enabled { 33 | rawContent, err := os.ReadFile(path.Join(devConfig.ViteDistPath, ".vite/manifest.json")) 34 | if err != nil { 35 | log.Fatalln("vite manifest could not be found") 36 | } 37 | var viteManifest map[string]ViteManifestSection 38 | if err := json.Unmarshal(rawContent, &viteManifest); err != nil { 39 | log.Fatalln("vite manifest could not be loaded") 40 | } 41 | for _, section := range viteManifest { 42 | scriptFiles = append(scriptFiles, section.File) 43 | cssFiles = append(cssFiles, section.Css...) 44 | } 45 | 46 | } 47 | return ViteProvider{devConfig: devConfig, scripts: scriptFiles, css: cssFiles} 48 | } 49 | 50 | func (m *ViteProvider) ProviderMiddleware(next http.Handler) http.Handler { 51 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 | var ctx context.Context 53 | if m.devConfig.Enabled { 54 | ctx = context.WithValue(r.Context(), ContextViteHeadKey, 55 | ` 56 | 57 | `) 58 | } else { 59 | builder := strings.Builder{} 60 | for _, cssFile := range m.css { 61 | builder.WriteString(``) 62 | } 63 | for _, scriptFile := range m.scripts { 64 | builder.WriteString(``) 65 | } 66 | builder.WriteString(``) 67 | ctx = context.WithValue(r.Context(), ContextViteHeadKey, builder.String()) 68 | } 69 | next.ServeHTTP(w, r.WithContext(ctx)) 70 | }) 71 | } 72 | 73 | func (m *ViteProvider) SetupHandlers(r *chi.Mux) { 74 | fs := http.FileServer(http.Dir(m.devConfig.ViteDistPath)) 75 | r.Handle("/*", fs) 76 | } 77 | -------------------------------------------------------------------------------- /app/migrations/20241129_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | users ( 3 | id UUID PRIMARY KEY, 4 | username TEXT NOT NULL UNIQUE, 5 | password_hash BLOB, 6 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 7 | ); 8 | 9 | CREATE TABLE 10 | oidc_users ( 11 | id INTEGER PRIMARY KEY, 12 | user_id UUID NOT NULL, 13 | client_id TEXT NOT NULL, 14 | user_sub TEXT NOT NULL, 15 | UNIQUE (user_id, client_id), 16 | UNIQUE (client_id, user_sub), 17 | FOREIGN KEY (user_id) REFERENCES users (id) 18 | ); 19 | 20 | CREATE UNIQUE INDEX oidc_client_sub_idx ON oidc_users (client_id, user_sub); 21 | 22 | CREATE TABLE 23 | pastes ( 24 | id UUID PRIMARY KEY, 25 | owner_id UUID NOT NULL, 26 | slug TEXT NOT NULL, 27 | content TEXT NOT NULL, 28 | content_format TEXT NOT NULL DEFAULT 'plain', 29 | visibility TEXT NOT NULL DEFAULT 'public', 30 | expires_at TIMESTAMP, 31 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 32 | UNIQUE (owner_id, slug), 33 | FOREIGN KEY (owner_id) REFERENCES users (id) 34 | ); 35 | 36 | CREATE TABLE 37 | attachments ( 38 | id UUID PRIMARY KEY, 39 | paste_id UUID NOT NULL, 40 | slug TEXT NOT NULL, 41 | mime_type TEXT NOT NULL DEFAULT 'application/octet-stream', 42 | size INTEGER NOT NULL, 43 | checksum TEXT NOT NULL, 44 | UNIQUE (paste_id, slug), 45 | FOREIGN KEY (paste_id) REFERENCES pastes (id) 46 | ); 47 | -------------------------------------------------------------------------------- /app/migrations/utils.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | 7 | "github.com/golang-migrate/migrate/v4" 8 | _ "github.com/golang-migrate/migrate/v4/database/sqlite" 9 | "github.com/golang-migrate/migrate/v4/source/iofs" 10 | ) 11 | 12 | //go:embed *.sql 13 | var migrationsFS embed.FS 14 | 15 | func MigrateDB(connectionURI string) error { 16 | d, err := iofs.New(migrationsFS, ".") 17 | if err != nil { 18 | return err 19 | } 20 | m, err := migrate.NewWithSourceInstance("iofs", d, connectionURI) 21 | if err != nil { 22 | return err 23 | } 24 | if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { 25 | return err 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hasty-paste", 3 | "version": "2.1.0", 4 | "description": "", 5 | "private": true, 6 | "license": "AGPL-3.0-only", 7 | "type": "module", 8 | "scripts": { 9 | "dev": "vite", 10 | "build": "vite build", 11 | "serve": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@tailwindcss/vite": "^4.1.4", 15 | "tailwindcss": "^4.1.4" 16 | }, 17 | "devDependencies": { 18 | "@tailwindcss/typography": "^0.5.16", 19 | "vite": "^6.3.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@tailwindcss/vite': 12 | specifier: ^4.1.4 13 | version: 4.1.4(vite@6.3.2(jiti@2.4.2)(lightningcss@1.29.2)) 14 | tailwindcss: 15 | specifier: ^4.1.4 16 | version: 4.1.4 17 | devDependencies: 18 | '@tailwindcss/typography': 19 | specifier: ^0.5.16 20 | version: 0.5.16(tailwindcss@4.1.4) 21 | vite: 22 | specifier: ^6.3.2 23 | version: 6.3.2(jiti@2.4.2)(lightningcss@1.29.2) 24 | 25 | packages: 26 | 27 | '@esbuild/aix-ppc64@0.25.2': 28 | resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} 29 | engines: {node: '>=18'} 30 | cpu: [ppc64] 31 | os: [aix] 32 | 33 | '@esbuild/android-arm64@0.25.2': 34 | resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} 35 | engines: {node: '>=18'} 36 | cpu: [arm64] 37 | os: [android] 38 | 39 | '@esbuild/android-arm@0.25.2': 40 | resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} 41 | engines: {node: '>=18'} 42 | cpu: [arm] 43 | os: [android] 44 | 45 | '@esbuild/android-x64@0.25.2': 46 | resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} 47 | engines: {node: '>=18'} 48 | cpu: [x64] 49 | os: [android] 50 | 51 | '@esbuild/darwin-arm64@0.25.2': 52 | resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} 53 | engines: {node: '>=18'} 54 | cpu: [arm64] 55 | os: [darwin] 56 | 57 | '@esbuild/darwin-x64@0.25.2': 58 | resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} 59 | engines: {node: '>=18'} 60 | cpu: [x64] 61 | os: [darwin] 62 | 63 | '@esbuild/freebsd-arm64@0.25.2': 64 | resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} 65 | engines: {node: '>=18'} 66 | cpu: [arm64] 67 | os: [freebsd] 68 | 69 | '@esbuild/freebsd-x64@0.25.2': 70 | resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} 71 | engines: {node: '>=18'} 72 | cpu: [x64] 73 | os: [freebsd] 74 | 75 | '@esbuild/linux-arm64@0.25.2': 76 | resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} 77 | engines: {node: '>=18'} 78 | cpu: [arm64] 79 | os: [linux] 80 | 81 | '@esbuild/linux-arm@0.25.2': 82 | resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} 83 | engines: {node: '>=18'} 84 | cpu: [arm] 85 | os: [linux] 86 | 87 | '@esbuild/linux-ia32@0.25.2': 88 | resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} 89 | engines: {node: '>=18'} 90 | cpu: [ia32] 91 | os: [linux] 92 | 93 | '@esbuild/linux-loong64@0.25.2': 94 | resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} 95 | engines: {node: '>=18'} 96 | cpu: [loong64] 97 | os: [linux] 98 | 99 | '@esbuild/linux-mips64el@0.25.2': 100 | resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} 101 | engines: {node: '>=18'} 102 | cpu: [mips64el] 103 | os: [linux] 104 | 105 | '@esbuild/linux-ppc64@0.25.2': 106 | resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} 107 | engines: {node: '>=18'} 108 | cpu: [ppc64] 109 | os: [linux] 110 | 111 | '@esbuild/linux-riscv64@0.25.2': 112 | resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} 113 | engines: {node: '>=18'} 114 | cpu: [riscv64] 115 | os: [linux] 116 | 117 | '@esbuild/linux-s390x@0.25.2': 118 | resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} 119 | engines: {node: '>=18'} 120 | cpu: [s390x] 121 | os: [linux] 122 | 123 | '@esbuild/linux-x64@0.25.2': 124 | resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} 125 | engines: {node: '>=18'} 126 | cpu: [x64] 127 | os: [linux] 128 | 129 | '@esbuild/netbsd-arm64@0.25.2': 130 | resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} 131 | engines: {node: '>=18'} 132 | cpu: [arm64] 133 | os: [netbsd] 134 | 135 | '@esbuild/netbsd-x64@0.25.2': 136 | resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} 137 | engines: {node: '>=18'} 138 | cpu: [x64] 139 | os: [netbsd] 140 | 141 | '@esbuild/openbsd-arm64@0.25.2': 142 | resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} 143 | engines: {node: '>=18'} 144 | cpu: [arm64] 145 | os: [openbsd] 146 | 147 | '@esbuild/openbsd-x64@0.25.2': 148 | resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} 149 | engines: {node: '>=18'} 150 | cpu: [x64] 151 | os: [openbsd] 152 | 153 | '@esbuild/sunos-x64@0.25.2': 154 | resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} 155 | engines: {node: '>=18'} 156 | cpu: [x64] 157 | os: [sunos] 158 | 159 | '@esbuild/win32-arm64@0.25.2': 160 | resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} 161 | engines: {node: '>=18'} 162 | cpu: [arm64] 163 | os: [win32] 164 | 165 | '@esbuild/win32-ia32@0.25.2': 166 | resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} 167 | engines: {node: '>=18'} 168 | cpu: [ia32] 169 | os: [win32] 170 | 171 | '@esbuild/win32-x64@0.25.2': 172 | resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} 173 | engines: {node: '>=18'} 174 | cpu: [x64] 175 | os: [win32] 176 | 177 | '@rollup/rollup-android-arm-eabi@4.40.0': 178 | resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} 179 | cpu: [arm] 180 | os: [android] 181 | 182 | '@rollup/rollup-android-arm64@4.40.0': 183 | resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} 184 | cpu: [arm64] 185 | os: [android] 186 | 187 | '@rollup/rollup-darwin-arm64@4.40.0': 188 | resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} 189 | cpu: [arm64] 190 | os: [darwin] 191 | 192 | '@rollup/rollup-darwin-x64@4.40.0': 193 | resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} 194 | cpu: [x64] 195 | os: [darwin] 196 | 197 | '@rollup/rollup-freebsd-arm64@4.40.0': 198 | resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} 199 | cpu: [arm64] 200 | os: [freebsd] 201 | 202 | '@rollup/rollup-freebsd-x64@4.40.0': 203 | resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} 204 | cpu: [x64] 205 | os: [freebsd] 206 | 207 | '@rollup/rollup-linux-arm-gnueabihf@4.40.0': 208 | resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} 209 | cpu: [arm] 210 | os: [linux] 211 | 212 | '@rollup/rollup-linux-arm-musleabihf@4.40.0': 213 | resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} 214 | cpu: [arm] 215 | os: [linux] 216 | 217 | '@rollup/rollup-linux-arm64-gnu@4.40.0': 218 | resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} 219 | cpu: [arm64] 220 | os: [linux] 221 | 222 | '@rollup/rollup-linux-arm64-musl@4.40.0': 223 | resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} 224 | cpu: [arm64] 225 | os: [linux] 226 | 227 | '@rollup/rollup-linux-loongarch64-gnu@4.40.0': 228 | resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} 229 | cpu: [loong64] 230 | os: [linux] 231 | 232 | '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': 233 | resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} 234 | cpu: [ppc64] 235 | os: [linux] 236 | 237 | '@rollup/rollup-linux-riscv64-gnu@4.40.0': 238 | resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} 239 | cpu: [riscv64] 240 | os: [linux] 241 | 242 | '@rollup/rollup-linux-riscv64-musl@4.40.0': 243 | resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} 244 | cpu: [riscv64] 245 | os: [linux] 246 | 247 | '@rollup/rollup-linux-s390x-gnu@4.40.0': 248 | resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} 249 | cpu: [s390x] 250 | os: [linux] 251 | 252 | '@rollup/rollup-linux-x64-gnu@4.40.0': 253 | resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} 254 | cpu: [x64] 255 | os: [linux] 256 | 257 | '@rollup/rollup-linux-x64-musl@4.40.0': 258 | resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} 259 | cpu: [x64] 260 | os: [linux] 261 | 262 | '@rollup/rollup-win32-arm64-msvc@4.40.0': 263 | resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} 264 | cpu: [arm64] 265 | os: [win32] 266 | 267 | '@rollup/rollup-win32-ia32-msvc@4.40.0': 268 | resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} 269 | cpu: [ia32] 270 | os: [win32] 271 | 272 | '@rollup/rollup-win32-x64-msvc@4.40.0': 273 | resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} 274 | cpu: [x64] 275 | os: [win32] 276 | 277 | '@tailwindcss/node@4.1.4': 278 | resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==} 279 | 280 | '@tailwindcss/oxide-android-arm64@4.1.4': 281 | resolution: {integrity: sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==} 282 | engines: {node: '>= 10'} 283 | cpu: [arm64] 284 | os: [android] 285 | 286 | '@tailwindcss/oxide-darwin-arm64@4.1.4': 287 | resolution: {integrity: sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==} 288 | engines: {node: '>= 10'} 289 | cpu: [arm64] 290 | os: [darwin] 291 | 292 | '@tailwindcss/oxide-darwin-x64@4.1.4': 293 | resolution: {integrity: sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==} 294 | engines: {node: '>= 10'} 295 | cpu: [x64] 296 | os: [darwin] 297 | 298 | '@tailwindcss/oxide-freebsd-x64@4.1.4': 299 | resolution: {integrity: sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==} 300 | engines: {node: '>= 10'} 301 | cpu: [x64] 302 | os: [freebsd] 303 | 304 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4': 305 | resolution: {integrity: sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==} 306 | engines: {node: '>= 10'} 307 | cpu: [arm] 308 | os: [linux] 309 | 310 | '@tailwindcss/oxide-linux-arm64-gnu@4.1.4': 311 | resolution: {integrity: sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==} 312 | engines: {node: '>= 10'} 313 | cpu: [arm64] 314 | os: [linux] 315 | 316 | '@tailwindcss/oxide-linux-arm64-musl@4.1.4': 317 | resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==} 318 | engines: {node: '>= 10'} 319 | cpu: [arm64] 320 | os: [linux] 321 | 322 | '@tailwindcss/oxide-linux-x64-gnu@4.1.4': 323 | resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==} 324 | engines: {node: '>= 10'} 325 | cpu: [x64] 326 | os: [linux] 327 | 328 | '@tailwindcss/oxide-linux-x64-musl@4.1.4': 329 | resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==} 330 | engines: {node: '>= 10'} 331 | cpu: [x64] 332 | os: [linux] 333 | 334 | '@tailwindcss/oxide-wasm32-wasi@4.1.4': 335 | resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==} 336 | engines: {node: '>=14.0.0'} 337 | cpu: [wasm32] 338 | bundledDependencies: 339 | - '@napi-rs/wasm-runtime' 340 | - '@emnapi/core' 341 | - '@emnapi/runtime' 342 | - '@tybys/wasm-util' 343 | - '@emnapi/wasi-threads' 344 | - tslib 345 | 346 | '@tailwindcss/oxide-win32-arm64-msvc@4.1.4': 347 | resolution: {integrity: sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==} 348 | engines: {node: '>= 10'} 349 | cpu: [arm64] 350 | os: [win32] 351 | 352 | '@tailwindcss/oxide-win32-x64-msvc@4.1.4': 353 | resolution: {integrity: sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==} 354 | engines: {node: '>= 10'} 355 | cpu: [x64] 356 | os: [win32] 357 | 358 | '@tailwindcss/oxide@4.1.4': 359 | resolution: {integrity: sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==} 360 | engines: {node: '>= 10'} 361 | 362 | '@tailwindcss/typography@0.5.16': 363 | resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} 364 | peerDependencies: 365 | tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' 366 | 367 | '@tailwindcss/vite@4.1.4': 368 | resolution: {integrity: sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A==} 369 | peerDependencies: 370 | vite: ^5.2.0 || ^6 371 | 372 | '@types/estree@1.0.7': 373 | resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} 374 | 375 | cssesc@3.0.0: 376 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 377 | engines: {node: '>=4'} 378 | hasBin: true 379 | 380 | detect-libc@2.0.4: 381 | resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} 382 | engines: {node: '>=8'} 383 | 384 | enhanced-resolve@5.18.1: 385 | resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} 386 | engines: {node: '>=10.13.0'} 387 | 388 | esbuild@0.25.2: 389 | resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} 390 | engines: {node: '>=18'} 391 | hasBin: true 392 | 393 | fdir@6.4.4: 394 | resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} 395 | peerDependencies: 396 | picomatch: ^3 || ^4 397 | peerDependenciesMeta: 398 | picomatch: 399 | optional: true 400 | 401 | fsevents@2.3.3: 402 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 403 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 404 | os: [darwin] 405 | 406 | graceful-fs@4.2.11: 407 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 408 | 409 | jiti@2.4.2: 410 | resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} 411 | hasBin: true 412 | 413 | lightningcss-darwin-arm64@1.29.2: 414 | resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} 415 | engines: {node: '>= 12.0.0'} 416 | cpu: [arm64] 417 | os: [darwin] 418 | 419 | lightningcss-darwin-x64@1.29.2: 420 | resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} 421 | engines: {node: '>= 12.0.0'} 422 | cpu: [x64] 423 | os: [darwin] 424 | 425 | lightningcss-freebsd-x64@1.29.2: 426 | resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} 427 | engines: {node: '>= 12.0.0'} 428 | cpu: [x64] 429 | os: [freebsd] 430 | 431 | lightningcss-linux-arm-gnueabihf@1.29.2: 432 | resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} 433 | engines: {node: '>= 12.0.0'} 434 | cpu: [arm] 435 | os: [linux] 436 | 437 | lightningcss-linux-arm64-gnu@1.29.2: 438 | resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} 439 | engines: {node: '>= 12.0.0'} 440 | cpu: [arm64] 441 | os: [linux] 442 | 443 | lightningcss-linux-arm64-musl@1.29.2: 444 | resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} 445 | engines: {node: '>= 12.0.0'} 446 | cpu: [arm64] 447 | os: [linux] 448 | 449 | lightningcss-linux-x64-gnu@1.29.2: 450 | resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} 451 | engines: {node: '>= 12.0.0'} 452 | cpu: [x64] 453 | os: [linux] 454 | 455 | lightningcss-linux-x64-musl@1.29.2: 456 | resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} 457 | engines: {node: '>= 12.0.0'} 458 | cpu: [x64] 459 | os: [linux] 460 | 461 | lightningcss-win32-arm64-msvc@1.29.2: 462 | resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} 463 | engines: {node: '>= 12.0.0'} 464 | cpu: [arm64] 465 | os: [win32] 466 | 467 | lightningcss-win32-x64-msvc@1.29.2: 468 | resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} 469 | engines: {node: '>= 12.0.0'} 470 | cpu: [x64] 471 | os: [win32] 472 | 473 | lightningcss@1.29.2: 474 | resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} 475 | engines: {node: '>= 12.0.0'} 476 | 477 | lodash.castarray@4.4.0: 478 | resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} 479 | 480 | lodash.isplainobject@4.0.6: 481 | resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} 482 | 483 | lodash.merge@4.6.2: 484 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 485 | 486 | nanoid@3.3.11: 487 | resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 488 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 489 | hasBin: true 490 | 491 | picocolors@1.1.1: 492 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 493 | 494 | picomatch@4.0.2: 495 | resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 496 | engines: {node: '>=12'} 497 | 498 | postcss-selector-parser@6.0.10: 499 | resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} 500 | engines: {node: '>=4'} 501 | 502 | postcss@8.5.3: 503 | resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} 504 | engines: {node: ^10 || ^12 || >=14} 505 | 506 | rollup@4.40.0: 507 | resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} 508 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 509 | hasBin: true 510 | 511 | source-map-js@1.2.1: 512 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 513 | engines: {node: '>=0.10.0'} 514 | 515 | tailwindcss@4.1.4: 516 | resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==} 517 | 518 | tapable@2.2.1: 519 | resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} 520 | engines: {node: '>=6'} 521 | 522 | tinyglobby@0.2.13: 523 | resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} 524 | engines: {node: '>=12.0.0'} 525 | 526 | util-deprecate@1.0.2: 527 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 528 | 529 | vite@6.3.2: 530 | resolution: {integrity: sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==} 531 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 532 | hasBin: true 533 | peerDependencies: 534 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 535 | jiti: '>=1.21.0' 536 | less: '*' 537 | lightningcss: ^1.21.0 538 | sass: '*' 539 | sass-embedded: '*' 540 | stylus: '*' 541 | sugarss: '*' 542 | terser: ^5.16.0 543 | tsx: ^4.8.1 544 | yaml: ^2.4.2 545 | peerDependenciesMeta: 546 | '@types/node': 547 | optional: true 548 | jiti: 549 | optional: true 550 | less: 551 | optional: true 552 | lightningcss: 553 | optional: true 554 | sass: 555 | optional: true 556 | sass-embedded: 557 | optional: true 558 | stylus: 559 | optional: true 560 | sugarss: 561 | optional: true 562 | terser: 563 | optional: true 564 | tsx: 565 | optional: true 566 | yaml: 567 | optional: true 568 | 569 | snapshots: 570 | 571 | '@esbuild/aix-ppc64@0.25.2': 572 | optional: true 573 | 574 | '@esbuild/android-arm64@0.25.2': 575 | optional: true 576 | 577 | '@esbuild/android-arm@0.25.2': 578 | optional: true 579 | 580 | '@esbuild/android-x64@0.25.2': 581 | optional: true 582 | 583 | '@esbuild/darwin-arm64@0.25.2': 584 | optional: true 585 | 586 | '@esbuild/darwin-x64@0.25.2': 587 | optional: true 588 | 589 | '@esbuild/freebsd-arm64@0.25.2': 590 | optional: true 591 | 592 | '@esbuild/freebsd-x64@0.25.2': 593 | optional: true 594 | 595 | '@esbuild/linux-arm64@0.25.2': 596 | optional: true 597 | 598 | '@esbuild/linux-arm@0.25.2': 599 | optional: true 600 | 601 | '@esbuild/linux-ia32@0.25.2': 602 | optional: true 603 | 604 | '@esbuild/linux-loong64@0.25.2': 605 | optional: true 606 | 607 | '@esbuild/linux-mips64el@0.25.2': 608 | optional: true 609 | 610 | '@esbuild/linux-ppc64@0.25.2': 611 | optional: true 612 | 613 | '@esbuild/linux-riscv64@0.25.2': 614 | optional: true 615 | 616 | '@esbuild/linux-s390x@0.25.2': 617 | optional: true 618 | 619 | '@esbuild/linux-x64@0.25.2': 620 | optional: true 621 | 622 | '@esbuild/netbsd-arm64@0.25.2': 623 | optional: true 624 | 625 | '@esbuild/netbsd-x64@0.25.2': 626 | optional: true 627 | 628 | '@esbuild/openbsd-arm64@0.25.2': 629 | optional: true 630 | 631 | '@esbuild/openbsd-x64@0.25.2': 632 | optional: true 633 | 634 | '@esbuild/sunos-x64@0.25.2': 635 | optional: true 636 | 637 | '@esbuild/win32-arm64@0.25.2': 638 | optional: true 639 | 640 | '@esbuild/win32-ia32@0.25.2': 641 | optional: true 642 | 643 | '@esbuild/win32-x64@0.25.2': 644 | optional: true 645 | 646 | '@rollup/rollup-android-arm-eabi@4.40.0': 647 | optional: true 648 | 649 | '@rollup/rollup-android-arm64@4.40.0': 650 | optional: true 651 | 652 | '@rollup/rollup-darwin-arm64@4.40.0': 653 | optional: true 654 | 655 | '@rollup/rollup-darwin-x64@4.40.0': 656 | optional: true 657 | 658 | '@rollup/rollup-freebsd-arm64@4.40.0': 659 | optional: true 660 | 661 | '@rollup/rollup-freebsd-x64@4.40.0': 662 | optional: true 663 | 664 | '@rollup/rollup-linux-arm-gnueabihf@4.40.0': 665 | optional: true 666 | 667 | '@rollup/rollup-linux-arm-musleabihf@4.40.0': 668 | optional: true 669 | 670 | '@rollup/rollup-linux-arm64-gnu@4.40.0': 671 | optional: true 672 | 673 | '@rollup/rollup-linux-arm64-musl@4.40.0': 674 | optional: true 675 | 676 | '@rollup/rollup-linux-loongarch64-gnu@4.40.0': 677 | optional: true 678 | 679 | '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': 680 | optional: true 681 | 682 | '@rollup/rollup-linux-riscv64-gnu@4.40.0': 683 | optional: true 684 | 685 | '@rollup/rollup-linux-riscv64-musl@4.40.0': 686 | optional: true 687 | 688 | '@rollup/rollup-linux-s390x-gnu@4.40.0': 689 | optional: true 690 | 691 | '@rollup/rollup-linux-x64-gnu@4.40.0': 692 | optional: true 693 | 694 | '@rollup/rollup-linux-x64-musl@4.40.0': 695 | optional: true 696 | 697 | '@rollup/rollup-win32-arm64-msvc@4.40.0': 698 | optional: true 699 | 700 | '@rollup/rollup-win32-ia32-msvc@4.40.0': 701 | optional: true 702 | 703 | '@rollup/rollup-win32-x64-msvc@4.40.0': 704 | optional: true 705 | 706 | '@tailwindcss/node@4.1.4': 707 | dependencies: 708 | enhanced-resolve: 5.18.1 709 | jiti: 2.4.2 710 | lightningcss: 1.29.2 711 | tailwindcss: 4.1.4 712 | 713 | '@tailwindcss/oxide-android-arm64@4.1.4': 714 | optional: true 715 | 716 | '@tailwindcss/oxide-darwin-arm64@4.1.4': 717 | optional: true 718 | 719 | '@tailwindcss/oxide-darwin-x64@4.1.4': 720 | optional: true 721 | 722 | '@tailwindcss/oxide-freebsd-x64@4.1.4': 723 | optional: true 724 | 725 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4': 726 | optional: true 727 | 728 | '@tailwindcss/oxide-linux-arm64-gnu@4.1.4': 729 | optional: true 730 | 731 | '@tailwindcss/oxide-linux-arm64-musl@4.1.4': 732 | optional: true 733 | 734 | '@tailwindcss/oxide-linux-x64-gnu@4.1.4': 735 | optional: true 736 | 737 | '@tailwindcss/oxide-linux-x64-musl@4.1.4': 738 | optional: true 739 | 740 | '@tailwindcss/oxide-wasm32-wasi@4.1.4': 741 | optional: true 742 | 743 | '@tailwindcss/oxide-win32-arm64-msvc@4.1.4': 744 | optional: true 745 | 746 | '@tailwindcss/oxide-win32-x64-msvc@4.1.4': 747 | optional: true 748 | 749 | '@tailwindcss/oxide@4.1.4': 750 | optionalDependencies: 751 | '@tailwindcss/oxide-android-arm64': 4.1.4 752 | '@tailwindcss/oxide-darwin-arm64': 4.1.4 753 | '@tailwindcss/oxide-darwin-x64': 4.1.4 754 | '@tailwindcss/oxide-freebsd-x64': 4.1.4 755 | '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.4 756 | '@tailwindcss/oxide-linux-arm64-gnu': 4.1.4 757 | '@tailwindcss/oxide-linux-arm64-musl': 4.1.4 758 | '@tailwindcss/oxide-linux-x64-gnu': 4.1.4 759 | '@tailwindcss/oxide-linux-x64-musl': 4.1.4 760 | '@tailwindcss/oxide-wasm32-wasi': 4.1.4 761 | '@tailwindcss/oxide-win32-arm64-msvc': 4.1.4 762 | '@tailwindcss/oxide-win32-x64-msvc': 4.1.4 763 | 764 | '@tailwindcss/typography@0.5.16(tailwindcss@4.1.4)': 765 | dependencies: 766 | lodash.castarray: 4.4.0 767 | lodash.isplainobject: 4.0.6 768 | lodash.merge: 4.6.2 769 | postcss-selector-parser: 6.0.10 770 | tailwindcss: 4.1.4 771 | 772 | '@tailwindcss/vite@4.1.4(vite@6.3.2(jiti@2.4.2)(lightningcss@1.29.2))': 773 | dependencies: 774 | '@tailwindcss/node': 4.1.4 775 | '@tailwindcss/oxide': 4.1.4 776 | tailwindcss: 4.1.4 777 | vite: 6.3.2(jiti@2.4.2)(lightningcss@1.29.2) 778 | 779 | '@types/estree@1.0.7': {} 780 | 781 | cssesc@3.0.0: {} 782 | 783 | detect-libc@2.0.4: {} 784 | 785 | enhanced-resolve@5.18.1: 786 | dependencies: 787 | graceful-fs: 4.2.11 788 | tapable: 2.2.1 789 | 790 | esbuild@0.25.2: 791 | optionalDependencies: 792 | '@esbuild/aix-ppc64': 0.25.2 793 | '@esbuild/android-arm': 0.25.2 794 | '@esbuild/android-arm64': 0.25.2 795 | '@esbuild/android-x64': 0.25.2 796 | '@esbuild/darwin-arm64': 0.25.2 797 | '@esbuild/darwin-x64': 0.25.2 798 | '@esbuild/freebsd-arm64': 0.25.2 799 | '@esbuild/freebsd-x64': 0.25.2 800 | '@esbuild/linux-arm': 0.25.2 801 | '@esbuild/linux-arm64': 0.25.2 802 | '@esbuild/linux-ia32': 0.25.2 803 | '@esbuild/linux-loong64': 0.25.2 804 | '@esbuild/linux-mips64el': 0.25.2 805 | '@esbuild/linux-ppc64': 0.25.2 806 | '@esbuild/linux-riscv64': 0.25.2 807 | '@esbuild/linux-s390x': 0.25.2 808 | '@esbuild/linux-x64': 0.25.2 809 | '@esbuild/netbsd-arm64': 0.25.2 810 | '@esbuild/netbsd-x64': 0.25.2 811 | '@esbuild/openbsd-arm64': 0.25.2 812 | '@esbuild/openbsd-x64': 0.25.2 813 | '@esbuild/sunos-x64': 0.25.2 814 | '@esbuild/win32-arm64': 0.25.2 815 | '@esbuild/win32-ia32': 0.25.2 816 | '@esbuild/win32-x64': 0.25.2 817 | 818 | fdir@6.4.4(picomatch@4.0.2): 819 | optionalDependencies: 820 | picomatch: 4.0.2 821 | 822 | fsevents@2.3.3: 823 | optional: true 824 | 825 | graceful-fs@4.2.11: {} 826 | 827 | jiti@2.4.2: {} 828 | 829 | lightningcss-darwin-arm64@1.29.2: 830 | optional: true 831 | 832 | lightningcss-darwin-x64@1.29.2: 833 | optional: true 834 | 835 | lightningcss-freebsd-x64@1.29.2: 836 | optional: true 837 | 838 | lightningcss-linux-arm-gnueabihf@1.29.2: 839 | optional: true 840 | 841 | lightningcss-linux-arm64-gnu@1.29.2: 842 | optional: true 843 | 844 | lightningcss-linux-arm64-musl@1.29.2: 845 | optional: true 846 | 847 | lightningcss-linux-x64-gnu@1.29.2: 848 | optional: true 849 | 850 | lightningcss-linux-x64-musl@1.29.2: 851 | optional: true 852 | 853 | lightningcss-win32-arm64-msvc@1.29.2: 854 | optional: true 855 | 856 | lightningcss-win32-x64-msvc@1.29.2: 857 | optional: true 858 | 859 | lightningcss@1.29.2: 860 | dependencies: 861 | detect-libc: 2.0.4 862 | optionalDependencies: 863 | lightningcss-darwin-arm64: 1.29.2 864 | lightningcss-darwin-x64: 1.29.2 865 | lightningcss-freebsd-x64: 1.29.2 866 | lightningcss-linux-arm-gnueabihf: 1.29.2 867 | lightningcss-linux-arm64-gnu: 1.29.2 868 | lightningcss-linux-arm64-musl: 1.29.2 869 | lightningcss-linux-x64-gnu: 1.29.2 870 | lightningcss-linux-x64-musl: 1.29.2 871 | lightningcss-win32-arm64-msvc: 1.29.2 872 | lightningcss-win32-x64-msvc: 1.29.2 873 | 874 | lodash.castarray@4.4.0: {} 875 | 876 | lodash.isplainobject@4.0.6: {} 877 | 878 | lodash.merge@4.6.2: {} 879 | 880 | nanoid@3.3.11: {} 881 | 882 | picocolors@1.1.1: {} 883 | 884 | picomatch@4.0.2: {} 885 | 886 | postcss-selector-parser@6.0.10: 887 | dependencies: 888 | cssesc: 3.0.0 889 | util-deprecate: 1.0.2 890 | 891 | postcss@8.5.3: 892 | dependencies: 893 | nanoid: 3.3.11 894 | picocolors: 1.1.1 895 | source-map-js: 1.2.1 896 | 897 | rollup@4.40.0: 898 | dependencies: 899 | '@types/estree': 1.0.7 900 | optionalDependencies: 901 | '@rollup/rollup-android-arm-eabi': 4.40.0 902 | '@rollup/rollup-android-arm64': 4.40.0 903 | '@rollup/rollup-darwin-arm64': 4.40.0 904 | '@rollup/rollup-darwin-x64': 4.40.0 905 | '@rollup/rollup-freebsd-arm64': 4.40.0 906 | '@rollup/rollup-freebsd-x64': 4.40.0 907 | '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 908 | '@rollup/rollup-linux-arm-musleabihf': 4.40.0 909 | '@rollup/rollup-linux-arm64-gnu': 4.40.0 910 | '@rollup/rollup-linux-arm64-musl': 4.40.0 911 | '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 912 | '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 913 | '@rollup/rollup-linux-riscv64-gnu': 4.40.0 914 | '@rollup/rollup-linux-riscv64-musl': 4.40.0 915 | '@rollup/rollup-linux-s390x-gnu': 4.40.0 916 | '@rollup/rollup-linux-x64-gnu': 4.40.0 917 | '@rollup/rollup-linux-x64-musl': 4.40.0 918 | '@rollup/rollup-win32-arm64-msvc': 4.40.0 919 | '@rollup/rollup-win32-ia32-msvc': 4.40.0 920 | '@rollup/rollup-win32-x64-msvc': 4.40.0 921 | fsevents: 2.3.3 922 | 923 | source-map-js@1.2.1: {} 924 | 925 | tailwindcss@4.1.4: {} 926 | 927 | tapable@2.2.1: {} 928 | 929 | tinyglobby@0.2.13: 930 | dependencies: 931 | fdir: 6.4.4(picomatch@4.0.2) 932 | picomatch: 4.0.2 933 | 934 | util-deprecate@1.0.2: {} 935 | 936 | vite@6.3.2(jiti@2.4.2)(lightningcss@1.29.2): 937 | dependencies: 938 | esbuild: 0.25.2 939 | fdir: 6.4.4(picomatch@4.0.2) 940 | picomatch: 4.0.2 941 | postcss: 8.5.3 942 | rollup: 4.40.0 943 | tinyglobby: 0.2.13 944 | optionalDependencies: 945 | fsevents: 2.3.3 946 | jiti: 2.4.2 947 | lightningcss: 1.29.2 948 | -------------------------------------------------------------------------------- /app/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/query.sql: -------------------------------------------------------------------------------- 1 | -- name: InsertUser :one 2 | INSERT INTO users (id,username, password_hash) VALUES (?,?,?) 3 | RETURNING id; 4 | 5 | -- name: InsertAnonymousUser :exec 6 | INSERT OR IGNORE INTO users (id, username) VALUES (?, "anonymous"); 7 | 8 | -- name: InsertOIDCUser :exec 9 | INSERT INTO oidc_users (user_id, client_id, user_sub) VALUES (?,?,?); 10 | 11 | -- name: InsertPaste :one 12 | INSERT INTO pastes (id, owner_id,slug,content,content_format,visibility,expires_at) VALUES (?,?,?,?,?,?,?) 13 | RETURNING id; 14 | 15 | -- name: InsertPasteAttachment :one 16 | INSERT INTO attachments (id, paste_id,slug,mime_type,size,checksum) VALUES (?,?,?,?,?,?) 17 | RETURNING id; 18 | 19 | -- name: GetUserByID :one 20 | SELECT * FROM users 21 | WHERE id = ? LIMIT 1; 22 | 23 | -- name: GetUserByUsername :one 24 | SELECT * FROM users 25 | WHERE username = ? LIMIT 1; 26 | 27 | -- name: GetUserByOIDC :one 28 | SELECT u.* FROM oidc_users AS o 29 | INNER JOIN users AS u ON u.id = o.user_id 30 | WHERE client_id = ? AND user_sub = ? 31 | LIMIT 1; 32 | 33 | -- name: GetLatestPublicPastes :many 34 | SELECT p.id, p.owner_id, p.slug, p.created_at, users.username FROM pastes as p 35 | INNER JOIN users ON users.id = p.owner_id 36 | WHERE ( 37 | p.visibility = 'public' 38 | AND (p.expires_at IS NULL OR p.expires_at > CURRENT_TIMESTAMP) 39 | ) 40 | ORDER BY p.id DESC 41 | LIMIT ?; 42 | 43 | -- name: GetLatestPastesByUser :many 44 | SELECT p.id, p.owner_id, p.slug, p.created_at, p.visibility FROM pastes as p 45 | INNER JOIN users ON users.id = p.owner_id 46 | WHERE username = sqlc.arg(username) AND ( 47 | (visibility = 'public' AND NOT p.owner_id = sqlc.arg(current_user_id)) 48 | OR 49 | (p.owner_id = sqlc.arg(current_user_id)) 50 | ) AND (p.expires_at IS NULL OR p.expires_at > CURRENT_TIMESTAMP) 51 | ORDER BY p.id DESC; 52 | 53 | -- name: GetPasteBySlug :one 54 | SELECT p.* FROM pastes as p 55 | INNER JOIN users AS u ON u.id = p.owner_id 56 | WHERE ( 57 | (u.username = sqlc.arg(username) AND p.slug = sqlc.arg(paste_slug)) 58 | AND 59 | ( 60 | (p.visibility IN ('public', 'unlisted') AND NOT p.owner_id = sqlc.arg(current_user_id)) 61 | OR 62 | (p.owner_id = sqlc.arg(current_user_id)) 63 | ) 64 | AND (p.expires_at IS NULL OR p.expires_at > CURRENT_TIMESTAMP) 65 | ) 66 | LIMIT 1; 67 | 68 | -- name: GetPastePathParts :one 69 | SELECT p.slug, u.username FROM pastes as p 70 | INNER JOIN users AS u ON u.id = p.owner_id 71 | WHERE p.id = ? 72 | LIMIT 1; 73 | 74 | -- name: GetAttachmentsByPasteID :many 75 | SELECT a.* FROM attachments AS a 76 | INNER JOIN pastes AS p ON a.paste_id = p.id 77 | WHERE ( 78 | a.paste_id = ? 79 | AND 80 | (p.expires_at IS NULL OR p.expires_at > CURRENT_TIMESTAMP) 81 | ); 82 | 83 | -- name: GetAttachmentBySlug :one 84 | SELECT a.* FROM attachments as a 85 | INNER JOIN pastes AS p ON a.paste_id = p.id 86 | INNER JOIN users AS u ON u.id = p.owner_id 87 | WHERE 88 | ( 89 | (u.username = sqlc.arg(username) AND p.slug = sqlc.arg(paste_slug) AND a.slug = sqlc.arg(attachment_slug)) 90 | AND 91 | ( 92 | (p.visibility IN ('public', 'unlisted') AND NOT p.owner_id = sqlc.arg(current_user_id)) 93 | OR 94 | (p.owner_id = sqlc.arg(current_user_id)) 95 | ) 96 | AND (p.expires_at IS NULL OR p.expires_at > CURRENT_TIMESTAMP) 97 | ) 98 | LIMIT 1; 99 | -------------------------------------------------------------------------------- /app/services/auth.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/enchant97/hasty-paste/app/core" 8 | "github.com/enchant97/hasty-paste/app/database" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type AuthService struct { 13 | dao *core.DAO 14 | } 15 | 16 | func (s AuthService) New(dao *core.DAO) AuthService { 17 | return AuthService{ 18 | dao: dao, 19 | } 20 | } 21 | 22 | func (s *AuthService) CreateNewUser(form core.NewUserForm) (uuid.UUID, error) { 23 | id, err := s.dao.Queries.InsertUser(context.Background(), database.InsertUserParams{ 24 | ID: core.NewUUID(), 25 | Username: form.Username, 26 | PasswordHash: core.HashPassword(form.Password), 27 | }) 28 | return id, wrapDbError(err) 29 | } 30 | 31 | func (s *AuthService) CreateNewOIDCUser(username string, clientID string, userSub string) (uuid.UUID, error) { 32 | ctx := context.Background() 33 | tx, err := s.dao.DB.Begin() 34 | if err != nil { 35 | return uuid.Nil, wrapDbError(err) 36 | } 37 | q := s.dao.Queries.WithTx(tx) 38 | userID, err := q.InsertUser(ctx, database.InsertUserParams{ 39 | ID: core.NewUUID(), 40 | Username: username, 41 | }) 42 | if err != nil { 43 | return uuid.Nil, wrapDbError(err) 44 | } 45 | if err := q.InsertOIDCUser(ctx, database.InsertOIDCUserParams{ 46 | UserID: userID, 47 | ClientID: clientID, 48 | UserSub: userSub, 49 | }); err != nil { 50 | 51 | return uuid.Nil, wrapDbError(err) 52 | } 53 | return userID, wrapDbError(tx.Commit()) 54 | } 55 | 56 | func (s *AuthService) CheckIfValidUser(form core.LoginUserForm) (bool, error) { 57 | user, err := s.dao.Queries.GetUserByUsername(context.Background(), form.Username) 58 | if err != nil { 59 | return false, wrapDbError(err) 60 | } 61 | return core.IsValidPassword(form.Password, user.PasswordHash), nil 62 | } 63 | 64 | func (s *AuthService) GetOIDCUser(oidcUser core.OIDCUser) (database.User, error) { 65 | return wrapDbErrorWithValue(s.dao.Queries.GetUserByOIDC(context.Background(), database.GetUserByOIDCParams{ 66 | ClientID: oidcUser.ClientID, 67 | UserSub: oidcUser.Subject, 68 | })) 69 | } 70 | 71 | func (s *AuthService) GetOrCreateOIDCUser(oidcUser core.OIDCUserWithUsername) (database.User, error) { 72 | user, err := s.GetOIDCUser(oidcUser.OIDCUser) 73 | if err != nil { 74 | if errors.Is(err, ErrNotFound) { 75 | userID, err := s.CreateNewOIDCUser(oidcUser.Username, oidcUser.ClientID, oidcUser.Subject) 76 | if err != nil { 77 | return database.User{}, err 78 | } 79 | return wrapDbErrorWithValue(s.dao.Queries.GetUserByID(context.Background(), userID)) 80 | } else { 81 | return database.User{}, err 82 | } 83 | } else { 84 | return user, err 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/services/aux.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "github.com/enchant97/hasty-paste/app/core" 4 | 5 | type AuxService struct { 6 | dao *core.DAO 7 | } 8 | 9 | func (s AuxService) New(dao *core.DAO) AuxService { 10 | return AuxService{ 11 | dao: dao, 12 | } 13 | } 14 | 15 | func (s *AuxService) IsHealthy() bool { 16 | return s.dao.DB.Ping() == nil 17 | } 18 | -------------------------------------------------------------------------------- /app/services/home.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/enchant97/hasty-paste/app/core" 8 | "github.com/enchant97/hasty-paste/app/database" 9 | "github.com/enchant97/hasty-paste/app/storage" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type HomeService struct { 14 | dao *core.DAO 15 | sc *storage.StorageController 16 | } 17 | 18 | func (s HomeService) New(dao *core.DAO, sc *storage.StorageController) HomeService { 19 | return HomeService{ 20 | dao: dao, 21 | sc: sc, 22 | } 23 | } 24 | 25 | func (s *HomeService) GetPastePathPartsByPasteID(pasteID uuid.UUID) (database.GetPastePathPartsRow, error) { 26 | return wrapDbErrorWithValue(s.dao.Queries.GetPastePathParts(context.Background(), pasteID)) 27 | } 28 | 29 | func (s *HomeService) GetLatestPublicPastes() ([]database.GetLatestPublicPastesRow, error) { 30 | return wrapDbErrorWithValue(s.dao.Queries.GetLatestPublicPastes(context.Background(), 5)) 31 | } 32 | 33 | func (s *HomeService) NewPaste(ownerID uuid.UUID, pasteForm core.NewPasteForm) (uuid.UUID, error) { 34 | ctx := context.Background() 35 | tx, err := s.dao.DB.Begin() 36 | if err != nil { 37 | return uuid.Nil, err 38 | } 39 | defer tx.Rollback() 40 | 41 | var expiry sql.NullTime 42 | if pasteForm.Expiry != nil { 43 | if err := expiry.Scan(*pasteForm.Expiry); err != nil { 44 | return uuid.Nil, err 45 | } 46 | } 47 | 48 | dbQueries := s.dao.Queries.WithTx(tx) 49 | pasteID, err := dbQueries.InsertPaste(ctx, database.InsertPasteParams{ 50 | ID: core.NewUUID(), 51 | OwnerID: ownerID, 52 | Slug: pasteForm.Slug, 53 | Content: pasteForm.Content, 54 | ContentFormat: pasteForm.ContentFormat, 55 | Visibility: pasteForm.Visibility, 56 | ExpiresAt: expiry, 57 | }) 58 | if err != nil { 59 | return wrapDbErrorWithValue(uuid.Nil, err) 60 | } 61 | 62 | // Process each attachment one by one (maybe make it do parallel in future?) 63 | for _, attachment := range pasteForm.Attachments { 64 | if err := func() error { 65 | r, err := attachment.Open() 66 | if err != nil { 67 | return err 68 | } 69 | defer r.Close() 70 | checksum, err := core.MakeChecksum(r) 71 | if err != nil { 72 | return err 73 | } 74 | r.Seek(0, 0) 75 | attachmentID, err := dbQueries.InsertPasteAttachment(ctx, database.InsertPasteAttachmentParams{ 76 | ID: core.NewUUID(), 77 | PasteID: pasteID, 78 | Slug: attachment.Slug, 79 | MimeType: attachment.Type, 80 | Size: attachment.Size, 81 | Checksum: checksum, 82 | }) 83 | if err != nil { 84 | return err 85 | } 86 | if err := s.sc.WritePasteAttachment(attachmentID, r); err != nil { 87 | return err 88 | } 89 | return nil 90 | }(); err != nil { 91 | return uuid.Nil, err 92 | } 93 | } 94 | 95 | return wrapDbErrorWithValue(pasteID, tx.Commit()) 96 | } 97 | -------------------------------------------------------------------------------- /app/services/user.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/enchant97/hasty-paste/app/core" 8 | "github.com/enchant97/hasty-paste/app/database" 9 | "github.com/enchant97/hasty-paste/app/storage" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type UserService struct { 14 | dao *core.DAO 15 | sc *storage.StorageController 16 | } 17 | 18 | func (s UserService) New(dao *core.DAO, sc *storage.StorageController) UserService { 19 | return UserService{ 20 | dao: dao, 21 | sc: sc, 22 | } 23 | } 24 | 25 | func (s *UserService) GetPastes(currentUserID uuid.UUID, username string) ([]database.GetLatestPastesByUserRow, error) { 26 | return wrapDbErrorWithValue(s.dao.Queries.GetLatestPastesByUser(context.Background(), database.GetLatestPastesByUserParams{ 27 | Username: username, 28 | CurrentUserID: currentUserID, 29 | })) 30 | } 31 | 32 | func (s *UserService) GetPaste(currentUserID uuid.UUID, username string, slug string) (database.Paste, error) { 33 | return wrapDbErrorWithValue(s.dao.Queries.GetPasteBySlug(context.Background(), database.GetPasteBySlugParams{ 34 | CurrentUserID: currentUserID, 35 | Username: username, 36 | PasteSlug: slug, 37 | })) 38 | } 39 | 40 | func (s *UserService) GetPasteAttachments(pasteId uuid.UUID) ([]database.Attachment, error) { 41 | return wrapDbErrorWithValue(s.dao.Queries.GetAttachmentsByPasteID(context.Background(), pasteId)) 42 | } 43 | 44 | func (s *UserService) GetPasteAttachment( 45 | currentUserID uuid.UUID, 46 | username string, 47 | pasteSlug string, 48 | attachmentSlug string, 49 | ) (database.Attachment, io.ReadCloser, error) { 50 | attachment, err := s.dao.Queries.GetAttachmentBySlug(context.Background(), database.GetAttachmentBySlugParams{ 51 | CurrentUserID: currentUserID, 52 | Username: username, 53 | PasteSlug: pasteSlug, 54 | AttachmentSlug: attachmentSlug, 55 | }) 56 | if err != nil { 57 | return database.Attachment{}, nil, wrapDbError(err) 58 | } 59 | attachmentReader, err := s.sc.ReadPasteAttachment(attachment.ID) 60 | return attachment, attachmentReader, wrapDbError(err) 61 | } 62 | -------------------------------------------------------------------------------- /app/services/utils.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | 7 | "modernc.org/sqlite" 8 | sqlite3 "modernc.org/sqlite/lib" 9 | ) 10 | 11 | var ErrNotFound = errors.New("resource not found") 12 | var ErrConflict = errors.New("resource conflict") 13 | 14 | // / wrap a database error with a specific service error 15 | func wrapDbError(err error) error { 16 | if errors.Is(err, sql.ErrNoRows) { 17 | return errors.Join(err, ErrNotFound) 18 | } else if err, ok := err.(*sqlite.Error); ok { 19 | switch err.Code() { 20 | case sqlite3.SQLITE_CONSTRAINT_UNIQUE: 21 | return errors.Join(err, ErrConflict) 22 | } 23 | } 24 | return err 25 | } 26 | 27 | // / wrap a database error and it's potential value with a specific service error 28 | func wrapDbErrorWithValue[T any](v T, err error) (T, error) { 29 | return v, wrapDbError(err) 30 | } 31 | -------------------------------------------------------------------------------- /app/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - engine: "sqlite" 4 | queries: "query.sql" 5 | schema: "migrations" 6 | gen: 7 | go: 8 | package: "database" 9 | out: "database" 10 | overrides: 11 | - db_type: "UUID" 12 | go_type: 13 | import: "github.com/google/uuid" 14 | type: "UUID" 15 | -------------------------------------------------------------------------------- /app/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type StorageController struct { 13 | attachmentsRootPath string 14 | } 15 | 16 | func (sc StorageController) New(attachmentsRootPath string) (StorageController, error) { 17 | if !filepath.IsAbs(attachmentsRootPath) { 18 | return StorageController{}, errors.New("rootPath must be a absolute path") 19 | } 20 | err := os.MkdirAll(attachmentsRootPath, 0755) 21 | return StorageController{ 22 | attachmentsRootPath: attachmentsRootPath, 23 | }, err 24 | } 25 | 26 | func (sc *StorageController) WritePasteAttachment( 27 | attachmentUID uuid.UUID, 28 | r io.Reader, 29 | ) error { 30 | filePath := filepath.Join(sc.attachmentsRootPath, attachmentUID.String()+".bin") 31 | f, err := os.Create(filePath) 32 | if err != nil { 33 | return err 34 | } 35 | defer f.Close() 36 | _, err = io.Copy(f, r) 37 | if err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func (sc *StorageController) ReadPasteAttachment( 44 | attachmentUID uuid.UUID, 45 | ) (io.ReadCloser, error) { 46 | filePath := filepath.Join(sc.attachmentsRootPath, attachmentUID.String()+".bin") 47 | return os.Open(filePath) 48 | } 49 | 50 | func (sc *StorageController) DeletePasteAttachment( 51 | attachmentUID uuid.UUID, 52 | ) error { 53 | filePath := filepath.Join(sc.attachmentsRootPath, attachmentUID.String()+".bin") 54 | return os.Remove(filePath) 55 | } 56 | -------------------------------------------------------------------------------- /app/style.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss" source(none); 2 | @plugin "@tailwindcss/typography"; 3 | @source "./components/*.templ"; 4 | 5 | @theme { 6 | --color-brand: oklch(55.14% 0.1251 248.34); 7 | --color-brand-400: oklch(from var(--color-brand) calc(l/2 + .4) c h); 8 | --color-brand-500: var(--color-brand); 9 | --color-brand-600: oklch(from var(--color-brand) calc(l/2 + .2) c h); 10 | --color-brand-700: oklch(from var(--color-brand) calc(l/2 + .1) c h); 11 | 12 | --color-base-light-100: var(--color-white); 13 | --color-base-light-200: var(--color-zinc-100); 14 | --color-base-light-300: var(--color-zinc-300); 15 | --color-base-dark-100: var(--color-zinc-900); 16 | --color-base-dark-200: var(--color-zinc-800); 17 | --color-base-dark-300: var(--color-zinc-700); 18 | } 19 | 20 | @layer base { 21 | body { 22 | @apply bg-base-light-100; 23 | 24 | @variant dark { 25 | @apply bg-base-dark-100 text-zinc-300; 26 | } 27 | } 28 | 29 | h1, 30 | h2, 31 | h3, 32 | h4 { 33 | @apply font-bold; 34 | } 35 | 36 | h1 { 37 | @apply text-4xl; 38 | } 39 | 40 | h2 { 41 | @apply text-2xl; 42 | } 43 | } 44 | 45 | @layer components { 46 | .flashes { 47 | @apply fixed bottom-4 right-4 flex flex-col gap-2; 48 | } 49 | 50 | .flash { 51 | @apply px-4 py-2 rounded-md shadow-sm bg-base-light-100 border-4 border-base-light-300 transition-discrete transition-opacity; 52 | 53 | @variant dark { 54 | @apply bg-base-dark-100 border-base-dark-300; 55 | } 56 | } 57 | 58 | .flash.ok { 59 | @apply border-green-600; 60 | 61 | @variant dark { 62 | @apply border-green-800; 63 | } 64 | } 65 | 66 | .flash.error { 67 | @apply border-red-600; 68 | 69 | @variant dark { 70 | @apply border-red-800; 71 | } 72 | } 73 | 74 | .footer.footer-center { 75 | @apply p-10 flex flex-col gap-1 items-center; 76 | } 77 | 78 | .btn.btn-neutral { 79 | @apply bg-base-light-100 shadow-sm; 80 | 81 | @variant dark { 82 | @apply bg-base-dark-100; 83 | } 84 | } 85 | 86 | 87 | .btn.btn-primary { 88 | @apply bg-brand-400 shadow-sm; 89 | 90 | @variant dark { 91 | @apply bg-brand-600; 92 | } 93 | } 94 | 95 | .btn { 96 | @apply px-4 py-2 cursor-pointer font-bold text-sm text-center rounded-md uppercase duration-200 hover:bg-base-light-300; 97 | 98 | @variant dark { 99 | @apply hover:bg-base-dark-300; 100 | } 101 | } 102 | 103 | .label-text { 104 | @apply text-sm 105 | } 106 | 107 | .input { 108 | @apply px-2 py-1.5 rounded-md bg-zinc-200 duration-200 border border-zinc-300; 109 | 110 | @variant dark { 111 | @apply bg-base-dark-100 border-base-dark-300; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import tailwindcss from '@tailwindcss/vite' 3 | export default defineConfig({ 4 | plugins: [ 5 | tailwindcss(), 6 | ], 7 | build: { 8 | manifest: true, 9 | rollupOptions: { 10 | input: "./main.js", 11 | } 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /.hugo_build.lock 2 | /resources/ 3 | /public/ 4 | -------------------------------------------------------------------------------- /docs/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | GNU Free Documentation License 3 | Version 1.3, 3 November 2008 4 | 5 | 6 | Copyright (C) 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc. 7 | 8 | Everyone is permitted to copy and distribute verbatim copies 9 | of this license document, but changing it is not allowed. 10 | 11 | 0. PREAMBLE 12 | 13 | The purpose of this License is to make a manual, textbook, or other 14 | functional and useful document "free" in the sense of freedom: to 15 | assure everyone the effective freedom to copy and redistribute it, 16 | with or without modifying it, either commercially or noncommercially. 17 | Secondarily, this License preserves for the author and publisher a way 18 | to get credit for their work, while not being considered responsible 19 | for modifications made by others. 20 | 21 | This License is a kind of "copyleft", which means that derivative 22 | works of the document must themselves be free in the same sense. It 23 | complements the GNU General Public License, which is a copyleft 24 | license designed for free software. 25 | 26 | We have designed this License in order to use it for manuals for free 27 | software, because free software needs free documentation: a free 28 | program should come with manuals providing the same freedoms that the 29 | software does. But this License is not limited to software manuals; 30 | it can be used for any textual work, regardless of subject matter or 31 | whether it is published as a printed book. We recommend this License 32 | principally for works whose purpose is instruction or reference. 33 | 34 | 35 | 1. APPLICABILITY AND DEFINITIONS 36 | 37 | This License applies to any manual or other work, in any medium, that 38 | contains a notice placed by the copyright holder saying it can be 39 | distributed under the terms of this License. Such a notice grants a 40 | world-wide, royalty-free license, unlimited in duration, to use that 41 | work under the conditions stated herein. The "Document", below, 42 | refers to any such manual or work. Any member of the public is a 43 | licensee, and is addressed as "you". You accept the license if you 44 | copy, modify or distribute the work in a way requiring permission 45 | under copyright law. 46 | 47 | A "Modified Version" of the Document means any work containing the 48 | Document or a portion of it, either copied verbatim, or with 49 | modifications and/or translated into another language. 50 | 51 | A "Secondary Section" is a named appendix or a front-matter section of 52 | the Document that deals exclusively with the relationship of the 53 | publishers or authors of the Document to the Document's overall 54 | subject (or to related matters) and contains nothing that could fall 55 | directly within that overall subject. (Thus, if the Document is in 56 | part a textbook of mathematics, a Secondary Section may not explain 57 | any mathematics.) The relationship could be a matter of historical 58 | connection with the subject or with related matters, or of legal, 59 | commercial, philosophical, ethical or political position regarding 60 | them. 61 | 62 | The "Invariant Sections" are certain Secondary Sections whose titles 63 | are designated, as being those of Invariant Sections, in the notice 64 | that says that the Document is released under this License. If a 65 | section does not fit the above definition of Secondary then it is not 66 | allowed to be designated as Invariant. The Document may contain zero 67 | Invariant Sections. If the Document does not identify any Invariant 68 | Sections then there are none. 69 | 70 | The "Cover Texts" are certain short passages of text that are listed, 71 | as Front-Cover Texts or Back-Cover Texts, in the notice that says that 72 | the Document is released under this License. A Front-Cover Text may 73 | be at most 5 words, and a Back-Cover Text may be at most 25 words. 74 | 75 | A "Transparent" copy of the Document means a machine-readable copy, 76 | represented in a format whose specification is available to the 77 | general public, that is suitable for revising the document 78 | straightforwardly with generic text editors or (for images composed of 79 | pixels) generic paint programs or (for drawings) some widely available 80 | drawing editor, and that is suitable for input to text formatters or 81 | for automatic translation to a variety of formats suitable for input 82 | to text formatters. A copy made in an otherwise Transparent file 83 | format whose markup, or absence of markup, has been arranged to thwart 84 | or discourage subsequent modification by readers is not Transparent. 85 | An image format is not Transparent if used for any substantial amount 86 | of text. A copy that is not "Transparent" is called "Opaque". 87 | 88 | Examples of suitable formats for Transparent copies include plain 89 | ASCII without markup, Texinfo input format, LaTeX input format, SGML 90 | or XML using a publicly available DTD, and standard-conforming simple 91 | HTML, PostScript or PDF designed for human modification. Examples of 92 | transparent image formats include PNG, XCF and JPG. Opaque formats 93 | include proprietary formats that can be read and edited only by 94 | proprietary word processors, SGML or XML for which the DTD and/or 95 | processing tools are not generally available, and the 96 | machine-generated HTML, PostScript or PDF produced by some word 97 | processors for output purposes only. 98 | 99 | The "Title Page" means, for a printed book, the title page itself, 100 | plus such following pages as are needed to hold, legibly, the material 101 | this License requires to appear in the title page. For works in 102 | formats which do not have any title page as such, "Title Page" means 103 | the text near the most prominent appearance of the work's title, 104 | preceding the beginning of the body of the text. 105 | 106 | The "publisher" means any person or entity that distributes copies of 107 | the Document to the public. 108 | 109 | A section "Entitled XYZ" means a named subunit of the Document whose 110 | title either is precisely XYZ or contains XYZ in parentheses following 111 | text that translates XYZ in another language. (Here XYZ stands for a 112 | specific section name mentioned below, such as "Acknowledgements", 113 | "Dedications", "Endorsements", or "History".) To "Preserve the Title" 114 | of such a section when you modify the Document means that it remains a 115 | section "Entitled XYZ" according to this definition. 116 | 117 | The Document may include Warranty Disclaimers next to the notice which 118 | states that this License applies to the Document. These Warranty 119 | Disclaimers are considered to be included by reference in this 120 | License, but only as regards disclaiming warranties: any other 121 | implication that these Warranty Disclaimers may have is void and has 122 | no effect on the meaning of this License. 123 | 124 | 2. VERBATIM COPYING 125 | 126 | You may copy and distribute the Document in any medium, either 127 | commercially or noncommercially, provided that this License, the 128 | copyright notices, and the license notice saying this License applies 129 | to the Document are reproduced in all copies, and that you add no 130 | other conditions whatsoever to those of this License. You may not use 131 | technical measures to obstruct or control the reading or further 132 | copying of the copies you make or distribute. However, you may accept 133 | compensation in exchange for copies. If you distribute a large enough 134 | number of copies you must also follow the conditions in section 3. 135 | 136 | You may also lend copies, under the same conditions stated above, and 137 | you may publicly display copies. 138 | 139 | 140 | 3. COPYING IN QUANTITY 141 | 142 | If you publish printed copies (or copies in media that commonly have 143 | printed covers) of the Document, numbering more than 100, and the 144 | Document's license notice requires Cover Texts, you must enclose the 145 | copies in covers that carry, clearly and legibly, all these Cover 146 | Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on 147 | the back cover. Both covers must also clearly and legibly identify 148 | you as the publisher of these copies. The front cover must present 149 | the full title with all words of the title equally prominent and 150 | visible. You may add other material on the covers in addition. 151 | Copying with changes limited to the covers, as long as they preserve 152 | the title of the Document and satisfy these conditions, can be treated 153 | as verbatim copying in other respects. 154 | 155 | If the required texts for either cover are too voluminous to fit 156 | legibly, you should put the first ones listed (as many as fit 157 | reasonably) on the actual cover, and continue the rest onto adjacent 158 | pages. 159 | 160 | If you publish or distribute Opaque copies of the Document numbering 161 | more than 100, you must either include a machine-readable Transparent 162 | copy along with each Opaque copy, or state in or with each Opaque copy 163 | a computer-network location from which the general network-using 164 | public has access to download using public-standard network protocols 165 | a complete Transparent copy of the Document, free of added material. 166 | If you use the latter option, you must take reasonably prudent steps, 167 | when you begin distribution of Opaque copies in quantity, to ensure 168 | that this Transparent copy will remain thus accessible at the stated 169 | location until at least one year after the last time you distribute an 170 | Opaque copy (directly or through your agents or retailers) of that 171 | edition to the public. 172 | 173 | It is requested, but not required, that you contact the authors of the 174 | Document well before redistributing any large number of copies, to 175 | give them a chance to provide you with an updated version of the 176 | Document. 177 | 178 | 179 | 4. MODIFICATIONS 180 | 181 | You may copy and distribute a Modified Version of the Document under 182 | the conditions of sections 2 and 3 above, provided that you release 183 | the Modified Version under precisely this License, with the Modified 184 | Version filling the role of the Document, thus licensing distribution 185 | and modification of the Modified Version to whoever possesses a copy 186 | of it. In addition, you must do these things in the Modified Version: 187 | 188 | A. Use in the Title Page (and on the covers, if any) a title distinct 189 | from that of the Document, and from those of previous versions 190 | (which should, if there were any, be listed in the History section 191 | of the Document). You may use the same title as a previous version 192 | if the original publisher of that version gives permission. 193 | B. List on the Title Page, as authors, one or more persons or entities 194 | responsible for authorship of the modifications in the Modified 195 | Version, together with at least five of the principal authors of the 196 | Document (all of its principal authors, if it has fewer than five), 197 | unless they release you from this requirement. 198 | C. State on the Title page the name of the publisher of the 199 | Modified Version, as the publisher. 200 | D. Preserve all the copyright notices of the Document. 201 | E. Add an appropriate copyright notice for your modifications 202 | adjacent to the other copyright notices. 203 | F. Include, immediately after the copyright notices, a license notice 204 | giving the public permission to use the Modified Version under the 205 | terms of this License, in the form shown in the Addendum below. 206 | G. Preserve in that license notice the full lists of Invariant Sections 207 | and required Cover Texts given in the Document's license notice. 208 | H. Include an unaltered copy of this License. 209 | I. Preserve the section Entitled "History", Preserve its Title, and add 210 | to it an item stating at least the title, year, new authors, and 211 | publisher of the Modified Version as given on the Title Page. If 212 | there is no section Entitled "History" in the Document, create one 213 | stating the title, year, authors, and publisher of the Document as 214 | given on its Title Page, then add an item describing the Modified 215 | Version as stated in the previous sentence. 216 | J. Preserve the network location, if any, given in the Document for 217 | public access to a Transparent copy of the Document, and likewise 218 | the network locations given in the Document for previous versions 219 | it was based on. These may be placed in the "History" section. 220 | You may omit a network location for a work that was published at 221 | least four years before the Document itself, or if the original 222 | publisher of the version it refers to gives permission. 223 | K. For any section Entitled "Acknowledgements" or "Dedications", 224 | Preserve the Title of the section, and preserve in the section all 225 | the substance and tone of each of the contributor acknowledgements 226 | and/or dedications given therein. 227 | L. Preserve all the Invariant Sections of the Document, 228 | unaltered in their text and in their titles. Section numbers 229 | or the equivalent are not considered part of the section titles. 230 | M. Delete any section Entitled "Endorsements". Such a section 231 | may not be included in the Modified Version. 232 | N. Do not retitle any existing section to be Entitled "Endorsements" 233 | or to conflict in title with any Invariant Section. 234 | O. Preserve any Warranty Disclaimers. 235 | 236 | If the Modified Version includes new front-matter sections or 237 | appendices that qualify as Secondary Sections and contain no material 238 | copied from the Document, you may at your option designate some or all 239 | of these sections as invariant. To do this, add their titles to the 240 | list of Invariant Sections in the Modified Version's license notice. 241 | These titles must be distinct from any other section titles. 242 | 243 | You may add a section Entitled "Endorsements", provided it contains 244 | nothing but endorsements of your Modified Version by various 245 | parties--for example, statements of peer review or that the text has 246 | been approved by an organization as the authoritative definition of a 247 | standard. 248 | 249 | You may add a passage of up to five words as a Front-Cover Text, and a 250 | passage of up to 25 words as a Back-Cover Text, to the end of the list 251 | of Cover Texts in the Modified Version. Only one passage of 252 | Front-Cover Text and one of Back-Cover Text may be added by (or 253 | through arrangements made by) any one entity. If the Document already 254 | includes a cover text for the same cover, previously added by you or 255 | by arrangement made by the same entity you are acting on behalf of, 256 | you may not add another; but you may replace the old one, on explicit 257 | permission from the previous publisher that added the old one. 258 | 259 | The author(s) and publisher(s) of the Document do not by this License 260 | give permission to use their names for publicity for or to assert or 261 | imply endorsement of any Modified Version. 262 | 263 | 264 | 5. COMBINING DOCUMENTS 265 | 266 | You may combine the Document with other documents released under this 267 | License, under the terms defined in section 4 above for modified 268 | versions, provided that you include in the combination all of the 269 | Invariant Sections of all of the original documents, unmodified, and 270 | list them all as Invariant Sections of your combined work in its 271 | license notice, and that you preserve all their Warranty Disclaimers. 272 | 273 | The combined work need only contain one copy of this License, and 274 | multiple identical Invariant Sections may be replaced with a single 275 | copy. If there are multiple Invariant Sections with the same name but 276 | different contents, make the title of each such section unique by 277 | adding at the end of it, in parentheses, the name of the original 278 | author or publisher of that section if known, or else a unique number. 279 | Make the same adjustment to the section titles in the list of 280 | Invariant Sections in the license notice of the combined work. 281 | 282 | In the combination, you must combine any sections Entitled "History" 283 | in the various original documents, forming one section Entitled 284 | "History"; likewise combine any sections Entitled "Acknowledgements", 285 | and any sections Entitled "Dedications". You must delete all sections 286 | Entitled "Endorsements". 287 | 288 | 289 | 6. COLLECTIONS OF DOCUMENTS 290 | 291 | You may make a collection consisting of the Document and other 292 | documents released under this License, and replace the individual 293 | copies of this License in the various documents with a single copy 294 | that is included in the collection, provided that you follow the rules 295 | of this License for verbatim copying of each of the documents in all 296 | other respects. 297 | 298 | You may extract a single document from such a collection, and 299 | distribute it individually under this License, provided you insert a 300 | copy of this License into the extracted document, and follow this 301 | License in all other respects regarding verbatim copying of that 302 | document. 303 | 304 | 305 | 7. AGGREGATION WITH INDEPENDENT WORKS 306 | 307 | A compilation of the Document or its derivatives with other separate 308 | and independent documents or works, in or on a volume of a storage or 309 | distribution medium, is called an "aggregate" if the copyright 310 | resulting from the compilation is not used to limit the legal rights 311 | of the compilation's users beyond what the individual works permit. 312 | When the Document is included in an aggregate, this License does not 313 | apply to the other works in the aggregate which are not themselves 314 | derivative works of the Document. 315 | 316 | If the Cover Text requirement of section 3 is applicable to these 317 | copies of the Document, then if the Document is less than one half of 318 | the entire aggregate, the Document's Cover Texts may be placed on 319 | covers that bracket the Document within the aggregate, or the 320 | electronic equivalent of covers if the Document is in electronic form. 321 | Otherwise they must appear on printed covers that bracket the whole 322 | aggregate. 323 | 324 | 325 | 8. TRANSLATION 326 | 327 | Translation is considered a kind of modification, so you may 328 | distribute translations of the Document under the terms of section 4. 329 | Replacing Invariant Sections with translations requires special 330 | permission from their copyright holders, but you may include 331 | translations of some or all Invariant Sections in addition to the 332 | original versions of these Invariant Sections. You may include a 333 | translation of this License, and all the license notices in the 334 | Document, and any Warranty Disclaimers, provided that you also include 335 | the original English version of this License and the original versions 336 | of those notices and disclaimers. In case of a disagreement between 337 | the translation and the original version of this License or a notice 338 | or disclaimer, the original version will prevail. 339 | 340 | If a section in the Document is Entitled "Acknowledgements", 341 | "Dedications", or "History", the requirement (section 4) to Preserve 342 | its Title (section 1) will typically require changing the actual 343 | title. 344 | 345 | 346 | 9. TERMINATION 347 | 348 | You may not copy, modify, sublicense, or distribute the Document 349 | except as expressly provided under this License. Any attempt 350 | otherwise to copy, modify, sublicense, or distribute it is void, and 351 | will automatically terminate your rights under this License. 352 | 353 | However, if you cease all violation of this License, then your license 354 | from a particular copyright holder is reinstated (a) provisionally, 355 | unless and until the copyright holder explicitly and finally 356 | terminates your license, and (b) permanently, if the copyright holder 357 | fails to notify you of the violation by some reasonable means prior to 358 | 60 days after the cessation. 359 | 360 | Moreover, your license from a particular copyright holder is 361 | reinstated permanently if the copyright holder notifies you of the 362 | violation by some reasonable means, this is the first time you have 363 | received notice of violation of this License (for any work) from that 364 | copyright holder, and you cure the violation prior to 30 days after 365 | your receipt of the notice. 366 | 367 | Termination of your rights under this section does not terminate the 368 | licenses of parties who have received copies or rights from you under 369 | this License. If your rights have been terminated and not permanently 370 | reinstated, receipt of a copy of some or all of the same material does 371 | not give you any rights to use it. 372 | 373 | 374 | 10. FUTURE REVISIONS OF THIS LICENSE 375 | 376 | The Free Software Foundation may publish new, revised versions of the 377 | GNU Free Documentation License from time to time. Such new versions 378 | will be similar in spirit to the present version, but may differ in 379 | detail to address new problems or concerns. See 380 | https://www.gnu.org/licenses/. 381 | 382 | Each version of the License is given a distinguishing version number. 383 | If the Document specifies that a particular numbered version of this 384 | License "or any later version" applies to it, you have the option of 385 | following the terms and conditions either of that specified version or 386 | of any later version that has been published (not as a draft) by the 387 | Free Software Foundation. If the Document does not specify a version 388 | number of this License, you may choose any version ever published (not 389 | as a draft) by the Free Software Foundation. If the Document 390 | specifies that a proxy can decide which future versions of this 391 | License can be used, that proxy's public statement of acceptance of a 392 | version permanently authorizes you to choose that version for the 393 | Document. 394 | 395 | 11. RELICENSING 396 | 397 | "Massive Multiauthor Collaboration Site" (or "MMC Site") means any 398 | World Wide Web server that publishes copyrightable works and also 399 | provides prominent facilities for anybody to edit those works. A 400 | public wiki that anybody can edit is an example of such a server. A 401 | "Massive Multiauthor Collaboration" (or "MMC") contained in the site 402 | means any set of copyrightable works thus published on the MMC site. 403 | 404 | "CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0 405 | license published by Creative Commons Corporation, a not-for-profit 406 | corporation with a principal place of business in San Francisco, 407 | California, as well as future copyleft versions of that license 408 | published by that same organization. 409 | 410 | "Incorporate" means to publish or republish a Document, in whole or in 411 | part, as part of another Document. 412 | 413 | An MMC is "eligible for relicensing" if it is licensed under this 414 | License, and if all works that were first published under this License 415 | somewhere other than this MMC, and subsequently incorporated in whole or 416 | in part into the MMC, (1) had no cover texts or invariant sections, and 417 | (2) were thus incorporated prior to November 1, 2008. 418 | 419 | The operator of an MMC Site may republish an MMC contained in the site 420 | under CC-BY-SA on the same site at any time before August 1, 2009, 421 | provided the MMC is eligible for relicensing. 422 | 423 | 424 | ADDENDUM: How to use this License for your documents 425 | 426 | To use this License in a document you have written, include a copy of 427 | the License in the document and put the following copyright and 428 | license notices just after the title page: 429 | 430 | Copyright (c) YEAR YOUR NAME. 431 | Permission is granted to copy, distribute and/or modify this document 432 | under the terms of the GNU Free Documentation License, Version 1.3 433 | or any later version published by the Free Software Foundation; 434 | with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. 435 | A copy of the license is included in the section entitled "GNU 436 | Free Documentation License". 437 | 438 | If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, 439 | replace the "with...Texts." line with this: 440 | 441 | with the Invariant Sections being LIST THEIR TITLES, with the 442 | Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. 443 | 444 | If you have Invariant Sections without Cover Texts, or some other 445 | combination of the three, merge those two alternatives to suit the 446 | situation. 447 | 448 | If your document contains nontrivial examples of program code, we 449 | recommend releasing these examples in parallel under your choice of 450 | free software license, such as the GNU General Public License, 451 | to permit their use in free software. 452 | -------------------------------------------------------------------------------- /docs/archetypes/default.md: -------------------------------------------------------------------------------- 1 | +++ 2 | date = '{{ .Date }}' 3 | draft = true 4 | title = '{{ replace .File.ContentBaseName "-" " " | title }}' 5 | +++ 6 | -------------------------------------------------------------------------------- /docs/assets/sass/mysti-guides/variables/_custom.scss: -------------------------------------------------------------------------------- 1 | $bg-primary--light: #2a76b7; 2 | $bg-primary--dark: #235886; 3 | -------------------------------------------------------------------------------- /docs/content/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome To Hasty Paste II 3 | --- 4 | Hasty Paste II is a lighting fast pastebin that features a sleek and responsive web UI. 5 | 6 | > Paste it all, with haste. 7 | 8 | ## Top Features 9 | 10 | {{< container/container type="grid" classes="center" >}} 11 | 12 | {{% container/element %}} 13 | ### Fast ⚡️ 14 | Using the latest web tech 15 | {{% /container/element %}} 16 | 17 | {{% container/element %}} 18 | ### Open Source 🌐 19 | Allowing for community improvement and inspection. 20 | {{% /container/element %}} 21 | 22 | {{% container/element %}} 23 | ### Themes 🌙 24 | Includes both light and dark mode, to suite your preference. 25 | {{% /container/element %}} 26 | 27 | {{% container/element %}} 28 | ### Assets 📷 29 | Upload and attach files to your pastes. 30 | {{% /container/element %}} 31 | 32 | {{% container/element %}} 33 | ### SSO 🔑 34 | SSO support with OpenID/OAuth2. 35 | {{% /container/element %}} 36 | 37 | {{% container/element %}} 38 | ### Access Control 🔒 39 | Control how your paste is shared (Public, Unlisted, Private). 40 | 41 | {{% /container/element %}} 42 | 43 | {{< /container/container >}} 44 | 45 | ## Like What You See? 46 | - [Tell Me More]({{< ref "about" >}}) 47 | - [How Do I Install]({{< ref "docs/setup/install" >}}) 48 | 49 | ## Support My Work 50 | Like this project? Consider supporting me financially so I can continue development. You can do that by [buying me a coffee](https://www.buymeacoffee.com/leospratt). 51 | -------------------------------------------------------------------------------- /docs/content/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About 3 | --- 4 | Hasty Paste II is a lighting fast pastebin that features a sleek and responsive web UI. 5 | 6 | > Paste it all, with haste. 7 | 8 | ## Showcase 9 | ![Showcase image, showing several app pages](/showcase.webp) 10 | 11 | ## Features 12 | - Paste visibility 13 | - Private 14 | - Unlisted 15 | - Public 16 | - Paste assets 17 | - Paste content rendering (plain, markdown, code) 18 | - Customisable paste slug (or random) 19 | - Paste expiry 20 | - User accounts with "anonymous" mode 21 | - SSO via OpenID/OAuth2 22 | - Dark/Light theme 23 | 24 | ## What's The Future Look Like? 25 | Stay Tuned! 26 | -------------------------------------------------------------------------------- /docs/content/docs/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Docs 3 | --- 4 | Choose a section from the TOC either at the side or above this message on mobile. 5 | 6 | - [Development Guide]({{< ref devs >}}) 7 | - [Setup Guide]({{< ref setup >}}) 8 | -------------------------------------------------------------------------------- /docs/content/docs/devs/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Development 3 | --- 4 | -------------------------------------------------------------------------------- /docs/content/docs/devs/setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Environment Setup 3 | --- 4 | 5 | ## Requirements 6 | - [Run Tool](https://github.com/enchant97/run-tool) 7 | - App 8 | - Go 1.23 9 | - Node v20 (with corepack) 10 | - templ: `go install github.com/a-h/templ/cmd/templ@latest` 11 | - sqlc: `go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest` 12 | - Docs 13 | - Hugo 14 | -------------------------------------------------------------------------------- /docs/content/docs/setup/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup 3 | --- 4 | -------------------------------------------------------------------------------- /docs/content/docs/setup/backup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 03 - Backups 3 | --- 4 | To avoid data-loss it is important to backup your application data. 5 | 6 | ## What Do I Need To Backup? 7 | Here's a simple list: 8 | 9 | - Database, located at: `DB_URI` 10 | - If using a database server, you will need to perform a database dump. 11 | - If using sqlite, just copying the sqlite file (while app is shutdown) is needed. 12 | - Paste Assets, located at: `ATTACHMENTS_PATH` 13 | - Copy directory (while app is shutdown) 14 | 15 | ## How Is Data Stored? 16 | Most data is stored in the database however, paste attachments are stored on the file system in the following format: 17 | 18 | ```text 19 | attachments/ 20 | 01953292-cfb0-7d1b-924e-de60d957ca6a.bin 21 | ... 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/content/docs/setup/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 02 - Configuration 3 | --- 4 | Configuration of the app is done through environment variables. See the below options: 5 | 6 | > TIP A secret can be generated using: `openssl rand -base64 32` 7 | 8 | | Key | Description | Default | 9 | |:----|:------------|:--------| 10 | | `BIND__HOST` | What interface to listen on | `127.0.0.1` | 11 | | `BIND__PORT` | What port to listen on | `8080` | 12 | | `OIDC__ENABLED` | Whether OpenID/OAuth2 Is Enabled | `false` | 13 | | `OIDC__NAME` | The provider name (used for UI) | - | 14 | | `OIDC__ISSUER_URL` | The OIDC issuer url | - | 15 | | `OIDC__CLIENT_ID` | The client id | - | 16 | | `OIDC__CLIENT_SECRET` | The client secret | - | 17 | | `PUBLIC_URL` | Public URL where service can be accessed | - | 18 | | `BEHIND_PROXY` | Whether app is behind a reverse proxy | `false` | 19 | | `DB_URI` | URI for database connection | - | 20 | | `ATTACHMENTS_PATH` | where paste attachments will be stored | - | 21 | | `AUTH_TOKEN_SECRET` | base64 encoded secret | - | 22 | | `AUTH_TOKEN_EXPIRY` | seconds until a token expires | `604800` | 23 | | `SESSION_SECRET` | base64 encoded secret | - | 24 | | `SIGNUP_ENABLED` | Whether to allow new accounts to be created | `true` | 25 | | `INTERNAL_AUTH_ENABLED` | Whether to allow login for internal accounts | `true` | 26 | | `RANDOM_SLUG_LENGTH` | How long the randomly generated slug will be | `10` | 27 | | `ANONYMOUS_PASTES_ENABLED` | Whether to allow anonymous users to create new pastes | `true` | 28 | | `MAX_PASTE_SIZE` | Max paste size in bytes including all attachments | `12582912` | 29 | | `ATTACHMENTS_ENABLED` | Whether to allow attachments | `true` | 30 | 31 | ## OIDC 32 | Single-Sign-On is handled via OpenID Connect and OAuth2. To use SSO you must have a compatible provider that supports the following features: 33 | 34 | - OpenID Connect (OIDC) Discovery - RFC5785 35 | - Claims 36 | - `sub`: the users id 37 | - `name`: a users full name 38 | - `preferred_username`: the users username, not the email 39 | - Scopes 40 | - openid 41 | - profile 42 | 43 | Depending on your SSO provider the issuer URL may be different, see below for examples: 44 | 45 | Authentik: 46 | 47 | ```text 48 | https://{example.com}/application/o/{hasty-paste}/ 49 | ``` 50 | 51 | ## Database URI 52 | Database URIs have to be set in a specific format, see below examples: 53 | 54 | SQLite: 55 | 56 | ```text 57 | sqlite://path/to/db.sqlite 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/content/docs/setup/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 01 - Install 3 | --- 4 | 5 | ## Docker (Official) 6 | The app is distributed by a Docker image. This guide will assume you are using Docker, however you should be able to run via other container platforms such as: Kubernetes or Podman. 7 | 8 | Below is the image path: 9 | 10 | ```text 11 | ghcr.io/enchant97/hasty-paste 12 | ``` 13 | 14 | The following labels are available: 15 | 16 | > TIP: Image labels follow Semantic Versioning 17 | 18 | ```text 19 | 20 | 21 | . 22 | 23 | .. 24 | ``` 25 | 26 | Here is an example Docker Compose file: 27 | 28 | ```yaml 29 | volumes: 30 | data: 31 | 32 | services: 33 | hasty-paste: 34 | image: ghcr.io/enchant97/hasty-paste:2 35 | restart: unless-stopped 36 | volumes: 37 | - data:/opt/hasty-paste/data 38 | environment: 39 | AUTH_TOKEN_SECRET: "${AUTH_TOKEN_SECRET}" 40 | SESSION_SECRET: "${SESSION_SECRET}" 41 | PUBLIC_URL: "http://example.com" 42 | ports: 43 | - 80:8080 44 | ``` 45 | 46 | Take a look at the [configuration]({{< ref configuration.md >}}) chapter to find more about the configuration values. 47 | 48 | > TIP: It is recommended to use a reverse proxy to provide https and a custom FQDN. 49 | 50 | ## Bare 51 | Not officially supported, but you should be able to follow the steps that the Dockerfile performs. 52 | -------------------------------------------------------------------------------- /docs/go.mod: -------------------------------------------------------------------------------- 1 | module enchant97/hasty-paste/docs 2 | 3 | go 1.23.4 4 | 5 | require github.com/enchant97/hugo-mysti-guides v0.0.0-20241231143403-529a113e0701 // indirect 6 | -------------------------------------------------------------------------------- /docs/go.sum: -------------------------------------------------------------------------------- 1 | github.com/enchant97/hugo-mysti-guides v0.0.0-20241231143403-529a113e0701 h1:utasJ/5oApxQvlXQAiF3mDM6f1xfgw2ZvHodp0unmsw= 2 | github.com/enchant97/hugo-mysti-guides v0.0.0-20241231143403-529a113e0701/go.mod h1:FCPF7N2+WOedylj4iFiMJZqeYCKseDU+XdsCDHkqj1c= 3 | -------------------------------------------------------------------------------- /docs/hugo.toml: -------------------------------------------------------------------------------- 1 | baseURL = "https://hastypaste.docs.enchantedcode.co.uk/" 2 | languageCode = "en" 3 | title = "Hasty Paste II" 4 | copyright = "Leo Spratt" 5 | 6 | [markup.highlight] 7 | noClasses = false 8 | 9 | enableGitInfo = true 10 | 11 | [menu] 12 | [[menu.main]] 13 | name = "Home" 14 | url = "/" 15 | [[menu.main]] 16 | name = "Docs" 17 | url = "/docs/" 18 | [[menu.main]] 19 | name = "About" 20 | url = "/about/" 21 | [[menu.sub]] 22 | name = "Repository" 23 | url = "https://github.com/enchant97/hasty-paste" 24 | [[menu.sub.params]] 25 | icon = "git-branch" 26 | hideName = true 27 | [[menu.sub]] 28 | name = "Support The Project" 29 | url = "https://www.buymeacoffee.com/leospratt" 30 | [[menu.sub.params]] 31 | icon = "coffee" 32 | 33 | [[params.favicon]] 34 | href = "/icon.svg" 35 | 36 | [module] 37 | [[module.imports]] 38 | path = "github.com/enchant97/hugo-mysti-guides" 39 | -------------------------------------------------------------------------------- /docs/static/icon.svg: -------------------------------------------------------------------------------- 1 | ../../app/public/icon.svg -------------------------------------------------------------------------------- /docs/static/showcase.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enchant97/hasty-paste/6558e0eb08d2be5cd1dda18a3e6ab2c36f94b115/docs/static/showcase.webp --------------------------------------------------------------------------------