mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-04 08:05:42 +00:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			v3.2.0-bet
			...
			feat/multi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					df10eb65ce | ||
| 
						 | 
					8bf5a6067e | 
@@ -26,7 +26,5 @@ DISABLE_CONTINUE=false
 | 
				
			|||||||
OAUTH_WHITELIST=
 | 
					OAUTH_WHITELIST=
 | 
				
			||||||
GENERIC_NAME=My OAuth
 | 
					GENERIC_NAME=My OAuth
 | 
				
			||||||
SESSION_EXPIRY=7200
 | 
					SESSION_EXPIRY=7200
 | 
				
			||||||
LOGIN_TIMEOUT=300
 | 
					 | 
				
			||||||
LOGIN_MAX_RETRIES=5
 | 
					 | 
				
			||||||
LOG_LEVEL=0
 | 
					LOG_LEVEL=0
 | 
				
			||||||
APP_TITLE=Tinyauth SSO
 | 
					APP_TITLE=Tinyauth SSO
 | 
				
			||||||
							
								
								
									
										58
									
								
								.github/workflows/translations.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/translations.yml
									
									
									
									
										vendored
									
									
								
							@@ -3,7 +3,7 @@ name: Publish translations
 | 
				
			|||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches:
 | 
					    branches:
 | 
				
			||||||
      - i18n_v*
 | 
					      - main
 | 
				
			||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
