mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-30 21:55:43 +00:00 
			
		
		
		
	Compare commits
	
		
			17 Commits
		
	
	
		
			v3.1.0-exp
			...
			refactor/a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 673381dae6 | ||
|   | 9747ea8014 | ||
|   | cb2521f3d9 | ||
|   | d859f74a10 | ||
|   | 7e39cb0dfe | ||
|   | be836c296c | ||
|   | 6f6b1f4862 | ||
|   | ada3531565 | ||
|   | 939ed26fd0 | ||
|   | da0641c115 | ||
|   | 753b95baff | ||
|   | 9dd9829058 | ||
|   | ec67ea3807 | ||
|   | 3649d0d84e | ||
|   | c0ffe3faf4 | ||
|   | ad718d3ef8 | ||
|   | 38105d0b4e | 
							
								
								
									
										30
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| PORT=3000 | ||||
| ADDRESS=0.0.0.0 | ||||
| SECRET=app_secret | ||||
| SECRET_FILE=app_secret_file | ||||
| APP_URL=http://localhost:3000 | ||||
| USERS=your_user_password_hash | ||||
| USERS_FILE=users_file | ||||
| COOKIE_SECURE=false | ||||
| GITHUB_CLIENT_ID=github_client_id | ||||
| GITHUB_CLIENT_SECRET=github_client_secret | ||||
| GITHUB_CLIENT_SECRET_FILE=github_client_secret_file | ||||
| GOOGLE_CLIENT_ID=google_client_id | ||||
| GOOGLE_CLIENT_SECRET=google_client_secret | ||||
| GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file | ||||
| TAILSCALE_CLIENT_ID=tailscale_client_id | ||||
| TAILSCALE_CLIENT_SECRET=tailscale_client_secret | ||||
| TAILSCALE_CLIENT_SECRET_FILE=tailscale__client_secret_file | ||||
| GENERIC_CLIENT_ID=generic_client_id | ||||
| GENERIC_CLIENT_SECRET=generic_client_secret | ||||
| GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file | ||||
| GENERIC_SCOPES=generic_scopes | ||||
| GENERIC_AUTH_URL=generic_auth_url | ||||
| GENERIC_TOKEN_URL=generic_token_url | ||||
| GENERIC_USER_URL=generic_user_url | ||||
| DISABLE_CONTINUE=false | ||||
| OAUTH_WHITELIST= | ||||
| GENERIC_NAME=My OAuth | ||||
| SESSION_EXPIRY=7200 | ||||
| LOG_LEVEL=0 | ||||
| APP_TITLE=Tinyauth SSO | ||||
							
								
								
									
										58
									
								
								.github/workflows/alpha-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/alpha-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,58 +0,0 @@ | ||||
| name: Alpha Release | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|     inputs: | ||||
|       alpha: | ||||
|         description: "Alpha version (e.g. 1, 2, 3)" | ||||
|         required: true | ||||
|  | ||||
| jobs: | ||||
|   get-tag: | ||||
|     runs-on: ubuntu-latest | ||||
|     outputs: | ||||
|       tag: ${{ steps.tag.outputs.name }} | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Get tag | ||||
|         id: tag | ||||
|         run: echo "name=$(cat internal/assets/version)-alpha.${{ github.event.inputs.alpha }}" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   build-docker: | ||||
|     needs: get-tag | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v3 | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           platforms: linux/arm64, linux/amd64 | ||||
|           tags: ghcr.io/${{ github.repository_owner }}/tinyauth:${{ needs.get-tag.outputs.tag }} | ||||
|  | ||||
|   alpha-release: | ||||
|     needs: [get-tag, build-docker] | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Create alpha release | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         with: | ||||
|           prerelease: true | ||||
|           tag_name: ${{ needs.get-tag.outputs.tag }} | ||||
							
								
								
									
										58
									
								
								.github/workflows/beta-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/beta-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,58 +0,0 @@ | ||||
| name: Beta Release | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|     inputs: | ||||
|       alpha: | ||||
|         description: "Beta version (e.g. 1, 2, 3)" | ||||
|         required: true | ||||
|  | ||||
| jobs: | ||||
|   get-tag: | ||||
|     runs-on: ubuntu-latest | ||||
|     outputs: | ||||
|       tag: ${{ steps.tag.outputs.name }} | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Get tag | ||||
|         id: tag | ||||
|         run: echo "name=$(cat internal/assets/version)-beta.${{ github.event.inputs.alpha }}" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   build-docker: | ||||
|     needs: get-tag | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v3 | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           platforms: linux/arm64, linux/amd64 | ||||
|           tags: ghcr.io/${{ github.repository_owner }}/tinyauth:${{ needs.get-tag.outputs.tag }} | ||||
|  | ||||
|   beta-release: | ||||
|     needs: [get-tag, build-docker] | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Create beta release | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         with: | ||||
|           prerelease: true | ||||
|           tag_name: ${{ needs.get-tag.outputs.tag }} | ||||
							
								
								
									
										136
									
								
								.github/workflows/experimental-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										136
									
								
								.github/workflows/experimental-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,136 +0,0 @@ | ||||
| name: Experimental Build | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     tags: | ||||
|       - "v*" | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Docker meta | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           images: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v6 | ||||
|         id: build | ||||
|         with: | ||||
|           platforms: linux/amd64 | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           tags: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|           outputs: type=image,push-by-digest=true,name-canonical=true,push=true | ||||
|  | ||||
|       - name: Export digest | ||||
|         run: | | ||||
|           mkdir -p ${{ runner.temp }}/digests | ||||
|           digest="${{ steps.build.outputs.digest }}" | ||||
|           touch "${{ runner.temp }}/digests/${digest#sha256:}" | ||||
|  | ||||
|       - name: Upload digest | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: digests-linux-amd64 | ||||
|           path: ${{ runner.temp }}/digests/* | ||||
|           if-no-files-found: error | ||||
|           retention-days: 1 | ||||
|  | ||||
|   build-arm: | ||||
|     runs-on: ubuntu-24.04-arm | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Docker meta | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           images: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v6 | ||||
|         id: build | ||||
|         with: | ||||
|           platforms: linux/arm64 | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           tags: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|           outputs: type=image,push-by-digest=true,name-canonical=true,push=true | ||||
|  | ||||
|       - name: Export digest | ||||
|         run: | | ||||
|           mkdir -p ${{ runner.temp }}/digests | ||||
|           digest="${{ steps.build.outputs.digest }}" | ||||
|           touch "${{ runner.temp }}/digests/${digest#sha256:}" | ||||
|  | ||||
|       - name: Upload digest | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: digests-linux-arm64 | ||||
|           path: ${{ runner.temp }}/digests/* | ||||
|           if-no-files-found: error | ||||
|           retention-days: 1 | ||||
|  | ||||
|   merge: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - build | ||||
|     steps: | ||||
|       - name: Download digests | ||||
|         uses: actions/download-artifact@v4 | ||||
|         with: | ||||
|           path: ${{ runner.temp }}/digests | ||||
|           pattern: digests-* | ||||
|           merge-multiple: true | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Docker meta | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           images: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|           tags: | | ||||
|             type=ref,event=branch | ||||
|             type=ref,event=pr | ||||
|             type=semver,pattern={{version}} | ||||
|             type=semver,pattern={{major}}.{{minor}} | ||||
|  | ||||
|       - name: Create manifest list and push | ||||
|         working-directory: ${{ runner.temp }}/digests | ||||
|         run: | | ||||
|           docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | ||||
|             $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) | ||||
							
								
								
									
										145
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										145
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,32 +1,22 @@ | ||||
| name: Release | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     tags: | ||||
|       - "v*" | ||||
|  | ||||
| jobs: | ||||
|   get-tag: | ||||
|     runs-on: ubuntu-latest | ||||
|     outputs: | ||||
|       tag: ${{ steps.tag.outputs.name }} | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Get tag | ||||
|         id: tag | ||||
|         run: echo "name=$(cat internal/assets/version)" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   build-docker: | ||||
|     needs: get-tag | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v3 | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Docker meta | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           images: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
| @@ -35,21 +25,112 @@ jobs: | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v6 | ||||
|         id: build | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           platforms: linux/arm64, linux/amd64 | ||||
|           tags: ghcr.io/${{ github.repository_owner }}/tinyauth:${{ needs.get-tag.outputs.tag }}, ghcr.io/${{ github.repository_owner }}/tinyauth:latest | ||||
|           platforms: linux/amd64 | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           tags: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|           outputs: type=image,push-by-digest=true,name-canonical=true,push=true | ||||
|  | ||||
|   release: | ||||
|     needs: [get-tag, build-docker] | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Create release | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|       - name: Export digest | ||||
|         run: | | ||||
|           mkdir -p ${{ runner.temp }}/digests | ||||
|           digest="${{ steps.build.outputs.digest }}" | ||||
|           touch "${{ runner.temp }}/digests/${digest#sha256:}" | ||||
|  | ||||
|       - name: Upload digest | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           prerelease: false | ||||
|           make_latest: false | ||||
|           tag_name: ${{ needs.get-tag.outputs.tag }} | ||||
|           name: digests-linux-amd64 | ||||
|           path: ${{ runner.temp }}/digests/* | ||||
|           if-no-files-found: error | ||||
|           retention-days: 1 | ||||
|  | ||||
|   build-arm: | ||||
|     runs-on: ubuntu-24.04-arm | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Docker meta | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           images: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v6 | ||||
|         id: build | ||||
|         with: | ||||
|           platforms: linux/arm64 | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           tags: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|           outputs: type=image,push-by-digest=true,name-canonical=true,push=true | ||||
|  | ||||
|       - name: Export digest | ||||
|         run: | | ||||
|           mkdir -p ${{ runner.temp }}/digests | ||||
|           digest="${{ steps.build.outputs.digest }}" | ||||
|           touch "${{ runner.temp }}/digests/${digest#sha256:}" | ||||
|  | ||||
|       - name: Upload digest | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: digests-linux-arm64 | ||||
|           path: ${{ runner.temp }}/digests/* | ||||
|           if-no-files-found: error | ||||
|           retention-days: 1 | ||||
|  | ||||
|   merge: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - build | ||||
|       - build-arm | ||||
|     steps: | ||||
|       - name: Download digests | ||||
|         uses: actions/download-artifact@v4 | ||||
|         with: | ||||
|           path: ${{ runner.temp }}/digests | ||||
|           pattern: digests-* | ||||
|           merge-multiple: true | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Docker meta | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           images: ghcr.io/${{ github.repository_owner }}/tinyauth | ||||
|           tags: | | ||||
|             type=semver,pattern=v{{version}} | ||||
|             type=semver,pattern=v{{major}} | ||||
|             type=semver,pattern=v{{major}}.{{minor}} | ||||
|  | ||||
|       - name: Create manifest list and push | ||||
|         working-directory: ${{ runner.temp }}/digests | ||||
|         run: | | ||||
|           docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | ||||
|             $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) | ||||
|   | ||||
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -18,4 +18,10 @@ secret_oauth.txt | ||||
| .vscode | ||||
|  | ||||
| # apple stuff | ||||
| .DS_Store | ||||
| .DS_Store | ||||
|  | ||||
| # env | ||||
| .env | ||||
|  | ||||
| # tmp directory | ||||
| tmp | ||||
| @@ -1,6 +1,6 @@ | ||||
| # Contributing | ||||
|  | ||||
| Contributing is relatively easy. | ||||
| Contributing is relatively easy, you just need to follow the steps carefully and you will be up and running with a development server in less than 5 minutes. | ||||
|  | ||||
| ## Requirements | ||||
|  | ||||
| @@ -20,62 +20,37 @@ cd tinyauth | ||||
|  | ||||
| ## Install requirements | ||||
|  | ||||
| Now it's time to install the requirements, firstly the Go ones: | ||||
| Although you will not need the requirements in your machine since the development will happen in docker, I still recommend to install them because this way you will not have errors, to install the go requirements, run: | ||||
|  | ||||
| ```sh | ||||
| go mod download | ||||
| go mod tidy | ||||
| ``` | ||||
|  | ||||
| And now the site ones: | ||||
| You also need to download the frontend requirements, this can be done like so: | ||||
|  | ||||
| ```sh | ||||
| cd site | ||||
| bun i | ||||
| cd site/ | ||||
| bun install | ||||
| ``` | ||||
|  | ||||
| ## Developing locally | ||||
| ## Create your `.env` file | ||||
|  | ||||
| In order to develop the app locally you need to build the frontend and copy it to the assets folder in order for Go to embed it and host it. In order to build the frontend run: | ||||
| In order to ocnfigure the app you need to create an environment file, this can be done by copying the `.env.example` file to `.env` and modifying the environment variables inside to suit your needs. | ||||
|  | ||||
| ```sh | ||||
| cd site | ||||
| bun run build | ||||
| cd .. | ||||
| ``` | ||||
| ## Developing | ||||
|  | ||||
| Copy it to the assets folder: | ||||
|  | ||||
| ```sh | ||||
| rm -rf internal/assets/dist | ||||
| cp -r site/dist internal/assets/dist | ||||
| ``` | ||||
|  | ||||
| Finally either run the app with: | ||||
|  | ||||
| ```sh | ||||
| go run main.go | ||||
| ``` | ||||
|  | ||||
| Or build it with: | ||||
|  | ||||
| ```sh | ||||
| go build | ||||
| ``` | ||||
|  | ||||
| > [!WARNING] | ||||
| > Make sure you have set the environment variables when running outside of docker else the app will fail. | ||||
|  | ||||
| ## Developing in docker | ||||
|  | ||||
| My recommended development method is docker so I can test that both my image works and that the app responds correctly to traefik. In my setup I have set these two DNS records in my DNS server: | ||||
| I have designed the development workflow to be entirely in docker, this is because it will directly work with traefik and you will not need to do any building in your host machine. The recommended development setup is to have a subdomain pointing to your machine like this: | ||||
|  | ||||
| ``` | ||||
| *.dev.local -> 127.0.0.1 | ||||
| dev.local -> 127.0.0.1 | ||||
| *.dev.example.com -> 127.0.0.1 | ||||
| dev.example.com -> 127.0.0.1 | ||||
| ``` | ||||
|  | ||||
| Then I can just make sure the domains are correct in the example docker compose file and do: | ||||
| Then you can just make sure the domains are correct in the example docker compose file and run: | ||||
|  | ||||
| ```sh | ||||
| docker compose -f docker-compose.dev.yml up --build | ||||
| ``` | ||||
|  | ||||
| > [!NOTE] | ||||
| > I would recommend copying the example `docker-compose.dev.yml` into a `docker-compose.test.yml` file, so as you don't accidentally commit any sensitive information. | ||||
|   | ||||
							
								
								
									
										22
									
								
								Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| FROM golang:1.23-alpine3.21 | ||||
|  | ||||
| WORKDIR /tinyauth | ||||
|  | ||||
| COPY go.mod ./ | ||||
| COPY go.sum ./ | ||||
|  | ||||
| RUN go mod download | ||||
|  | ||||
| COPY ./cmd ./cmd | ||||
| COPY ./internal ./internal | ||||
| COPY ./main.go ./ | ||||
| COPY ./air.toml ./ | ||||
|  | ||||
| RUN mkdir -p ./internal/assets/dist && \ | ||||
|     echo "app running" > ./internal/assets/dist/index.html | ||||
|  | ||||
| RUN go install github.com/air-verse/air@v1.61.7 | ||||
|  | ||||
| EXPOSE 3000 | ||||
|  | ||||
| ENTRYPOINT ["air", "-c", "air.toml"] | ||||
							
								
								
									
										25
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								Makefile
									
									
									
									
									
								
							| @@ -1,25 +0,0 @@ | ||||
| # Build website | ||||
| web: | ||||
| 	cd site; bun run build | ||||
|  | ||||
| # Copy site assets | ||||
| assets: web | ||||
| 	rm -rf internal/assets/dist | ||||
| 	mkdir -p internal/assets/dist | ||||
| 	cp -r site/dist/* internal/assets/dist | ||||
|  | ||||
| # Run development binary | ||||
| run: assets | ||||
| 	go run main.go | ||||
|  | ||||
| # Test | ||||
| test: | ||||
| 	go test ./... | ||||
|  | ||||
| # Build | ||||
| build: assets | ||||
| 	go build -o tinyauth | ||||
|  | ||||
| # Build no site | ||||
| build-skip-web: | ||||
| 	go build -o tinyauth | ||||
| @@ -28,11 +28,11 @@ I just made a Discord server for Tinyauth! It is not only for Tinyauth but gener | ||||
|  | ||||
| ## Getting Started | ||||
|  | ||||
| You can easily get started with tinyauth by following the guide on the [documentation](https://tinyauth.doesmycode.work/docs/getting-started.html). There is also an available [docker compose file](./docker-compose.example.yml) that has traefik, nginx and tinyauth to demonstrate its capabilities. | ||||
| You can easily get started with tinyauth by following the guide on the [documentation](https://tinyauth.app/docs/getting-started.html). There is also an available [docker compose file](./docker-compose.example.yml) that has traefik, nginx and tinyauth to demonstrate its capabilities. | ||||
|  | ||||
| ## Documentation | ||||
|  | ||||
| You can find documentation and guides on all available configuration of tinyauth [here](https://tinyauth.doesmycode.work). | ||||
| You can find documentation and guides on all available configuration of tinyauth [here](https://tinyauth.app). | ||||
|  | ||||
| ## Contributing | ||||
|  | ||||
|   | ||||
							
								
								
									
										24
									
								
								air.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								air.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| root = "/tinyauth" | ||||
| tmp_dir = "tmp" | ||||
|  | ||||
| [build] | ||||
| pre_cmd = ["go mod tidy"] | ||||
| cmd = "go build -o ./tmp/tinyauth ." | ||||
| bin = "tmp/tinyauth" | ||||
| include_ext = ["go"] | ||||
| exclude_dir = ["internal/assets/dist"] | ||||
| exclude_regex = [".*_test\\.go"] | ||||
| stop_on_error = true | ||||
|  | ||||
| [color] | ||||
| main = "magenta" | ||||
| watcher = "cyan" | ||||
| build = "yellow" | ||||
| runner = "green" | ||||
|  | ||||
| [misc] | ||||
| clean_on_exit = true | ||||
|  | ||||
| [screen] | ||||
| clear_on_rebuild = false | ||||
| keep_scroll = true | ||||
| @@ -3,8 +3,8 @@ | ||||
|   "embeds": [ | ||||
|     { | ||||
|       "title": "Welcome to Tinyauth Discord!", | ||||
|       "description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.\n\n**Information**\n\n• Github: <https://github.com/steveiliop56/tinyauth>\n• Website: <https://tinyauth.doesmycode.work>", | ||||
|       "url": "https://tinyauth.doesmycode.work", | ||||
|       "description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.\n\n**Information**\n\n• Github: <https://github.com/steveiliop56/tinyauth>\n• Website: <https://tinyauth.app>", | ||||
|       "url": "https://tinyauth.app", | ||||
|       "color": 7002085, | ||||
|       "author": { | ||||
|         "name": "Tinyauth" | ||||
| @@ -12,11 +12,11 @@ | ||||
|       "footer": { | ||||
|         "text": "Updated at" | ||||
|       }, | ||||
|       "timestamp": "2025-02-06T22:00:00.000Z", | ||||
|       "timestamp": "2025-03-10T19:00:00.000Z", | ||||
|       "thumbnail": { | ||||
|         "url": "https://github.com/steveiliop56/tinyauth/blob/main/site/public/logo.png?raw=true" | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "attachments": [] | ||||
| } | ||||
| } | ||||
| @@ -8,27 +8,41 @@ services: | ||||
|     volumes: | ||||
|       - /var/run/docker.sock:/var/run/docker.sock | ||||
|  | ||||
|   nginx: | ||||
|     container_name: nginx | ||||
|     image: nginx:latest | ||||
|   whoami: | ||||
|     container_name: whoami | ||||
|     image: traefik/whoami:latest | ||||
|     labels: | ||||
|       traefik.enable: true | ||||
|       traefik.http.routers.nginx.rule: Host(`nginx.dev.local`) | ||||
|       traefik.http.routers.nginx.rule: Host(`whoami.example.com`) | ||||
|       traefik.http.services.nginx.loadbalancer.server.port: 80 | ||||
|       traefik.http.routers.nginx.middlewares: tinyauth | ||||
|  | ||||
|   tinyauth: | ||||
|     container_name: tinyauth | ||||
|   tinyauth-frontend: | ||||
|     container_name: tinyauth-frontend | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: Dockerfile | ||||
|     environment: | ||||
|       - SECRET=some-random-32-chars-string | ||||
|       - APP_URL=http://tinyauth.dev.local | ||||
|       - USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password | ||||
|       dockerfile: site/Dockerfile.dev | ||||
|     volumes: | ||||
|       - ./site/src:/site/src | ||||
|     ports: | ||||
|       - 5173:5173 | ||||
|     labels: | ||||
|       traefik.enable: true | ||||
|       traefik.http.routers.tinyauth.rule: Host(`tinyauth.dev.local`) | ||||
|       traefik.http.services.tinyauth.loadbalancer.server.port: 3000 | ||||
|       traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik | ||||
|       traefik.http.middlewares.tinyauth.forwardauth.authResponseHeaders: Remote-User | ||||
|       traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`) | ||||
|       traefik.http.services.tinyauth.loadbalancer.server.port: 5173 | ||||
|  | ||||
|   tinyauth-backend: | ||||
|     container_name: tinyauth-backend | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: Dockerfile.dev | ||||
|     env_file: .env | ||||
|     volumes: | ||||
|       - ./internal:/tinyauth/internal | ||||
|       - ./cmd:/tinyauth/cmd | ||||
|       - ./main.go:/tinyauth/main.go | ||||
|     ports: | ||||
|       - 3000:3000 | ||||
|     labels: | ||||
|       traefik.enable: true | ||||
|       traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth-backend:3000/api/auth/traefik | ||||
|   | ||||
| @@ -8,18 +8,18 @@ services: | ||||
|     volumes: | ||||
|       - /var/run/docker.sock:/var/run/docker.sock | ||||
|  | ||||
|   nginx: | ||||
|     container_name: nginx | ||||
|     image: nginx:latest | ||||
|   whoami: | ||||
|     container_name: whoami | ||||
|     image: traefik/whoami:latest | ||||
|     labels: | ||||
|       traefik.enable: true | ||||
|       traefik.http.routers.nginx.rule: Host(`nginx.example.com`) | ||||
|       traefik.http.routers.nginx.rule: Host(`whoami.example.com`) | ||||
|       traefik.http.services.nginx.loadbalancer.server.port: 80 | ||||
|       traefik.http.routers.nginx.middlewares: tinyauth | ||||
|  | ||||
|   tinyauth: | ||||
|     container_name: tinyauth | ||||
|     image: ghcr.io/steveiliop56/tinyauth:latest | ||||
|     image: ghcr.io/steveiliop56/tinyauth:v3 | ||||
|     environment: | ||||
|       - SECRET=some-random-32-chars-string | ||||
|       - APP_URL=https://tinyauth.example.com | ||||
| @@ -29,4 +29,3 @@ services: | ||||
|       traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`) | ||||
|       traefik.http.services.tinyauth.loadbalancer.server.port: 3000 | ||||
|       traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik | ||||
|       traefik.http.middlewares.tinyauth.forwardauth.authResponseHeaders: Remote-User | ||||
|   | ||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -7,6 +7,7 @@ require ( | ||||
| 	github.com/gin-gonic/gin v1.10.0 | ||||
| 	github.com/go-playground/validator/v10 v10.24.0 | ||||
| 	github.com/google/go-querystring v1.1.0 | ||||
| 	github.com/mdp/qrterminal/v3 v3.2.0 | ||||
| 	github.com/rs/zerolog v1.33.0 | ||||
| 	github.com/spf13/cobra v1.8.1 | ||||
| 	github.com/spf13/viper v1.19.0 | ||||
| @@ -15,7 +16,6 @@ require ( | ||||
|  | ||||
| require ( | ||||
| 	github.com/containerd/log v0.1.0 // indirect | ||||
| 	github.com/mdp/qrterminal/v3 v3.2.0 // indirect | ||||
| 	github.com/moby/term v0.5.2 // indirect | ||||
| 	github.com/morikuni/aec v1.0.0 // indirect | ||||
| 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect | ||||
|   | ||||
| @@ -131,18 +131,24 @@ func (api *API) SetupRoutes() { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy") | ||||
| 		// Check if the request is coming from a browser (tools like curl/bruno use */* and they don't include the text/html) | ||||
| 		isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html") | ||||
|  | ||||
| 		// Check if using basic auth | ||||
| 		_, _, basicAuth := c.Request.BasicAuth() | ||||
| 		if isBrowser { | ||||
| 			log.Debug().Msg("Request is most likely coming from a browser") | ||||
| 		} else { | ||||
| 			log.Debug().Msg("Request is most likely not coming from a browser") | ||||
| 		} | ||||
|  | ||||
| 		log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy") | ||||
|  | ||||
| 		// Check if auth is enabled | ||||
| 		authEnabled, authEnabledErr := api.Auth.AuthEnabled(c) | ||||
|  | ||||
| 		// Handle error | ||||
| 		if authEnabledErr != nil { | ||||
| 			// Return 500 if nginx is the proxy or if the request is using basic auth | ||||
| 			if proxy.Proxy == "nginx" || basicAuth { | ||||
| 			// Return 500 if nginx is the proxy or if the request is not coming from a browser | ||||
| 			if proxy.Proxy == "nginx" || !isBrowser { | ||||
| 				log.Error().Err(authEnabledErr).Msg("Failed to check if auth is enabled") | ||||
| 				c.JSON(500, gin.H{ | ||||
| 					"status":  500, | ||||
| @@ -186,8 +192,8 @@ func (api *API) SetupRoutes() { | ||||
|  | ||||
| 			// Check if there was an error | ||||
| 			if appAllowedErr != nil { | ||||
| 				// Return 500 if nginx is the proxy or if the request is using basic auth | ||||
| 				if proxy.Proxy == "nginx" || basicAuth { | ||||
| 				// Return 500 if nginx is the proxy or if the request is not coming from a browser | ||||
| 				if proxy.Proxy == "nginx" || !isBrowser { | ||||
| 					log.Error().Err(appAllowedErr).Msg("Failed to check if app is allowed") | ||||
| 					c.JSON(500, gin.H{ | ||||
| 						"status":  500, | ||||
| @@ -208,9 +214,11 @@ func (api *API) SetupRoutes() { | ||||
| 			if !appAllowed { | ||||
| 				log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User not allowed") | ||||
|  | ||||
| 				// Return 401 if nginx is the proxy or if the request is using an Authorization header | ||||
| 				if proxy.Proxy == "nginx" || basicAuth { | ||||
| 					c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") | ||||
| 				// Set WWW-Authenticate header | ||||
| 				c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") | ||||
|  | ||||
| 				// Return 401 if nginx is the proxy or if the request is not coming from a browser | ||||
| 				if proxy.Proxy == "nginx" || !isBrowser { | ||||
| 					c.JSON(401, gin.H{ | ||||
| 						"status":  401, | ||||
| 						"message": "Unauthorized", | ||||
| @@ -252,9 +260,11 @@ func (api *API) SetupRoutes() { | ||||
| 		// The user is not logged in | ||||
| 		log.Debug().Msg("Unauthorized") | ||||
|  | ||||
| 		// Return 401 if nginx is the proxy or if the request is using an Authorization header | ||||
| 		if proxy.Proxy == "nginx" || basicAuth { | ||||
| 			c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") | ||||
| 		// Set www-authenticate header | ||||
| 		c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") | ||||
|  | ||||
| 		// Return 401 if nginx is the proxy or if the request is not coming from a browser | ||||
| 		if proxy.Proxy == "nginx" || !isBrowser { | ||||
| 			c.JSON(401, gin.H{ | ||||
| 				"status":  401, | ||||
| 				"message": "Unauthorized", | ||||
| @@ -362,7 +372,7 @@ func (api *API) SetupRoutes() { | ||||
|  | ||||
| 	api.Router.POST("/api/totp", func(c *gin.Context) { | ||||
| 		// Create totp struct | ||||
| 		var totpReq types.Totp | ||||
| 		var totpReq types.TotpRequest | ||||
|  | ||||
| 		// Bind JSON | ||||
| 		err := c.BindJSON(&totpReq) | ||||
| @@ -451,11 +461,8 @@ func (api *API) SetupRoutes() { | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	api.Router.GET("/api/status", func(c *gin.Context) { | ||||
| 		log.Debug().Msg("Checking status") | ||||
|  | ||||
| 		// Get user context | ||||
| 		userContext := api.Hooks.UseUserContext(c) | ||||
| 	api.Router.GET("/api/app", func(c *gin.Context) { | ||||
| 		log.Debug().Msg("Getting app context") | ||||
|  | ||||
| 		// Get configured providers | ||||
| 		configuredProviders := api.Providers.GetConfiguredProviders() | ||||
| @@ -465,33 +472,48 @@ func (api *API) SetupRoutes() { | ||||
| 			configuredProviders = append(configuredProviders, "username") | ||||
| 		} | ||||
|  | ||||
| 		// Fill status struct with data from user context and api config | ||||
| 		status := types.Status{ | ||||
| 			Username:            userContext.Username, | ||||
| 			IsLoggedIn:          userContext.IsLoggedIn, | ||||
| 			Oauth:               userContext.OAuth, | ||||
| 			Provider:            userContext.Provider, | ||||
| 		// Create app context struct | ||||
| 		appContext := types.AppContext{ | ||||
| 			Status:              200, | ||||
| 			Message:             "Ok", | ||||
| 			ConfiguredProviders: configuredProviders, | ||||
| 			DisableContinue:     api.Config.DisableContinue, | ||||
| 			Title:               api.Config.Title, | ||||
| 			GenericName:         api.Config.GenericName, | ||||
| 			TotpPending:         userContext.TotpPending, | ||||
| 		} | ||||
|  | ||||
| 		// Return app context | ||||
| 		c.JSON(200, appContext) | ||||
| 	}) | ||||
|  | ||||
| 	api.Router.GET("/api/user", func(c *gin.Context) { | ||||
| 		log.Debug().Msg("Getting user context") | ||||
|  | ||||
| 		// Get user context | ||||
| 		userContext := api.Hooks.UseUserContext(c) | ||||
|  | ||||
| 		// Create user context response | ||||
| 		userContextResponse := types.UserContextResponse{ | ||||
| 			Status:      200, | ||||
| 			IsLoggedIn:  userContext.IsLoggedIn, | ||||
| 			Username:    userContext.Username, | ||||
| 			Provider:    userContext.Provider, | ||||
| 			Oauth:       userContext.OAuth, | ||||
| 			TotpPending: userContext.TotpPending, | ||||
| 		} | ||||
|  | ||||
| 		// If we are not logged in we set the status to 401 and add the WWW-Authenticate header else we set it to 200 | ||||
| 		if !userContext.IsLoggedIn { | ||||
| 			log.Debug().Msg("Unauthorized") | ||||
| 			c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") | ||||
| 			status.Status = 401 | ||||
| 			status.Message = "Unauthorized" | ||||
| 			userContextResponse.Message = "Unauthorized" | ||||
| 		} else { | ||||
| 			log.Debug().Interface("userContext", userContext).Strs("configuredProviders", configuredProviders).Bool("disableContinue", api.Config.DisableContinue).Msg("Authenticated") | ||||
| 			status.Status = 200 | ||||
| 			status.Message = "Authenticated" | ||||
| 			log.Debug().Interface("userContext", userContext).Msg("Authenticated") | ||||
| 			userContextResponse.Message = "Authenticated" | ||||
| 		} | ||||
|  | ||||
| 		// Return data | ||||
| 		c.JSON(200, status) | ||||
| 		// Return user context | ||||
| 		c.JSON(200, userContextResponse) | ||||
| 	}) | ||||
|  | ||||
| 	api.Router.GET("/api/oauth/url/:provider", func(c *gin.Context) { | ||||
| @@ -700,7 +722,12 @@ func (api *API) Run() { | ||||
| 	log.Info().Str("address", api.Config.Address).Int("port", api.Config.Port).Msg("Starting server") | ||||
|  | ||||
| 	// Run server | ||||
| 	api.Router.Run(fmt.Sprintf("%s:%d", api.Config.Address, api.Config.Port)) | ||||
| 	err := api.Router.Run(fmt.Sprintf("%s:%d", api.Config.Address, api.Config.Port)) | ||||
|  | ||||
| 	// Check error | ||||
| 	if err != nil { | ||||
| 		log.Fatal().Err(err).Msg("Failed to start server") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // handleError logs the error and redirects to the error page (only meant for stuff the user may access does not apply for login paths) | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package api_test | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"strings" | ||||
| @@ -122,9 +123,9 @@ func TestLogin(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Test status | ||||
| func TestStatus(t *testing.T) { | ||||
| 	t.Log("Testing status") | ||||
| // Test user context | ||||
| func TestUserContext(t *testing.T) { | ||||
| 	t.Log("Testing user context") | ||||
|  | ||||
| 	// Get API | ||||
| 	api := getAPI(t) | ||||
| @@ -133,7 +134,7 @@ func TestStatus(t *testing.T) { | ||||
| 	recorder := httptest.NewRecorder() | ||||
|  | ||||
| 	// Create request | ||||
| 	req, err := http.NewRequest("GET", "/api/status", nil) | ||||
| 	req, err := http.NewRequest("GET", "/api/user", nil) | ||||
|  | ||||
| 	// Check if there was an error | ||||
| 	if err != nil { | ||||
| @@ -152,11 +153,31 @@ func TestStatus(t *testing.T) { | ||||
| 	// Assert | ||||
| 	assert.Equal(t, recorder.Code, http.StatusOK) | ||||
|  | ||||
| 	// Parse the body | ||||
| 	body := recorder.Body.String() | ||||
| 	// Read the body of the response | ||||
| 	body, bodyErr := io.ReadAll(recorder.Body) | ||||
|  | ||||
| 	if !strings.Contains(body, "user") { | ||||
| 		t.Fatalf("Expected user in body") | ||||
| 	// Check if there was an error | ||||
| 	if bodyErr != nil { | ||||
| 		t.Fatalf("Error getting body: %v", bodyErr) | ||||
| 	} | ||||
|  | ||||
| 	// Unmarshal the body into the user struct | ||||
| 	type User struct { | ||||
| 		Username string `json:"username"` | ||||
| 	} | ||||
|  | ||||
| 	var user User | ||||
|  | ||||
| 	jsonErr := json.Unmarshal(body, &user) | ||||
|  | ||||
| 	// Check if there was an error | ||||
| 	if jsonErr != nil { | ||||
| 		t.Fatalf("Error unmarshalling body: %v", jsonErr) | ||||
| 	} | ||||
|  | ||||
| 	// We should get the username back | ||||
| 	if user.Username != "user" { | ||||
| 		t.Fatalf("Expected user, got %s", user.Username) | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -162,7 +162,10 @@ func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bo | ||||
| 	// Check if resource is allowed | ||||
| 	allowed, allowedErr := auth.Docker.ContainerAction(appId, func(labels types.TinyauthLabels) (bool, error) { | ||||
| 		// If the container has an oauth whitelist, check if the user is in it | ||||
| 		if context.OAuth && len(labels.OAuthWhitelist) != 0 { | ||||
| 		if context.OAuth { | ||||
| 			if len(labels.OAuthWhitelist) == 0 { | ||||
| 				return true, nil | ||||
| 			} | ||||
| 			log.Debug().Msg("Checking OAuth whitelist") | ||||
| 			if slices.Contains(labels.OAuthWhitelist, context.Username) { | ||||
| 				return true, nil | ||||
|   | ||||
| @@ -55,6 +55,7 @@ type Config struct { | ||||
| 	SessionExpiry             int    `mapstructure:"session-expiry"` | ||||
| 	LogLevel                  int8   `mapstructure:"log-level" validate:"min=-1,max=5"` | ||||
| 	Title                     string `mapstructure:"app-title"` | ||||
| 	EnvFile                   string `mapstructure:"env-file"` | ||||
| } | ||||
|  | ||||
| // UserContext is the context for the user | ||||
| @@ -138,22 +139,28 @@ type Proxy struct { | ||||
| 	Proxy string `uri:"proxy" binding:"required"` | ||||
| } | ||||
|  | ||||
| // Status response | ||||
| type Status struct { | ||||
| // User Context response is the response for the user context endpoint | ||||
| type UserContextResponse struct { | ||||
| 	Status      int    `json:"status"` | ||||
| 	Message     string `json:"message"` | ||||
| 	IsLoggedIn  bool   `json:"isLoggedIn"` | ||||
| 	Username    string `json:"username"` | ||||
| 	Provider    string `json:"provider"` | ||||
| 	Oauth       bool   `json:"oauth"` | ||||
| 	TotpPending bool   `json:"totpPending"` | ||||
| } | ||||
|  | ||||
| // App Context is the response for the app context endpoint | ||||
| type AppContext struct { | ||||
| 	Status              int      `json:"status"` | ||||
| 	Message             string   `json:"message"` | ||||
| 	IsLoggedIn          bool     `json:"isLoggedIn"` | ||||
| 	Username            string   `json:"username"` | ||||
| 	Provider            string   `json:"provider"` | ||||
| 	Oauth               bool     `json:"oauth"` | ||||
| 	ConfiguredProviders []string `json:"configuredProviders"` | ||||
| 	DisableContinue     bool     `json:"disableContinue"` | ||||
| 	Title               string   `json:"title"` | ||||
| 	GenericName         string   `json:"genericName"` | ||||
| 	TotpPending         bool     `json:"totpPending"` | ||||
| } | ||||
|  | ||||
| // Totp request | ||||
| type Totp struct { | ||||
| // Totp request is the request for the totp endpoint | ||||
| type TotpRequest struct { | ||||
| 	Code string `json:"code"` | ||||
| } | ||||
|   | ||||
							
								
								
									
										23
									
								
								site/Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								site/Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| FROM oven/bun:1.1.45-alpine | ||||
|  | ||||
| WORKDIR /site | ||||
|  | ||||
| COPY ./site/package.json ./ | ||||
| COPY ./site/bun.lockb ./ | ||||
|  | ||||
| RUN bun install | ||||
|  | ||||
| COPY ./site/public ./public | ||||
| COPY ./site/src ./src | ||||
|  | ||||
| COPY ./site/eslint.config.js ./ | ||||
| COPY ./site/index.html ./ | ||||
| COPY ./site/tsconfig.json ./ | ||||
| COPY ./site/tsconfig.app.json ./ | ||||
| COPY ./site/tsconfig.node.json ./ | ||||
| COPY ./site/vite.config.ts ./ | ||||
| COPY ./site/postcss.config.cjs ./ | ||||
|  | ||||
| EXPOSE 5173 | ||||
|  | ||||
| ENTRYPOINT ["bun", "run", "dev"] | ||||
							
								
								
									
										42
									
								
								site/src/context/app-context.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								site/src/context/app-context.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import React, { createContext, useContext } from "react"; | ||||
| import axios from "axios"; | ||||
| import { AppContextSchemaType } from "../schemas/app-context-schema"; | ||||
|  | ||||
| const AppContext = createContext<AppContextSchemaType | null>(null); | ||||
|  | ||||
| export const AppContextProvider = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode; | ||||
| }) => { | ||||
|   const { | ||||
|     data: userContext, | ||||
|     isLoading, | ||||
|     error, | ||||
|   } = useQuery({ | ||||
|     queryKey: ["appContext"], | ||||
|     queryFn: async () => { | ||||
|       const res = await axios.get("/api/app"); | ||||
|       return res.data; | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   if (error && !isLoading) { | ||||
|     throw error; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <AppContext.Provider value={userContext}>{children}</AppContext.Provider> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const useAppContext = () => { | ||||
|   const context = useContext(AppContext); | ||||
|  | ||||
|   if (context === null) { | ||||
|     throw new Error("useAppContext must be used within an AppContextProvider"); | ||||
|   } | ||||
|  | ||||
|   return context; | ||||
| }; | ||||
| @@ -17,7 +17,7 @@ export const UserContextProvider = ({ | ||||
|   } = useQuery({ | ||||
|     queryKey: ["userContext"], | ||||
|     queryFn: async () => { | ||||
|       const res = await axios.get("/api/status"); | ||||
|       const res = await axios.get("/api/user"); | ||||
|       return res.data; | ||||
|     }, | ||||
|   }); | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import { NotFoundPage } from "./pages/not-found-page.tsx"; | ||||
| import { UnauthorizedPage } from "./pages/unauthorized-page.tsx"; | ||||
| import { InternalServerError } from "./pages/internal-server-error.tsx"; | ||||
| import { TotpPage } from "./pages/totp-page.tsx"; | ||||
| import { AppContextProvider } from "./context/app-context.tsx"; | ||||
|  | ||||
| const queryClient = new QueryClient({ | ||||
|   defaultOptions: { | ||||
| @@ -30,20 +31,22 @@ createRoot(document.getElementById("root")!).render( | ||||
|     <MantineProvider forceColorScheme="dark"> | ||||
|       <QueryClientProvider client={queryClient}> | ||||
|         <Notifications /> | ||||
|         <UserContextProvider> | ||||
|           <BrowserRouter> | ||||
|             <Routes> | ||||
|               <Route path="/" element={<App />} /> | ||||
|               <Route path="/login" element={<LoginPage />} /> | ||||
|               <Route path="/totp" element={<TotpPage />} /> | ||||
|               <Route path="/logout" element={<LogoutPage />} /> | ||||
|               <Route path="/continue" element={<ContinuePage />} /> | ||||
|               <Route path="/unauthorized" element={<UnauthorizedPage />} /> | ||||
|               <Route path="/error" element={<InternalServerError />} /> | ||||
|               <Route path="*" element={<NotFoundPage />} /> | ||||
|             </Routes> | ||||
|           </BrowserRouter> | ||||
|         </UserContextProvider> | ||||
|         <AppContextProvider> | ||||
|           <UserContextProvider> | ||||
|             <BrowserRouter> | ||||
|               <Routes> | ||||
|                 <Route path="/" element={<App />} /> | ||||
|                 <Route path="/login" element={<LoginPage />} /> | ||||
|                 <Route path="/totp" element={<TotpPage />} /> | ||||
|                 <Route path="/logout" element={<LogoutPage />} /> | ||||
|                 <Route path="/continue" element={<ContinuePage />} /> | ||||
|                 <Route path="/unauthorized" element={<UnauthorizedPage />} /> | ||||
|                 <Route path="/error" element={<InternalServerError />} /> | ||||
|                 <Route path="*" element={<NotFoundPage />} /> | ||||
|               </Routes> | ||||
|             </BrowserRouter> | ||||
|           </UserContextProvider> | ||||
|         </AppContextProvider> | ||||
|       </QueryClientProvider> | ||||
|     </MantineProvider> | ||||
|   </StrictMode>, | ||||
|   | ||||
| @@ -5,13 +5,15 @@ import { useUserContext } from "../context/user-context"; | ||||
| import { Layout } from "../components/layouts/layout"; | ||||
| import { ReactNode } from "react"; | ||||
| import { isQueryValid } from "../utils/utils"; | ||||
| import { useAppContext } from "../context/app-context"; | ||||
|  | ||||
| export const ContinuePage = () => { | ||||
|   const queryString = window.location.search; | ||||
|   const params = new URLSearchParams(queryString); | ||||
|   const redirectUri = params.get("redirect_uri") ?? ""; | ||||
|  | ||||
|   const { isLoggedIn, disableContinue } = useUserContext(); | ||||
|   const { isLoggedIn } = useUserContext(); | ||||
|   const { disableContinue } = useAppContext(); | ||||
|  | ||||
|   if (!isLoggedIn) { | ||||
|     return <Navigate to={`/login?redirect_uri=${redirectUri}`} />; | ||||
|   | ||||
| @@ -9,14 +9,15 @@ import { OAuthButtons } from "../components/auth/oauth-buttons"; | ||||
| import { LoginFormValues } from "../schemas/login-schema"; | ||||
| import { LoginForm } from "../components/auth/login-forn"; | ||||
| import { isQueryValid } from "../utils/utils"; | ||||
| import { useAppContext } from "../context/app-context"; | ||||
|  | ||||
| export const LoginPage = () => { | ||||
|   const queryString = window.location.search; | ||||
|   const params = new URLSearchParams(queryString); | ||||
|   const redirectUri = params.get("redirect_uri") ?? ""; | ||||
|  | ||||
|   const { isLoggedIn, configuredProviders, title, genericName } = | ||||
|     useUserContext(); | ||||
|   const { isLoggedIn } = useUserContext(); | ||||
|   const { configuredProviders, title, genericName } = useAppContext(); | ||||
|  | ||||
|   const oauthProviders = configuredProviders.filter( | ||||
|     (value) => value !== "username", | ||||
|   | ||||
| @@ -6,9 +6,11 @@ import { useUserContext } from "../context/user-context"; | ||||
| import { Navigate } from "react-router"; | ||||
| import { Layout } from "../components/layouts/layout"; | ||||
| import { capitalize } from "../utils/utils"; | ||||
| import { useAppContext } from "../context/app-context"; | ||||
|  | ||||
| export const LogoutPage = () => { | ||||
|   const { isLoggedIn, username, oauth, provider, genericName } = useUserContext(); | ||||
|   const { isLoggedIn, username, oauth, provider } = useUserContext(); | ||||
|   const { genericName } = useAppContext(); | ||||
|  | ||||
|   if (!isLoggedIn) { | ||||
|     return <Navigate to="/login" />; | ||||
| @@ -45,8 +47,9 @@ export const LogoutPage = () => { | ||||
|         </Text> | ||||
|         <Text> | ||||
|           You are currently logged in as <Code>{username}</Code> | ||||
|           {oauth && ` using ${capitalize(provider === "generic" ? genericName : provider)} OAuth`}. Click the button | ||||
|           below to log out. | ||||
|           {oauth && | ||||
|             ` using ${capitalize(provider === "generic" ? genericName : provider)} OAuth`} | ||||
|           . Click the button below to log out. | ||||
|         </Text> | ||||
|         <Button | ||||
|           fullWidth | ||||
|   | ||||
| @@ -6,13 +6,15 @@ import { TotpForm } from "../components/auth/totp-form"; | ||||
| import { useMutation } from "@tanstack/react-query"; | ||||
| import axios from "axios"; | ||||
| import { notifications } from "@mantine/notifications"; | ||||
| import { useAppContext } from "../context/app-context"; | ||||
|  | ||||
| export const TotpPage = () => { | ||||
|   const queryString = window.location.search; | ||||
|   const params = new URLSearchParams(queryString); | ||||
|   const redirectUri = params.get("redirect_uri") ?? ""; | ||||
|  | ||||
|   const { totpPending, isLoggedIn, title } = useUserContext(); | ||||
|   const { totpPending, isLoggedIn } = useUserContext(); | ||||
|   const { title } = useAppContext(); | ||||
|  | ||||
|   if (isLoggedIn) { | ||||
|     return <Navigate to={`/logout`} />; | ||||
|   | ||||
							
								
								
									
										10
									
								
								site/src/schemas/app-context-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								site/src/schemas/app-context-schema.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { z } from "zod"; | ||||
|  | ||||
| export const appContextSchema = z.object({ | ||||
|   configuredProviders: z.array(z.string()), | ||||
|   disableContinue: z.boolean(), | ||||
|   title: z.string(), | ||||
|   genericName: z.string(), | ||||
| }); | ||||
|  | ||||
| export type AppContextSchemaType = z.infer<typeof appContextSchema>; | ||||
| @@ -5,10 +5,6 @@ export const userContextSchema = z.object({ | ||||
|   username: z.string(), | ||||
|   oauth: z.boolean(), | ||||
|   provider: z.string(), | ||||
|   configuredProviders: z.array(z.string()), | ||||
|   disableContinue: z.boolean(), | ||||
|   title: z.string(), | ||||
|   genericName: z.string(), | ||||
|   totpPending: z.boolean(), | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -4,4 +4,14 @@ import react from "@vitejs/plugin-react-swc"; | ||||
| // https://vite.dev/config/ | ||||
| export default defineConfig({ | ||||
|   plugins: [react()], | ||||
|   server: { | ||||
|     host: "0.0.0.0", | ||||
|     proxy: { | ||||
|       "/api": { | ||||
|         target: "http://tinyauth-backend:3000/api", | ||||
|         changeOrigin: true, | ||||
|         rewrite: (path) => path.replace(/^\/api/, ""), | ||||
|       }, | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user