@@ -16,53 +16,7 @@ concurrency:
 | 
				
			|||||||
  cancel-in-progress: false
 | 
					  cancel-in-progress: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  get-branches:
 | 
					 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					 | 
				
			||||||
    outputs:
 | 
					 | 
				
			||||||
      i18n-branches: ${{ steps.get-branches.outputs.result }}
 | 
					 | 
				
			||||||
    steps:
 | 
					 | 
				
			||||||
      - name: Get branches
 | 
					 | 
				
			||||||
        id: get-branches
 | 
					 | 
				
			||||||
        uses: actions/github-script@v7
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          script: |
 | 
					 | 
				
			||||||
            const { data: repos } = await github.rest.repos.listBranches({
 | 
					 | 
				
			||||||
              owner: context.repo.owner,
 | 
					 | 
				
			||||||
              repo: context.repo.repo,
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const i18nBranches = repos.filter((branch) => branch.name.startsWith("i18n_v"))
 | 
					 | 
				
			||||||
            const i18nBranchNames = i18nBranches.map((branch) => branch.name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return i18nBranchNames
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  get-translations:
 | 
					 | 
				
			||||||
    needs: get-branches
 | 
					 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					 | 
				
			||||||
    strategy:
 | 
					 | 
				
			||||||
      matrix:
 | 
					 | 
				
			||||||
        branch: ${{ fromJson(needs.get-branches.outputs.i18n-branches) }}
 | 
					 | 
				
			||||||
    steps:
 | 
					 | 
				
			||||||
      - name: Checkout
 | 
					 | 
				
			||||||
        uses: actions/checkout@v4
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          ref: ${{ matrix.branch }}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Get translation version
 | 
					 | 
				
			||||||
        id: get-version
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          branch=${{ matrix.branch }}
 | 
					 | 
				
			||||||
          version=${branch#i18n_}
 | 
					 | 
				
			||||||
          echo "version=$version" >> $GITHUB_OUTPUT
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Upload translations
 | 
					 | 
				
			||||||
        uses: actions/upload-artifact@v4
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          name: ${{ steps.get-version.outputs.version }}
 | 
					 | 
				
			||||||
          path: frontend/src/lib/i18n/locales
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  build:
 | 
					  build:
 | 
				
			||||||
    needs: get-translations
 | 
					 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - name: Checkout
 | 
					      - name: Checkout
 | 
				
			||||||
@@ -71,14 +25,10 @@ jobs:
 | 
				
			|||||||
      - name: Setup Pages
 | 
					      - name: Setup Pages
 | 
				
			||||||
        uses: actions/configure-pages@v4
 | 
					        uses: actions/configure-pages@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Prepare output directory
 | 
					      - name: Move translations
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          mkdir -p dist/i18n/
 | 
					          mkdir -p dist
 | 
				
			||||||
 | 
					          mv frontend/src/lib/i18n/locales dist/i18n
 | 
				
			||||||
      - name: Download translations
 | 
					 | 
				
			||||||
        uses: actions/download-artifact@v4
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          path: dist/i18n/
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Upload artifact
 | 
					      - name: Upload artifact
 | 
				
			||||||
        uses: actions/upload-pages-artifact@v3
 | 
					        uses: actions/upload-pages-artifact@v3
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,7 +61,3 @@ Credits for the logo of this app go to:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- **Freepik** for providing the police hat and badge.
 | 
					- **Freepik** for providing the police hat and badge.
 | 
				
			||||||
- **Renee French** for the original gopher logo.
 | 
					- **Renee French** for the original gopher logo.
 | 
				
			||||||
 | 
					 | 
				
			||||||
## Star History
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[](https://www.star-history.com/#steveiliop56/tinyauth&Date)
 | 
					 | 
				
			||||||
							
								
								
									
										32
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								cmd/root.go
									
									
									
									
									
								
							@@ -2,6 +2,7 @@ package cmd
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
@@ -93,8 +94,10 @@ var rootCmd = &cobra.Command{
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create handlers config
 | 
							// Create handlers config
 | 
				
			||||||
		handlersConfig := types.HandlersConfig{
 | 
							serverConfig := types.HandlersConfig{
 | 
				
			||||||
			AppURL:          config.AppURL,
 | 
								AppURL:          config.AppURL,
 | 
				
			||||||
 | 
								Domain:          fmt.Sprintf(".%s", domain),
 | 
				
			||||||
 | 
								CookieSecure:    config.CookieSecure,
 | 
				
			||||||
			DisableContinue: config.DisableContinue,
 | 
								DisableContinue: config.DisableContinue,
 | 
				
			||||||
			Title:           config.Title,
 | 
								Title:           config.Title,
 | 
				
			||||||
			GenericName:     config.GenericName,
 | 
								GenericName:     config.GenericName,
 | 
				
			||||||
@@ -106,18 +109,6 @@ var rootCmd = &cobra.Command{
 | 
				
			|||||||
			Address: config.Address,
 | 
								Address: config.Address,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create auth config
 | 
					 | 
				
			||||||
		authConfig := types.AuthConfig{
 | 
					 | 
				
			||||||
			Users:           users,
 | 
					 | 
				
			||||||
			OauthWhitelist:  oauthWhitelist,
 | 
					 | 
				
			||||||
			Secret:          config.Secret,
 | 
					 | 
				
			||||||
			CookieSecure:    config.CookieSecure,
 | 
					 | 
				
			||||||
			SessionExpiry:   config.SessionExpiry,
 | 
					 | 
				
			||||||
			Domain:          domain,
 | 
					 | 
				
			||||||
			LoginTimeout:    config.LoginTimeout,
 | 
					 | 
				
			||||||
			LoginMaxRetries: config.LoginMaxRetries,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Create docker service
 | 
							// Create docker service
 | 
				
			||||||
		docker := docker.NewDocker()
 | 
							docker := docker.NewDocker()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -126,7 +117,14 @@ var rootCmd = &cobra.Command{
 | 
				
			|||||||
		HandleError(err, "Failed to initialize docker")
 | 
							HandleError(err, "Failed to initialize docker")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create auth service
 | 
							// Create auth service
 | 
				
			||||||
		auth := auth.NewAuth(authConfig, docker)
 | 
							auth := auth.NewAuth(types.AuthConfig{
 | 
				
			||||||
 | 
								Domain:         domain,
 | 
				
			||||||
 | 
								Secret:         config.Secret,
 | 
				
			||||||
 | 
								SessionExpiry:  config.SessionExpiry,
 | 
				
			||||||
 | 
								CookieSecure:   config.CookieSecure,
 | 
				
			||||||
 | 
								Users:          users,
 | 
				
			||||||
 | 
								OAuthWhitelist: oauthWhitelist,
 | 
				
			||||||
 | 
							}, docker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create OAuth providers service
 | 
							// Create OAuth providers service
 | 
				
			||||||
		providers := providers.NewProviders(oauthConfig)
 | 
							providers := providers.NewProviders(oauthConfig)
 | 
				
			||||||
@@ -138,7 +136,7 @@ var rootCmd = &cobra.Command{
 | 
				
			|||||||
		hooks := hooks.NewHooks(auth, providers)
 | 
							hooks := hooks.NewHooks(auth, providers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create handlers
 | 
							// Create handlers
 | 
				
			||||||
		handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
 | 
							handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create API
 | 
							// Create API
 | 
				
			||||||
		api := api.NewAPI(apiConfig, handlers)
 | 
							api := api.NewAPI(apiConfig, handlers)
 | 
				
			||||||
@@ -203,8 +201,6 @@ func init() {
 | 
				
			|||||||
	rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
 | 
						rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
 | 
				
			||||||
	rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
 | 
						rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
 | 
				
			||||||
	rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
 | 
						rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
 | 
				
			||||||
	rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).")
 | 
					 | 
				
			||||||
	rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).")
 | 
					 | 
				
			||||||
	rootCmd.Flags().Int("log-level", 1, "Log level.")
 | 
						rootCmd.Flags().Int("log-level", 1, "Log level.")
 | 
				
			||||||
	rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
 | 
						rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -239,8 +235,6 @@ func init() {
 | 
				
			|||||||
	viper.BindEnv("session-expiry", "SESSION_EXPIRY")
 | 
						viper.BindEnv("session-expiry", "SESSION_EXPIRY")
 | 
				
			||||||
	viper.BindEnv("log-level", "LOG_LEVEL")
 | 
						viper.BindEnv("log-level", "LOG_LEVEL")
 | 
				
			||||||
	viper.BindEnv("app-title", "APP_TITLE")
 | 
						viper.BindEnv("app-title", "APP_TITLE")
 | 
				
			||||||
	viper.BindEnv("login-timeout", "LOGIN_TIMEOUT")
 | 
					 | 
				
			||||||
	viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Bind flags to viper
 | 
						// Bind flags to viper
 | 
				
			||||||
	viper.BindPFlags(rootCmd.Flags())
 | 
						viper.BindPFlags(rootCmd.Flags())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,7 +28,7 @@ i18n
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
      backendOptions: [
 | 
					      backendOptions: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
 | 
					          loadPath: "https://cdn.tinyauth.app/i18n/{{lng}}.json",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,6 @@
 | 
				
			|||||||
    "loginSubmit": "Login",
 | 
					    "loginSubmit": "Login",
 | 
				
			||||||
    "loginFailTitle": "Failed to log in",
 | 
					    "loginFailTitle": "Failed to log in",
 | 
				
			||||||
    "loginFailSubtitle": "Please check your username and password",
 | 
					    "loginFailSubtitle": "Please check your username and password",
 | 
				
			||||||
    "loginFailRateLimit": "You failed to login too many times, please try again later",
 | 
					 | 
				
			||||||
    "loginSuccessTitle": "Logged in",
 | 
					    "loginSuccessTitle": "Logged in",
 | 
				
			||||||
    "loginSuccessSubtitle": "Welcome back!",
 | 
					    "loginSuccessSubtitle": "Welcome back!",
 | 
				
			||||||
    "loginOauthFailTitle": "Internal error",
 | 
					    "loginOauthFailTitle": "Internal error",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,6 @@
 | 
				
			|||||||
    "loginSubmit": "Login",
 | 
					    "loginSubmit": "Login",
 | 
				
			||||||
    "loginFailTitle": "Failed to log in",
 | 
					    "loginFailTitle": "Failed to log in",
 | 
				
			||||||
    "loginFailSubtitle": "Please check your username and password",
 | 
					    "loginFailSubtitle": "Please check your username and password",
 | 
				
			||||||
    "loginFailRateLimit": "You failed to login too many times, please try again later",
 | 
					 | 
				
			||||||
    "loginSuccessTitle": "Logged in",
 | 
					    "loginSuccessTitle": "Logged in",
 | 
				
			||||||
    "loginSuccessSubtitle": "Welcome back!",
 | 
					    "loginSuccessSubtitle": "Welcome back!",
 | 
				
			||||||
    "loginOauthFailTitle": "Internal error",
 | 
					    "loginOauthFailTitle": "Internal error",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import { Paper, Title, Text, Divider } from "@mantine/core";
 | 
					import { Paper, Title, Text, Divider } from "@mantine/core";
 | 
				
			||||||
import { notifications } from "@mantine/notifications";
 | 
					import { notifications } from "@mantine/notifications";
 | 
				
			||||||
import { useMutation } from "@tanstack/react-query";
 | 
					import { useMutation } from "@tanstack/react-query";
 | 
				
			||||||
import axios, { type AxiosError } from "axios";
 | 
					import axios from "axios";
 | 
				
			||||||
import { useUserContext } from "../context/user-context";
 | 
					import { useUserContext } from "../context/user-context";
 | 
				
			||||||
import { Navigate } from "react-router";
 | 
					import { Navigate } from "react-router";
 | 
				
			||||||
import { Layout } from "../components/layouts/layout";
 | 
					import { Layout } from "../components/layouts/layout";
 | 
				
			||||||
@@ -33,17 +33,7 @@ export const LoginPage = () => {
 | 
				
			|||||||
    mutationFn: (login: LoginFormValues) => {
 | 
					    mutationFn: (login: LoginFormValues) => {
 | 
				
			||||||
      return axios.post("/api/login", login);
 | 
					      return axios.post("/api/login", login);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    onError: (data: AxiosError) => {
 | 
					    onError: () => {
 | 
				
			||||||
      if (data.response) {
 | 
					 | 
				
			||||||
        if (data.response.status === 429) {
 | 
					 | 
				
			||||||
          notifications.show({
 | 
					 | 
				
			||||||
            title: t("loginFailTitle"),
 | 
					 | 
				
			||||||
            message: t("loginFailRateLimit"),
 | 
					 | 
				
			||||||
            color: "red",
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
          return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      notifications.show({
 | 
					      notifications.show({
 | 
				
			||||||
        title: t("loginFailTitle"),
 | 
					        title: t("loginFailTitle"),
 | 
				
			||||||
        message: t("loginFailSubtitle"),
 | 
					        message: t("loginFailSubtitle"),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,15 +17,15 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
 | 
					func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
 | 
				
			||||||
	return &API{
 | 
						return &API{
 | 
				
			||||||
		Config:   config,
 | 
					 | 
				
			||||||
		Handlers: handlers,
 | 
							Handlers: handlers,
 | 
				
			||||||
 | 
							Config:   config,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type API struct {
 | 
					type API struct {
 | 
				
			||||||
	Config   types.APIConfig
 | 
					 | 
				
			||||||
	Router   *gin.Engine
 | 
						Router   *gin.Engine
 | 
				
			||||||
	Handlers *handlers.Handlers
 | 
						Handlers *handlers.Handlers
 | 
				
			||||||
 | 
						Config   types.APIConfig
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (api *API) Init() {
 | 
					func (api *API) Init() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,12 @@ import (
 | 
				
			|||||||
	"github.com/magiconair/properties/assert"
 | 
						"github.com/magiconair/properties/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// User
 | 
				
			||||||
 | 
					var User = types.User{
 | 
				
			||||||
 | 
						Username: "user",
 | 
				
			||||||
 | 
						Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Simple API config for tests
 | 
					// Simple API config for tests
 | 
				
			||||||
var apiConfig = types.APIConfig{
 | 
					var apiConfig = types.APIConfig{
 | 
				
			||||||
	Port:    8080,
 | 
						Port:    8080,
 | 
				
			||||||
@@ -28,6 +34,8 @@ var apiConfig = types.APIConfig{
 | 
				
			|||||||
// Simple handlers config for tests
 | 
					// Simple handlers config for tests
 | 
				
			||||||
var handlersConfig = types.HandlersConfig{
 | 
					var handlersConfig = types.HandlersConfig{
 | 
				
			||||||
	AppURL:          "http://localhost:8080",
 | 
						AppURL:          "http://localhost:8080",
 | 
				
			||||||
 | 
						Domain:          ".localhost",
 | 
				
			||||||
 | 
						CookieSecure:    false,
 | 
				
			||||||
	DisableContinue: false,
 | 
						DisableContinue: false,
 | 
				
			||||||
	Title:           "Tinyauth",
 | 
						Title:           "Tinyauth",
 | 
				
			||||||
	GenericName:     "Generic",
 | 
						GenericName:     "Generic",
 | 
				
			||||||
@@ -35,24 +43,19 @@ var handlersConfig = types.HandlersConfig{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Simple auth config for tests
 | 
					// Simple auth config for tests
 | 
				
			||||||
var authConfig = types.AuthConfig{
 | 
					var authConfig = types.AuthConfig{
 | 
				
			||||||
	Users:           types.Users{},
 | 
						Domain:        "localhost",
 | 
				
			||||||
	OauthWhitelist:  []string{},
 | 
						Secret:        "super-secret-api-thing-for-tests", // It is 32 chars long
 | 
				
			||||||
	Secret:          "super-secret-api-thing-for-tests", // It is 32 chars long
 | 
						CookieSecure:  false,
 | 
				
			||||||
	CookieSecure:    false,
 | 
						SessionExpiry: 3600,
 | 
				
			||||||
	SessionExpiry:   3600,
 | 
						Users: types.Users{
 | 
				
			||||||
	LoginTimeout:    0,
 | 
							User,
 | 
				
			||||||
	LoginMaxRetries: 0,
 | 
						},
 | 
				
			||||||
 | 
						OAuthWhitelist: []string{},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Cookie
 | 
					// Cookie
 | 
				
			||||||
var cookie string
 | 
					var cookie string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// User
 | 
					 | 
				
			||||||
var user = types.User{
 | 
					 | 
				
			||||||
	Username: "user",
 | 
					 | 
				
			||||||
	Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// We need all this to be able to test the API
 | 
					// We need all this to be able to test the API
 | 
				
			||||||
func getAPI(t *testing.T) *api.API {
 | 
					func getAPI(t *testing.T) *api.API {
 | 
				
			||||||
	// Create docker service
 | 
						// Create docker service
 | 
				
			||||||
@@ -67,12 +70,6 @@ func getAPI(t *testing.T) *api.API {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create auth service
 | 
						// Create auth service
 | 
				
			||||||
	authConfig.Users = types.Users{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Username: user.Username,
 | 
					 | 
				
			||||||
			Password: user.Password,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	auth := auth.NewAuth(authConfig, docker)
 | 
						auth := auth.NewAuth(authConfig, docker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create providers service
 | 
						// Create providers service
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,11 +2,9 @@ package auth
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
	"slices"
 | 
						"slices"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
	"tinyauth/internal/docker"
 | 
						"tinyauth/internal/docker"
 | 
				
			||||||
	"tinyauth/internal/types"
 | 
						"tinyauth/internal/types"
 | 
				
			||||||
@@ -19,41 +17,14 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
 | 
					func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
 | 
				
			||||||
	return &Auth{
 | 
						return &Auth{
 | 
				
			||||||
		Config:        config,
 | 
							Docker: docker,
 | 
				
			||||||
		Docker:        docker,
 | 
							Config: config,
 | 
				
			||||||
		LoginAttempts: make(map[string]*types.LoginAttempt),
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Auth struct {
 | 
					type Auth struct {
 | 
				
			||||||
	Config        types.AuthConfig
 | 
						Docker *docker.Docker
 | 
				
			||||||
	Docker        *docker.Docker
 | 
						Config types.AuthConfig
 | 
				
			||||||
	LoginAttempts map[string]*types.LoginAttempt
 | 
					 | 
				
			||||||
	LoginMutex    sync.RWMutex
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) {
 | 
					 | 
				
			||||||
	// Create cookie store
 | 
					 | 
				
			||||||
	store := sessions.NewCookieStore([]byte(auth.Config.Secret))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Configure cookie store
 | 
					 | 
				
			||||||
	store.Options = &sessions.Options{
 | 
					 | 
				
			||||||
		Path:     "/",
 | 
					 | 
				
			||||||
		MaxAge:   auth.Config.SessionExpiry,
 | 
					 | 
				
			||||||
		Secure:   auth.Config.CookieSecure,
 | 
					 | 
				
			||||||
		HttpOnly: true,
 | 
					 | 
				
			||||||
		SameSite: http.SameSiteDefaultMode,
 | 
					 | 
				
			||||||
		Domain:   fmt.Sprintf(".%s", auth.Config.Domain),
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get session
 | 
					 | 
				
			||||||
	session, err := store.Get(c.Request, "tinyauth")
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Error().Err(err).Msg("Failed to get session")
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return session, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (auth *Auth) GetUser(username string) *types.User {
 | 
					func (auth *Auth) GetUser(username string) *types.User {
 | 
				
			||||||
@@ -71,78 +42,14 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool {
 | 
				
			|||||||
	return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
 | 
						return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// IsAccountLocked checks if a username or IP is locked due to too many failed login attempts
 | 
					 | 
				
			||||||
func (auth *Auth) IsAccountLocked(identifier string) (bool, int) {
 | 
					 | 
				
			||||||
	auth.LoginMutex.RLock()
 | 
					 | 
				
			||||||
	defer auth.LoginMutex.RUnlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Return false if rate limiting is not configured
 | 
					 | 
				
			||||||
	if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
 | 
					 | 
				
			||||||
		return false, 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if the identifier exists in the map
 | 
					 | 
				
			||||||
	attempt, exists := auth.LoginAttempts[identifier]
 | 
					 | 
				
			||||||
	if !exists {
 | 
					 | 
				
			||||||
		return false, 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// If account is locked, check if lock time has expired
 | 
					 | 
				
			||||||
	if attempt.LockedUntil.After(time.Now()) {
 | 
					 | 
				
			||||||
		// Calculate remaining lockout time in seconds
 | 
					 | 
				
			||||||
		remaining := int(time.Until(attempt.LockedUntil).Seconds())
 | 
					 | 
				
			||||||
		return true, remaining
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Lock has expired
 | 
					 | 
				
			||||||
	return false, 0
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// RecordLoginAttempt records a login attempt for rate limiting
 | 
					 | 
				
			||||||
func (auth *Auth) RecordLoginAttempt(identifier string, success bool) {
 | 
					 | 
				
			||||||
	// Skip if rate limiting is not configured
 | 
					 | 
				
			||||||
	if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	auth.LoginMutex.Lock()
 | 
					 | 
				
			||||||
	defer auth.LoginMutex.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get current attempt record or create a new one
 | 
					 | 
				
			||||||
	attempt, exists := auth.LoginAttempts[identifier]
 | 
					 | 
				
			||||||
	if !exists {
 | 
					 | 
				
			||||||
		attempt = &types.LoginAttempt{}
 | 
					 | 
				
			||||||
		auth.LoginAttempts[identifier] = attempt
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Update last attempt time
 | 
					 | 
				
			||||||
	attempt.LastAttempt = time.Now()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// If successful login, reset failed attempts
 | 
					 | 
				
			||||||
	if success {
 | 
					 | 
				
			||||||
		attempt.FailedAttempts = 0
 | 
					 | 
				
			||||||
		attempt.LockedUntil = time.Time{} // Reset lock time
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Increment failed attempts
 | 
					 | 
				
			||||||
	attempt.FailedAttempts++
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// If max retries reached, lock the account
 | 
					 | 
				
			||||||
	if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
 | 
					 | 
				
			||||||
		attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
 | 
					 | 
				
			||||||
		log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
 | 
					func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
 | 
				
			||||||
	// If the whitelist is empty, allow all emails
 | 
						// If the whitelist is empty, allow all emails
 | 
				
			||||||
	if len(auth.Config.OauthWhitelist) == 0 {
 | 
						if len(auth.Config.OAuthWhitelist) == 0 {
 | 
				
			||||||
		return true
 | 
							return true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Loop through the whitelist and return true if the email matches
 | 
						// Loop through the whitelist and return true if the email matches
 | 
				
			||||||
	for _, email := range auth.Config.OauthWhitelist {
 | 
						for _, email := range auth.Config.OAuthWhitelist {
 | 
				
			||||||
		if email == emailSrc {
 | 
							if email == emailSrc {
 | 
				
			||||||
			return true
 | 
								return true
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -152,13 +59,33 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
 | 
				
			|||||||
	return false
 | 
						return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (auth *Auth) GetCookieStore() *sessions.CookieStore {
 | 
				
			||||||
 | 
						// Create a new cookie store
 | 
				
			||||||
 | 
						store := sessions.NewCookieStore([]byte(auth.Config.Secret))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Configure the cookie store
 | 
				
			||||||
 | 
						store.Options = &sessions.Options{
 | 
				
			||||||
 | 
							Path:     "/",
 | 
				
			||||||
 | 
							Domain:   fmt.Sprintf(".%s", auth.Config.Domain),
 | 
				
			||||||
 | 
							Secure:   auth.Config.CookieSecure,
 | 
				
			||||||
 | 
							MaxAge:   auth.Config.SessionExpiry,
 | 
				
			||||||
 | 
							HttpOnly: true,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set the cookie store
 | 
				
			||||||
 | 
						return store
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
 | 
					func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
 | 
				
			||||||
	log.Debug().Msg("Creating session cookie")
 | 
						log.Debug().Msg("Creating session cookie")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get cookie store
 | 
				
			||||||
 | 
						store := auth.GetCookieStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get session
 | 
						// Get session
 | 
				
			||||||
	session, err := auth.GetSession(c)
 | 
						sessions, err := store.Get(c.Request, "tinyauth")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error().Err(err).Msg("Failed to get session")
 | 
					 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -174,16 +101,15 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set data
 | 
						// Set data
 | 
				
			||||||
	session.Values["username"] = data.Username
 | 
						sessions.Values["username"] = data.Username
 | 
				
			||||||
	session.Values["provider"] = data.Provider
 | 
						sessions.Values["provider"] = data.Provider
 | 
				
			||||||
	session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
 | 
						sessions.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
 | 
				
			||||||
	session.Values["totpPending"] = data.TotpPending
 | 
						sessions.Values["totpPending"] = data.TotpPending
 | 
				
			||||||
	session.Values["redirectURI"] = data.RedirectURI
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Save session
 | 
						// Save session
 | 
				
			||||||
	err = session.Save(c.Request, c.Writer)
 | 
						err = sessions.Save(c.Request, c.Writer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error().Err(err).Msg("Failed to save session")
 | 
					 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -194,22 +120,25 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
 | 
				
			|||||||
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
 | 
					func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
 | 
				
			||||||
	log.Debug().Msg("Deleting session cookie")
 | 
						log.Debug().Msg("Deleting session cookie")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get cookie store
 | 
				
			||||||
 | 
						store := auth.GetCookieStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get session
 | 
						// Get session
 | 
				
			||||||
	session, err := auth.GetSession(c)
 | 
						sessions, err := store.Get(c.Request, "tinyauth")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error().Err(err).Msg("Failed to get session")
 | 
					 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Delete all values in the session
 | 
						// Clear session
 | 
				
			||||||
	for key := range session.Values {
 | 
						for key := range sessions.Values {
 | 
				
			||||||
		delete(session.Values, key)
 | 
							delete(sessions.Values, key)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Save session
 | 
						// Save session
 | 
				
			||||||
	err = session.Save(c.Request, c.Writer)
 | 
						err = sessions.Save(c.Request, c.Writer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error().Err(err).Msg("Failed to save session")
 | 
					 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -220,22 +149,31 @@ func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
 | 
				
			|||||||
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
 | 
					func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
 | 
				
			||||||
	log.Debug().Msg("Getting session cookie")
 | 
						log.Debug().Msg("Getting session cookie")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get cookie store
 | 
				
			||||||
 | 
						store := auth.GetCookieStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get session
 | 
						// Get session
 | 
				
			||||||
	session, err := auth.GetSession(c)
 | 
						sessions, err := store.Get(c.Request, "tinyauth")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error().Err(err).Msg("Failed to get session")
 | 
					 | 
				
			||||||
		return types.SessionCookie{}, err
 | 
							return types.SessionCookie{}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get data from session
 | 
						// Get data
 | 
				
			||||||
	username, usernameOk := session.Values["username"].(string)
 | 
						cookieUsername := sessions.Values["username"]
 | 
				
			||||||
	provider, providerOK := session.Values["provider"].(string)
 | 
						cookieProvider := sessions.Values["provider"]
 | 
				
			||||||
	redirectURI, redirectOK := session.Values["redirectURI"].(string)
 | 
						cookieExpiry := sessions.Values["expiry"]
 | 
				
			||||||
	expiry, expiryOk := session.Values["expiry"].(int64)
 | 
						cookieTotpPending := sessions.Values["totpPending"]
 | 
				
			||||||
	totpPending, totpPendingOk := session.Values["totpPending"].(bool)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !usernameOk || !providerOK || !expiryOk || !redirectOK || !totpPendingOk {
 | 
						// Convert interfaces to correct types
 | 
				
			||||||
		log.Warn().Msg("Session cookie is missing data")
 | 
						username, usernameOk := cookieUsername.(string)
 | 
				
			||||||
 | 
						provider, providerOk := cookieProvider.(string)
 | 
				
			||||||
 | 
						expiry, expiryOk := cookieExpiry.(int64)
 | 
				
			||||||
 | 
						totpPending, totpPendingOk := cookieTotpPending.(bool)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if the cookie is invalid
 | 
				
			||||||
 | 
						if !usernameOk || !providerOk || !expiryOk || !totpPendingOk {
 | 
				
			||||||
 | 
							log.Warn().Msg("Session cookie invalid")
 | 
				
			||||||
		return types.SessionCookie{}, nil
 | 
							return types.SessionCookie{}, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -257,7 +195,6 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
 | 
				
			|||||||
		Username:    username,
 | 
							Username:    username,
 | 
				
			||||||
		Provider:    provider,
 | 
							Provider:    provider,
 | 
				
			||||||
		TotpPending: totpPending,
 | 
							TotpPending: totpPending,
 | 
				
			||||||
		RedirectURI: redirectURI,
 | 
					 | 
				
			||||||
	}, nil
 | 
						}, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,147 +0,0 @@
 | 
				
			|||||||
package auth_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
	"tinyauth/internal/auth"
 | 
					 | 
				
			||||||
	"tinyauth/internal/docker"
 | 
					 | 
				
			||||||
	"tinyauth/internal/types"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var config = types.AuthConfig{
 | 
					 | 
				
			||||||
	Users:          types.Users{},
 | 
					 | 
				
			||||||
	OauthWhitelist: []string{},
 | 
					 | 
				
			||||||
	SessionExpiry:  3600,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestLoginRateLimiting(t *testing.T) {
 | 
					 | 
				
			||||||
	// Initialize a new auth service with 3 max retries and 5 seconds timeout
 | 
					 | 
				
			||||||
	config.LoginMaxRetries = 3
 | 
					 | 
				
			||||||
	config.LoginTimeout = 5
 | 
					 | 
				
			||||||
	authService := auth.NewAuth(config, &docker.Docker{})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Test identifier
 | 
					 | 
				
			||||||
	identifier := "test_user"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Test successful login - should not lock account
 | 
					 | 
				
			||||||
	t.Log("Testing successful login")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	authService.RecordLoginAttempt(identifier, true)
 | 
					 | 
				
			||||||
	locked, _ := authService.IsAccountLocked(identifier)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if locked {
 | 
					 | 
				
			||||||
		t.Fatalf("Account should not be locked after successful login")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Test 2 failed attempts - should not lock account yet
 | 
					 | 
				
			||||||
	t.Log("Testing 2 failed login attempts")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	authService.RecordLoginAttempt(identifier, false)
 | 
					 | 
				
			||||||
	authService.RecordLoginAttempt(identifier, false)
 | 
					 | 
				
			||||||
	locked, _ = authService.IsAccountLocked(identifier)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if locked {
 | 
					 | 
				
			||||||
		t.Fatalf("Account should not be locked after only 2 failed attempts")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Add one more failed attempt (total 3) - should lock account with maxRetries=3
 | 
					 | 
				
			||||||
	t.Log("Testing 3 failed login attempts")
 | 
					 | 
				
			||||||
	authService.RecordLoginAttempt(identifier, false)
 | 
					 | 
				
			||||||
	locked, remainingTime := authService.IsAccountLocked(identifier)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !locked {
 | 
					 | 
				
			||||||
		t.Fatalf("Account should be locked after reaching max retries")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if remainingTime <= 0 || remainingTime > 5 {
 | 
					 | 
				
			||||||
		t.Fatalf("Expected remaining time between 1-5 seconds, got %d", remainingTime)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Test reset after waiting for timeout - use 1 second timeout for fast testing
 | 
					 | 
				
			||||||
	t.Log("Testing unlocking after timeout")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Reinitialize auth service with a shorter timeout for testing
 | 
					 | 
				
			||||||
	config.LoginTimeout = 1
 | 
					 | 
				
			||||||
	config.LoginMaxRetries = 3
 | 
					 | 
				
			||||||
	authService = auth.NewAuth(config, &docker.Docker{})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Add enough failed attempts to lock the account
 | 
					 | 
				
			||||||
	for i := 0; i < 3; i++ {
 | 
					 | 
				
			||||||
		authService.RecordLoginAttempt(identifier, false)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Verify it's locked
 | 
					 | 
				
			||||||
	locked, _ = authService.IsAccountLocked(identifier)
 | 
					 | 
				
			||||||
	if !locked {
 | 
					 | 
				
			||||||
		t.Fatalf("Account should be locked initially")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Wait a bit and verify it gets unlocked after timeout
 | 
					 | 
				
			||||||
	time.Sleep(1500 * time.Millisecond) // Wait longer than the timeout
 | 
					 | 
				
			||||||
	locked, _ = authService.IsAccountLocked(identifier)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if locked {
 | 
					 | 
				
			||||||
		t.Fatalf("Account should be unlocked after timeout period")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Test disabled rate limiting
 | 
					 | 
				
			||||||
	t.Log("Testing disabled rate limiting")
 | 
					 | 
				
			||||||
	config.LoginMaxRetries = 0
 | 
					 | 
				
			||||||
	config.LoginTimeout = 0
 | 
					 | 
				
			||||||
	authService = auth.NewAuth(config, &docker.Docker{})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for i := 0; i < 10; i++ {
 | 
					 | 
				
			||||||
		authService.RecordLoginAttempt(identifier, false)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	locked, _ = authService.IsAccountLocked(identifier)
 | 
					 | 
				
			||||||
	if locked {
 | 
					 | 
				
			||||||
		t.Fatalf("Account should not be locked when rate limiting is disabled")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestConcurrentLoginAttempts(t *testing.T) {
 | 
					 | 
				
			||||||
	// Initialize a new auth service with 2 max retries and 5 seconds timeout
 | 
					 | 
				
			||||||
	config.LoginMaxRetries = 2
 | 
					 | 
				
			||||||
	config.LoginTimeout = 5
 | 
					 | 
				
			||||||
	authService := auth.NewAuth(config, &docker.Docker{})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Test multiple identifiers
 | 
					 | 
				
			||||||
	identifiers := []string{"user1", "user2", "user3"}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Test that locking one identifier doesn't affect others
 | 
					 | 
				
			||||||
	t.Log("Testing multiple identifiers")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Add enough failed attempts to lock first user (2 attempts with maxRetries=2)
 | 
					 | 
				
			||||||
	authService.RecordLoginAttempt(identifiers[0], false)
 | 
					 | 
				
			||||||
	authService.RecordLoginAttempt(identifiers[0], false)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if first user is locked
 | 
					 | 
				
			||||||
	locked, _ := authService.IsAccountLocked(identifiers[0])
 | 
					 | 
				
			||||||
	if !locked {
 | 
					 | 
				
			||||||
		t.Fatalf("User1 should be locked after reaching max retries")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check that other users are not affected
 | 
					 | 
				
			||||||
	for i := 1; i < len(identifiers); i++ {
 | 
					 | 
				
			||||||
		locked, _ := authService.IsAccountLocked(identifiers[i])
 | 
					 | 
				
			||||||
		if locked {
 | 
					 | 
				
			||||||
			t.Fatalf("User%d should not be locked", i+1)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Test successful login after failed attempts (but before lock)
 | 
					 | 
				
			||||||
	t.Log("Testing successful login after failed attempts but before lock")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// One failed attempt for user2
 | 
					 | 
				
			||||||
	authService.RecordLoginAttempt(identifiers[1], false)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Successful login should reset the counter
 | 
					 | 
				
			||||||
	authService.RecordLoginAttempt(identifiers[1], true)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Now try a failed login again - should not be locked as counter was reset
 | 
					 | 
				
			||||||
	authService.RecordLoginAttempt(identifiers[1], false)
 | 
					 | 
				
			||||||
	locked, _ = authService.IsAccountLocked(identifiers[1])
 | 
					 | 
				
			||||||
	if locked {
 | 
					 | 
				
			||||||
		t.Fatalf("User2 should not be locked after successful login reset")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -19,20 +19,20 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
 | 
					func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
 | 
				
			||||||
	return &Handlers{
 | 
						return &Handlers{
 | 
				
			||||||
		Config:    config,
 | 
					 | 
				
			||||||
		Auth:      auth,
 | 
							Auth:      auth,
 | 
				
			||||||
		Hooks:     hooks,
 | 
							Hooks:     hooks,
 | 
				
			||||||
		Providers: providers,
 | 
							Providers: providers,
 | 
				
			||||||
		Docker:    docker,
 | 
							Docker:    docker,
 | 
				
			||||||
 | 
							Config:    config,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Handlers struct {
 | 
					type Handlers struct {
 | 
				
			||||||
	Config    types.HandlersConfig
 | 
					 | 
				
			||||||
	Auth      *auth.Auth
 | 
						Auth      *auth.Auth
 | 
				
			||||||
	Hooks     *hooks.Hooks
 | 
						Hooks     *hooks.Hooks
 | 
				
			||||||
	Providers *providers.Providers
 | 
						Providers *providers.Providers
 | 
				
			||||||
	Docker    *docker.Docker
 | 
						Docker    *docker.Docker
 | 
				
			||||||
 | 
						Config    types.HandlersConfig
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
					func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
				
			||||||
@@ -249,34 +249,12 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	log.Debug().Msg("Got login request")
 | 
						log.Debug().Msg("Got login request")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get client IP for rate limiting
 | 
					 | 
				
			||||||
	clientIP := c.ClientIP()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create an identifier for rate limiting (username or IP if username doesn't exist yet)
 | 
					 | 
				
			||||||
	rateIdentifier := login.Username
 | 
					 | 
				
			||||||
	if rateIdentifier == "" {
 | 
					 | 
				
			||||||
		rateIdentifier = clientIP
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if the account is locked due to too many failed attempts
 | 
					 | 
				
			||||||
	locked, remainingTime := h.Auth.IsAccountLocked(rateIdentifier)
 | 
					 | 
				
			||||||
	if locked {
 | 
					 | 
				
			||||||
		log.Warn().Str("identifier", rateIdentifier).Int("remaining_seconds", remainingTime).Msg("Account is locked due to too many failed login attempts")
 | 
					 | 
				
			||||||
		c.JSON(429, gin.H{
 | 
					 | 
				
			||||||
			"status":  429,
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime),
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get user based on username
 | 
						// Get user based on username
 | 
				
			||||||
	user := h.Auth.GetUser(login.Username)
 | 
						user := h.Auth.GetUser(login.Username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// User does not exist
 | 
						// User does not exist
 | 
				
			||||||
	if user == nil {
 | 
						if user == nil {
 | 
				
			||||||
		log.Debug().Str("username", login.Username).Msg("User not found")
 | 
							log.Debug().Str("username", login.Username).Msg("User not found")
 | 
				
			||||||
		// Record failed login attempt
 | 
					 | 
				
			||||||
		h.Auth.RecordLoginAttempt(rateIdentifier, false)
 | 
					 | 
				
			||||||
		c.JSON(401, gin.H{
 | 
							c.JSON(401, gin.H{
 | 
				
			||||||
			"status":  401,
 | 
								"status":  401,
 | 
				
			||||||
			"message": "Unauthorized",
 | 
								"message": "Unauthorized",
 | 
				
			||||||
@@ -289,8 +267,6 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
 | 
				
			|||||||
	// Check if password is correct
 | 
						// Check if password is correct
 | 
				
			||||||
	if !h.Auth.CheckPassword(*user, login.Password) {
 | 
						if !h.Auth.CheckPassword(*user, login.Password) {
 | 
				
			||||||
		log.Debug().Str("username", login.Username).Msg("Password incorrect")
 | 
							log.Debug().Str("username", login.Username).Msg("Password incorrect")
 | 
				
			||||||
		// Record failed login attempt
 | 
					 | 
				
			||||||
		h.Auth.RecordLoginAttempt(rateIdentifier, false)
 | 
					 | 
				
			||||||
		c.JSON(401, gin.H{
 | 
							c.JSON(401, gin.H{
 | 
				
			||||||
			"status":  401,
 | 
								"status":  401,
 | 
				
			||||||
			"message": "Unauthorized",
 | 
								"message": "Unauthorized",
 | 
				
			||||||
@@ -300,9 +276,6 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	log.Debug().Msg("Password correct, checking totp")
 | 
						log.Debug().Msg("Password correct, checking totp")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Record successful login attempt (will reset failed attempt counter)
 | 
					 | 
				
			||||||
	h.Auth.RecordLoginAttempt(rateIdentifier, true)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if user has totp enabled
 | 
						// Check if user has totp enabled
 | 
				
			||||||
	if user.TotpSecret != "" {
 | 
						if user.TotpSecret != "" {
 | 
				
			||||||
		log.Debug().Msg("Totp enabled")
 | 
							log.Debug().Msg("Totp enabled")
 | 
				
			||||||
@@ -420,6 +393,9 @@ func (h *Handlers) LogoutHandler(c *gin.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	log.Debug().Msg("Cleaning up redirect cookie")
 | 
						log.Debug().Msg("Cleaning up redirect cookie")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Clean up redirect cookie if it exists
 | 
				
			||||||
 | 
						c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Return logged out
 | 
						// Return logged out
 | 
				
			||||||
	c.JSON(200, gin.H{
 | 
						c.JSON(200, gin.H{
 | 
				
			||||||
		"status":  200,
 | 
							"status":  200,
 | 
				
			||||||
@@ -526,9 +502,7 @@ func (h *Handlers) OauthUrlHandler(c *gin.Context) {
 | 
				
			|||||||
	// Set redirect cookie if redirect URI is provided
 | 
						// Set redirect cookie if redirect URI is provided
 | 
				
			||||||
	if redirectURI != "" {
 | 
						if redirectURI != "" {
 | 
				
			||||||
		log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
 | 
							log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
 | 
				
			||||||
		h.Auth.CreateSessionCookie(c, &types.SessionCookie{
 | 
							c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", h.Config.Domain, h.Config.CookieSecure, true)
 | 
				
			||||||
			RedirectURI: redirectURI,
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it
 | 
						// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it
 | 
				
			||||||
@@ -650,25 +624,28 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	log.Debug().Msg("Email whitelisted")
 | 
						log.Debug().Msg("Email whitelisted")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get redirect URI
 | 
						// Create session cookie
 | 
				
			||||||
	cookie, err := h.Auth.GetSessionCookie(c)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create session cookie (also cleans up redirect cookie)
 | 
					 | 
				
			||||||
	h.Auth.CreateSessionCookie(c, &types.SessionCookie{
 | 
						h.Auth.CreateSessionCookie(c, &types.SessionCookie{
 | 
				
			||||||
		Username: email,
 | 
							Username: email,
 | 
				
			||||||
		Provider: providerName.Provider,
 | 
							Provider: providerName.Provider,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get redirect URI
 | 
				
			||||||
 | 
						redirectURI, err := c.Cookie("tinyauth_redirect_uri")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// If it is empty it means that no redirect_uri was provided to the login screen so we just log in
 | 
						// If it is empty it means that no redirect_uri was provided to the login screen so we just log in
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
 | 
							c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Str("redirectURI", cookie.RedirectURI).Msg("Got redirect URI")
 | 
						log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Clean up redirect cookie since we already have the value
 | 
				
			||||||
 | 
						c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Build query
 | 
						// Build query
 | 
				
			||||||
	queries, err := query.Values(types.LoginQuery{
 | 
						queries, err := query.Values(types.LoginQuery{
 | 
				
			||||||
		RedirectURI: cookie.RedirectURI,
 | 
							RedirectURI: redirectURI,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Debug().Msg("Got redirect query")
 | 
						log.Debug().Msg("Got redirect query")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,12 @@ type Hooks struct {
 | 
				
			|||||||
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
					func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
				
			||||||
	// Get session cookie and basic auth
 | 
						// Get session cookie and basic auth
 | 
				
			||||||
	cookie, err := hooks.Auth.GetSessionCookie(c)
 | 
						cookie, err := hooks.Auth.GetSessionCookie(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error().Err(err).Msg("Failed to get session cookie")
 | 
				
			||||||
 | 
							return types.UserContext{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	basic := hooks.Auth.GetBasicAuth(c)
 | 
						basic := hooks.Auth.GetBasicAuth(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if basic auth is set
 | 
						// Check if basic auth is set
 | 
				
			||||||
@@ -46,19 +52,6 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check cookie error after basic auth
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Error().Err(err).Msg("Failed to get session cookie")
 | 
					 | 
				
			||||||
		// Return empty context
 | 
					 | 
				
			||||||
		return types.UserContext{
 | 
					 | 
				
			||||||
			Username:    "",
 | 
					 | 
				
			||||||
			IsLoggedIn:  false,
 | 
					 | 
				
			||||||
			OAuth:       false,
 | 
					 | 
				
			||||||
			Provider:    "",
 | 
					 | 
				
			||||||
			TotpPending: false,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if session cookie has totp pending
 | 
						// Check if session cookie has totp pending
 | 
				
			||||||
	if cookie.TotpPending {
 | 
						if cookie.TotpPending {
 | 
				
			||||||
		log.Debug().Msg("Totp pending")
 | 
							log.Debug().Msg("Totp pending")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,10 +14,10 @@ func NewOAuth(config oauth2.Config) *OAuth {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OAuth struct {
 | 
					type OAuth struct {
 | 
				
			||||||
	Config   oauth2.Config
 | 
						Verifier string
 | 
				
			||||||
	Context  context.Context
 | 
						Context  context.Context
 | 
				
			||||||
	Token    *oauth2.Token
 | 
						Token    *oauth2.Token
 | 
				
			||||||
	Verifier string
 | 
						Config   oauth2.Config
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (oauth *OAuth) Init() {
 | 
					func (oauth *OAuth) Init() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,11 +17,11 @@ func NewProviders(config types.OAuthConfig) *Providers {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Providers struct {
 | 
					type Providers struct {
 | 
				
			||||||
	Config    types.OAuthConfig
 | 
					 | 
				
			||||||
	Github    *oauth.OAuth
 | 
						Github    *oauth.OAuth
 | 
				
			||||||
	Google    *oauth.OAuth
 | 
						Google    *oauth.OAuth
 | 
				
			||||||
	Tailscale *oauth.OAuth
 | 
						Tailscale *oauth.OAuth
 | 
				
			||||||
	Generic   *oauth.OAuth
 | 
						Generic   *oauth.OAuth
 | 
				
			||||||
 | 
						Config    types.OAuthConfig
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (providers *Providers) Init() {
 | 
					func (providers *Providers) Init() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,16 +43,6 @@ type UserContextResponse struct {
 | 
				
			|||||||
	TotpPending bool   `json:"totpPending"`
 | 
						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"`
 | 
					 | 
				
			||||||
	ConfiguredProviders []string `json:"configuredProviders"`
 | 
					 | 
				
			||||||
	DisableContinue     bool     `json:"disableContinue"`
 | 
					 | 
				
			||||||
	Title               string   `json:"title"`
 | 
					 | 
				
			||||||
	GenericName         string   `json:"genericName"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Totp request is the request for the totp endpoint
 | 
					// Totp request is the request for the totp endpoint
 | 
				
			||||||
type TotpRequest struct {
 | 
					type TotpRequest struct {
 | 
				
			||||||
	Code string `json:"code"`
 | 
						Code string `json:"code"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,16 +33,12 @@ type Config struct {
 | 
				
			|||||||
	LogLevel                  int8   `mapstructure:"log-level" validate:"min=-1,max=5"`
 | 
						LogLevel                  int8   `mapstructure:"log-level" validate:"min=-1,max=5"`
 | 
				
			||||||
	Title                     string `mapstructure:"app-title"`
 | 
						Title                     string `mapstructure:"app-title"`
 | 
				
			||||||
	EnvFile                   string `mapstructure:"env-file"`
 | 
						EnvFile                   string `mapstructure:"env-file"`
 | 
				
			||||||
	LoginTimeout              int    `mapstructure:"login-timeout"`
 | 
					 | 
				
			||||||
	LoginMaxRetries           int    `mapstructure:"login-max-retries"`
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Server configuration
 | 
					// APIConfig is the configuration for the API
 | 
				
			||||||
type HandlersConfig struct {
 | 
					type APIConfig struct {
 | 
				
			||||||
	AppURL          string
 | 
						Port    int
 | 
				
			||||||
	DisableContinue bool
 | 
						Address string
 | 
				
			||||||
	GenericName     string
 | 
					 | 
				
			||||||
	Title           string
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// OAuthConfig is the configuration for the providers
 | 
					// OAuthConfig is the configuration for the providers
 | 
				
			||||||
@@ -62,20 +58,22 @@ type OAuthConfig struct {
 | 
				
			|||||||
	AppURL                string
 | 
						AppURL                string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// APIConfig is the configuration for the API
 | 
					// Server configuration
 | 
				
			||||||
type APIConfig struct {
 | 
					type HandlersConfig struct {
 | 
				
			||||||
	Port    int
 | 
						AppURL          string
 | 
				
			||||||
	Address string
 | 
						Domain          string
 | 
				
			||||||
 | 
						CookieSecure    bool
 | 
				
			||||||
 | 
						DisableContinue bool
 | 
				
			||||||
 | 
						GenericName     string
 | 
				
			||||||
 | 
						Title           string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// AuthConfig is the configuration for the auth service
 | 
					// Auth configuration
 | 
				
			||||||
type AuthConfig struct {
 | 
					type AuthConfig struct {
 | 
				
			||||||
	Users           Users
 | 
						Domain         string
 | 
				
			||||||
	OauthWhitelist  []string
 | 
						Secret         string
 | 
				
			||||||
	SessionExpiry   int
 | 
						CookieSecure   bool
 | 
				
			||||||
	Secret          string
 | 
						SessionExpiry  int
 | 
				
			||||||
	CookieSecure    bool
 | 
						Users          Users
 | 
				
			||||||
	Domain          string
 | 
						OAuthWhitelist []string
 | 
				
			||||||
	LoginTimeout    int
 | 
					 | 
				
			||||||
	LoginMaxRetries int
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,6 @@
 | 
				
			|||||||
package types
 | 
					package types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import "tinyauth/internal/oauth"
 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
	"tinyauth/internal/oauth"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// User is the struct for a user
 | 
					// User is the struct for a user
 | 
				
			||||||
type User struct {
 | 
					type User struct {
 | 
				
			||||||
@@ -27,7 +24,6 @@ type SessionCookie struct {
 | 
				
			|||||||
	Username    string
 | 
						Username    string
 | 
				
			||||||
	Provider    string
 | 
						Provider    string
 | 
				
			||||||
	TotpPending bool
 | 
						TotpPending bool
 | 
				
			||||||
	RedirectURI string
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TinyauthLabels is the labels for the tinyauth container
 | 
					// TinyauthLabels is the labels for the tinyauth container
 | 
				
			||||||
@@ -47,9 +43,12 @@ type UserContext struct {
 | 
				
			|||||||
	TotpPending bool
 | 
						TotpPending bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoginAttempt tracks information about login attempts for rate limiting
 | 
					// App Context is the response for the app context endpoint
 | 
				
			||||||
type LoginAttempt struct {
 | 
					type AppContext struct {
 | 
				
			||||||
	FailedAttempts int
 | 
						Status              int      `json:"status"`
 | 
				
			||||||
	LastAttempt    time.Time
 | 
						Message             string   `json:"message"`
 | 
				
			||||||
	LockedUntil    time.Time
 | 
						ConfiguredProviders []string `json:"configuredProviders"`
 | 
				
			||||||
 | 
						DisableContinue     bool     `json:"disableContinue"`
 | 
				
			||||||
 | 
						Title               string   `json:"title"`
 | 
				
			||||||
 | 
						GenericName         string   `json:"genericName"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user