mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-28 20:55:42 +00:00
Compare commits
27 Commits
v3.2.0-alp
...
v3.2.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
525f4f3041 | ||
|
|
8a21345706 | ||
|
|
1169c633cc | ||
|
|
2242c9c1e6 | ||
|
|
939919df39 | ||
|
|
a579cf37ce | ||
|
|
2647aa07b4 | ||
|
|
f68c580e11 | ||
|
|
9b39a2b856 | ||
|
|
6d17ce699a | ||
|
|
20dbb35d44 | ||
|
|
36d9dd7354 | ||
|
|
5129f9bff8 | ||
|
|
496a56676d | ||
|
|
57e25524c7 | ||
|
|
614a9b468a | ||
|
|
94a5359080 | ||
|
|
38c5cd7b32 | ||
|
|
c664be5cc5 | ||
|
|
bafcb9a867 | ||
|
|
d322c13791 | ||
|
|
8e84e59c2f | ||
|
|
bd7e160e10 | ||
|
|
df849d5a5c | ||
|
|
5cf4e208c6 | ||
|
|
07ddd4f917 | ||
|
|
98abe514e1 |
@@ -12,9 +12,6 @@ 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
|
||||
@@ -26,5 +23,7 @@ DISABLE_CONTINUE=false
|
||||
OAUTH_WHITELIST=
|
||||
GENERIC_NAME=My OAuth
|
||||
SESSION_EXPIRY=7200
|
||||
LOGIN_TIMEOUT=300
|
||||
LOGIN_MAX_RETRIES=5
|
||||
LOG_LEVEL=0
|
||||
APP_TITLE=Tinyauth SSO
|
||||
105
.github/workflows/release.yml
vendored
105
.github/workflows/release.yml
vendored
@@ -6,7 +6,85 @@ on:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
binary-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.23.2"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
bun install
|
||||
|
||||
- name: Install backend dependencies
|
||||
run: |
|
||||
go mod tidy
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd frontend
|
||||
bun run build
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
CGO_ENABLED=0 go build -ldflags "-s -w" -o tinyauth-amd64
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tinyauth-amd64
|
||||
path: tinyauth-amd64
|
||||
|
||||
binary-build-arm:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.23.2"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
bun install
|
||||
|
||||
- name: Install backend dependencies
|
||||
run: |
|
||||
go mod tidy
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd frontend
|
||||
bun run build
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cp -r frontend/dist internal/assets/dist
|
||||
CGO_ENABLED=0 go build -ldflags "-s -w" -o tinyauth-arm64
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tinyauth-arm64
|
||||
path: tinyauth-arm64
|
||||
|
||||
image-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -51,7 +129,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-arm:
|
||||
image-build-arm:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -96,11 +174,11 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
image-merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
- build-arm
|
||||
- image-build
|
||||
- image-build-arm
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -134,3 +212,20 @@ jobs:
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
||||
|
||||
update-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- binary-build
|
||||
- binary-build-arm
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: tinyauth-*
|
||||
path: binaries
|
||||
merge-multiple: true
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: binaries/*
|
||||
|
||||
58
.github/workflows/translations.yml
vendored
58
.github/workflows/translations.yml
vendored
@@ -3,7 +3,7 @@ name: Publish translations
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- i18n_v*
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -16,7 +16,53 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
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:
|
||||
needs: get-translations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -25,10 +71,14 @@ jobs:
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Move translations
|
||||
- name: Prepare output directory
|
||||
run: |
|
||||
mkdir -p dist
|
||||
mv frontend/src/lib/i18n/locales dist/i18n
|
||||
mkdir -p dist/i18n/
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist/i18n/
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
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. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
|
||||
|
||||

|
||||
|
||||
> [!WARNING]
|
||||
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
||||
|
||||
@@ -61,3 +63,7 @@ Credits for the logo of this app go to:
|
||||
|
||||
- **Freepik** for providing the police hat and badge.
|
||||
- **Renee French** for the original gopher logo.
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#steveiliop56/tinyauth&Date)
|
||||
|
||||
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
64
cmd/root.go
64
cmd/root.go
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -43,7 +42,6 @@ var rootCmd = &cobra.Command{
|
||||
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
|
||||
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
|
||||
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
|
||||
config.TailscaleClientSecret = utils.GetSecret(config.TailscaleClientSecret, config.TailscaleClientSecretFile)
|
||||
|
||||
// Validate config
|
||||
validator := validator.New()
|
||||
@@ -78,26 +76,22 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
// Create OAuth config
|
||||
oauthConfig := types.OAuthConfig{
|
||||
GithubClientId: config.GithubClientId,
|
||||
GithubClientSecret: config.GithubClientSecret,
|
||||
GoogleClientId: config.GoogleClientId,
|
||||
GoogleClientSecret: config.GoogleClientSecret,
|
||||
TailscaleClientId: config.TailscaleClientId,
|
||||
TailscaleClientSecret: config.TailscaleClientSecret,
|
||||
GenericClientId: config.GenericClientId,
|
||||
GenericClientSecret: config.GenericClientSecret,
|
||||
GenericScopes: strings.Split(config.GenericScopes, ","),
|
||||
GenericAuthURL: config.GenericAuthURL,
|
||||
GenericTokenURL: config.GenericTokenURL,
|
||||
GenericUserURL: config.GenericUserURL,
|
||||
AppURL: config.AppURL,
|
||||
GithubClientId: config.GithubClientId,
|
||||
GithubClientSecret: config.GithubClientSecret,
|
||||
GoogleClientId: config.GoogleClientId,
|
||||
GoogleClientSecret: config.GoogleClientSecret,
|
||||
GenericClientId: config.GenericClientId,
|
||||
GenericClientSecret: config.GenericClientSecret,
|
||||
GenericScopes: strings.Split(config.GenericScopes, ","),
|
||||
GenericAuthURL: config.GenericAuthURL,
|
||||
GenericTokenURL: config.GenericTokenURL,
|
||||
GenericUserURL: config.GenericUserURL,
|
||||
AppURL: config.AppURL,
|
||||
}
|
||||
|
||||
// Create handlers config
|
||||
serverConfig := types.HandlersConfig{
|
||||
handlersConfig := types.HandlersConfig{
|
||||
AppURL: config.AppURL,
|
||||
Domain: fmt.Sprintf(".%s", domain),
|
||||
CookieSecure: config.CookieSecure,
|
||||
DisableContinue: config.DisableContinue,
|
||||
Title: config.Title,
|
||||
GenericName: config.GenericName,
|
||||
@@ -105,12 +99,20 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
// Create api config
|
||||
apiConfig := types.APIConfig{
|
||||
Port: config.Port,
|
||||
Address: config.Address,
|
||||
Secret: config.Secret,
|
||||
CookieSecure: config.CookieSecure,
|
||||
SessionExpiry: config.SessionExpiry,
|
||||
Domain: domain,
|
||||
Port: config.Port,
|
||||
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
|
||||
@@ -121,7 +123,7 @@ var rootCmd = &cobra.Command{
|
||||
HandleError(err, "Failed to initialize docker")
|
||||
|
||||
// Create auth service
|
||||
auth := auth.NewAuth(docker, users, oauthWhitelist, config.SessionExpiry)
|
||||
auth := auth.NewAuth(authConfig, docker)
|
||||
|
||||
// Create OAuth providers service
|
||||
providers := providers.NewProviders(oauthConfig)
|
||||
@@ -133,7 +135,7 @@ var rootCmd = &cobra.Command{
|
||||
hooks := hooks.NewHooks(auth, providers)
|
||||
|
||||
// Create handlers
|
||||
handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker)
|
||||
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
||||
|
||||
// Create API
|
||||
api := api.NewAPI(apiConfig, handlers)
|
||||
@@ -184,9 +186,6 @@ func init() {
|
||||
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
|
||||
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
|
||||
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
|
||||
rootCmd.Flags().String("tailscale-client-id", "", "Tailscale OAuth client ID.")
|
||||
rootCmd.Flags().String("tailscale-client-secret", "", "Tailscale OAuth client secret.")
|
||||
rootCmd.Flags().String("tailscale-client-secret-file", "", "Tailscale OAuth client secret file.")
|
||||
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
|
||||
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
|
||||
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
|
||||
@@ -198,6 +197,8 @@ func init() {
|
||||
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().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().String("app-title", "Tinyauth", "Title of the app.")
|
||||
|
||||
@@ -216,9 +217,6 @@ func init() {
|
||||
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
|
||||
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
|
||||
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
|
||||
viper.BindEnv("tailscale-client-id", "TAILSCALE_CLIENT_ID")
|
||||
viper.BindEnv("tailscale-client-secret", "TAILSCALE_CLIENT_SECRET")
|
||||
viper.BindEnv("tailscale-client-secret-file", "TAILSCALE_CLIENT_SECRET_FILE")
|
||||
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
|
||||
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
|
||||
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
|
||||
@@ -232,6 +230,8 @@ func init() {
|
||||
viper.BindEnv("session-expiry", "SESSION_EXPIRY")
|
||||
viper.BindEnv("log-level", "LOG_LEVEL")
|
||||
viper.BindEnv("app-title", "APP_TITLE")
|
||||
viper.BindEnv("login-timeout", "LOGIN_TIMEOUT")
|
||||
viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES")
|
||||
|
||||
// Bind flags to viper
|
||||
viper.BindPFlags(rootCmd.Flags())
|
||||
|
||||
@@ -25,7 +25,7 @@ export const LoginForm = (props: LoginFormProps) => {
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
label={t("loginUsername")}
|
||||
placeholder="user@example.com"
|
||||
placeholder="username"
|
||||
required
|
||||
disabled={isLoading}
|
||||
key={form.key("username")}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Grid, Button } from "@mantine/core";
|
||||
import { GithubIcon } from "../../icons/github";
|
||||
import { GoogleIcon } from "../../icons/google";
|
||||
import { OAuthIcon } from "../../icons/oauth";
|
||||
import { TailscaleIcon } from "../../icons/tailscale";
|
||||
|
||||
interface OAuthButtonsProps {
|
||||
oauthProviders: string[];
|
||||
@@ -41,19 +40,6 @@ export const OAuthButtons = (props: OAuthButtonsProps) => {
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
)}
|
||||
{oauthProviders.includes("tailscale") && (
|
||||
<Grid.Col span="content">
|
||||
<Button
|
||||
radius="xl"
|
||||
leftSection={<TailscaleIcon style={{ width: 14, height: 14 }} />}
|
||||
variant="default"
|
||||
onClick={() => mutate("tailscale")}
|
||||
loading={isLoading}
|
||||
>
|
||||
Tailscale
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
)}
|
||||
{oauthProviders.includes("generic") && (
|
||||
<Grid.Col span="content">
|
||||
<Button
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
width={24}
|
||||
height={24}
|
||||
{...props}
|
||||
>
|
||||
<style>{".st0{opacity:0.2;fill:#CCCAC9;}.st1{fill:#FFFFFF;}"}</style>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
className="st0"
|
||||
d="M65.6,127.7c35.3,0,63.9-28.6,63.9-63.9S100.9,0,65.6,0S1.8,28.6,1.8,63.9S30.4,127.7,65.6,127.7z"
|
||||
/>
|
||||
<path
|
||||
className="st1"
|
||||
d="M65.6,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,219,1.8,254.2S30.4,318.1,65.6,318.1z"
|
||||
/>
|
||||
<path
|
||||
className="st0"
|
||||
d="M65.6,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,412.9,1.8,448.1S30.4,512,65.6,512z"
|
||||
/>
|
||||
<path
|
||||
className="st1"
|
||||
d="M257.2,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,318.1,257.2,318.1z"
|
||||
/>
|
||||
<path
|
||||
className="st1"
|
||||
d="M257.2,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,512,257.2,512z"
|
||||
/>
|
||||
<path
|
||||
className="st0"
|
||||
d="M257.2,127.7c35.3,0,63.9-28.6,63.9-63.9S292.5,0,257.2,0s-63.9,28.6-63.9,63.9S221.9,127.7,257.2,127.7z"
|
||||
/>
|
||||
<path
|
||||
className="st0"
|
||||
d="M446.4,127.7c35.3,0,63.9-28.6,63.9-63.9S481.6,0,446.4,0c-35.3,0-63.9,28.6-63.9,63.9S411.1,127.7,446.4,127.7z"
|
||||
/>
|
||||
<path
|
||||
className="st1"
|
||||
d="M446.4,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,318.1,446.4,318.1z"
|
||||
/>
|
||||
<path
|
||||
className="st0"
|
||||
d="M446.4,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,512,446.4,512z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ i18n
|
||||
],
|
||||
backendOptions: [
|
||||
{
|
||||
loadPath: "https://cdn.tinyauth.app/i18n/{{lng}}.json",
|
||||
loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"loginTitle": "Welcome back, login with",
|
||||
"loginDivider": "Or continue with password",
|
||||
"loginUsername": "Username",
|
||||
"loginPassword": "Password",
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginTitle": "Willkommen zurück, logge dich ein mit",
|
||||
"loginDivider": "Oder mit Passwort fortfahren",
|
||||
"loginUsername": "Benutzername",
|
||||
"loginPassword": "Passwort",
|
||||
"loginSubmit": "Anmelden",
|
||||
"loginFailTitle": "Login fehlgeschlagen",
|
||||
"loginFailSubtitle": "Bitte überprüfe deinen Benutzernamen und Passwort",
|
||||
"loginFailRateLimit": "Sie konnten sich zu oft nicht einloggen, bitte versuchen Sie es später erneut",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Είσοδος",
|
||||
"loginFailTitle": "Αποτυχία σύνδεσης",
|
||||
"loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης",
|
||||
"loginFailRateLimit": "Αποτύχατε να συνδεθείτε πάρα πολλές φορές, παρακαλώ προσπαθήστε ξανά αργότερα",
|
||||
"loginSuccessTitle": "Συνδεδεμένος",
|
||||
"loginSuccessSubtitle": "Καλώς ήρθατε!",
|
||||
"loginOauthFailTitle": "Εσωτερικό σφάλμα",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας",
|
||||
"totpTitle": "Εισάγετε τον κωδικό TOTP",
|
||||
"unauthorizedTitle": "Μη εξουσιοδοτημένο",
|
||||
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να έχει πρόσβαση στον πόρο <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να συνδεθεί.",
|
||||
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.",
|
||||
"unauthorizedButton": "Προσπαθήστε ξανά"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Se connecter",
|
||||
"loginFailTitle": "Échec de la connexion",
|
||||
"loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe",
|
||||
"loginFailRateLimit": "Vous n'avez pas pu vous connecter trop de fois, veuillez réessayer plus tard",
|
||||
"loginSuccessTitle": "Connecté",
|
||||
"loginSuccessSubtitle": "Bienvenue!",
|
||||
"loginOauthFailTitle": "Erreur interne",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirection vers votre application",
|
||||
"totpTitle": "Saisissez votre code TOTP",
|
||||
"unauthorizedTitle": "Non autorisé",
|
||||
"unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à se connecter.",
|
||||
"unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à se connecter.",
|
||||
"unauthorizedButton": "Réessayer"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"loginTitle": "Welcome back, login with",
|
||||
"loginDivider": "Or continue with password",
|
||||
"loginUsername": "Username",
|
||||
"loginPassword": "Password",
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginTitle": "Welkom terug, log in met",
|
||||
"loginDivider": "Of ga door met wachtwoord",
|
||||
"loginUsername": "Gebruikersnaam",
|
||||
"loginPassword": "Wachtwoord",
|
||||
"loginSubmit": "Log in",
|
||||
"loginFailTitle": "Mislukt om in te loggen",
|
||||
"loginFailSubtitle": "Gelieve uw gebruikersnaam en wachtwoord te controleren",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
"loginDivider": "Lub kontynuuj z hasłem",
|
||||
"loginUsername": "Nazwa użytkownika",
|
||||
"loginPassword": "Hasło",
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginSubmit": "Zaloguj się",
|
||||
"loginFailTitle": "Nie udało się zalogować",
|
||||
"loginFailSubtitle": "Sprawdź swoją nazwę użytkownika i hasło",
|
||||
"loginFailRateLimit": "Nie udało się zalogować zbyt wiele razy, spróbuj ponownie później",
|
||||
"loginSuccessTitle": "Zalogowano",
|
||||
"loginSuccessSubtitle": "Witaj ponownie!",
|
||||
"loginOauthFailTitle": "Wewnętrzny błąd",
|
||||
@@ -21,9 +22,9 @@
|
||||
"continueTitle": "Kontynuuj",
|
||||
"continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.",
|
||||
"internalErrorTitle": "Wewnętrzny błąd serwera",
|
||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
||||
"internalErrorSubtitle": "Wystąpił błąd na serwerze i obecnie nie można obsłużyć tego żądania.",
|
||||
"internalErrorButton": "Spróbuj ponownie",
|
||||
"logoutFailTitle": "Failed to log out",
|
||||
"logoutFailTitle": "Nie udało się wylogować",
|
||||
"logoutFailSubtitle": "Spróbuj ponownie",
|
||||
"logoutSuccessTitle": "Wylogowano",
|
||||
"logoutSuccessSubtitle": "Zostałeś wylogowany",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Przekierowywanie do aplikacji",
|
||||
"totpTitle": "Wprowadź kod TOTP",
|
||||
"unauthorizedTitle": "Nieautoryzowany",
|
||||
"unauthorizedResourceSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do logowania.",
|
||||
"unauthorizedResourceSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do logowania.",
|
||||
"unauthorizedButton": "Spróbuj ponownie"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"loginSubmit": "Login",
|
||||
"loginFailTitle": "Failed to log in",
|
||||
"loginFailSubtitle": "Please check your username and password",
|
||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||
"loginSuccessTitle": "Logged in",
|
||||
"loginSuccessSubtitle": "Welcome back!",
|
||||
"loginOauthFailTitle": "Internal error",
|
||||
@@ -39,7 +40,7 @@
|
||||
"totpSuccessSubtitle": "Redirecting to your app",
|
||||
"totpTitle": "Enter your TOTP code",
|
||||
"unauthorizedTitle": "Unauthorized",
|
||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||
"unauthorizedButton": "Try again"
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const queryClient = new QueryClient({
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<MantineProvider forceColorScheme="dark">
|
||||
<MantineProvider defaultColorScheme="auto">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Notifications />
|
||||
<AppContextProvider>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Paper, Title, Text, Divider } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import axios, { type AxiosError } from "axios";
|
||||
import { useUserContext } from "../context/user-context";
|
||||
import { Navigate } from "react-router";
|
||||
import { Layout } from "../components/layouts/layout";
|
||||
@@ -33,7 +33,17 @@ export const LoginPage = () => {
|
||||
mutationFn: (login: LoginFormValues) => {
|
||||
return axios.post("/api/login", login);
|
||||
},
|
||||
onError: () => {
|
||||
onError: (data: AxiosError) => {
|
||||
if (data.response) {
|
||||
if (data.response.status === 429) {
|
||||
notifications.show({
|
||||
title: t("loginFailTitle"),
|
||||
message: t("loginFailRateLimit"),
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
notifications.show({
|
||||
title: t("loginFailTitle"),
|
||||
message: t("loginFailSubtitle"),
|
||||
|
||||
@@ -37,6 +37,7 @@ export const UnauthorizedPage = () => {
|
||||
<Trans
|
||||
i18nKey="unauthorizedLoginSubtitle"
|
||||
t={t}
|
||||
components={{ Code: <Code /> }}
|
||||
values={{ username }}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
4
go.mod
4
go.mod
@@ -3,7 +3,6 @@ module tinyauth
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sessions v1.0.2
|
||||
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
|
||||
@@ -58,9 +57,8 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -65,8 +65,6 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
|
||||
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
@@ -99,8 +97,6 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"tinyauth/internal/handlers"
|
||||
"tinyauth/internal/types"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -51,21 +49,6 @@ func (api *API) Init() {
|
||||
log.Debug().Msg("Setting up file server")
|
||||
fileServer := http.FileServer(http.FS(dist))
|
||||
|
||||
// Setup cookie store
|
||||
log.Debug().Msg("Setting up cookie store")
|
||||
store := cookie.NewStore([]byte(api.Config.Secret))
|
||||
|
||||
// Use session middleware
|
||||
store.Options(sessions.Options{
|
||||
Domain: api.Config.Domain,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: api.Config.CookieSecure,
|
||||
MaxAge: api.Config.SessionExpiry,
|
||||
})
|
||||
|
||||
router.Use(sessions.Sessions("tinyauth", store))
|
||||
|
||||
// UI middleware
|
||||
router.Use(func(c *gin.Context) {
|
||||
// If not an API request, serve the UI
|
||||
|
||||
@@ -21,23 +21,29 @@ import (
|
||||
|
||||
// Simple API config for tests
|
||||
var apiConfig = types.APIConfig{
|
||||
Port: 8080,
|
||||
Address: "0.0.0.0",
|
||||
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
||||
CookieSecure: false,
|
||||
SessionExpiry: 3600,
|
||||
Port: 8080,
|
||||
Address: "0.0.0.0",
|
||||
}
|
||||
|
||||
// Simple handlers config for tests
|
||||
var handlersConfig = types.HandlersConfig{
|
||||
AppURL: "http://localhost:8080",
|
||||
Domain: ".localhost",
|
||||
CookieSecure: false,
|
||||
DisableContinue: false,
|
||||
Title: "Tinyauth",
|
||||
GenericName: "Generic",
|
||||
}
|
||||
|
||||
// Simple auth config for tests
|
||||
var authConfig = types.AuthConfig{
|
||||
Users: types.Users{},
|
||||
OauthWhitelist: []string{},
|
||||
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
||||
CookieSecure: false,
|
||||
SessionExpiry: 3600,
|
||||
LoginTimeout: 0,
|
||||
LoginMaxRetries: 0,
|
||||
}
|
||||
|
||||
// Cookie
|
||||
var cookie string
|
||||
|
||||
@@ -61,12 +67,13 @@ func getAPI(t *testing.T) *api.API {
|
||||
}
|
||||
|
||||
// Create auth service
|
||||
auth := auth.NewAuth(docker, types.Users{
|
||||
authConfig.Users = types.Users{
|
||||
{
|
||||
Username: user.Username,
|
||||
Password: user.Password,
|
||||
},
|
||||
}, nil, apiConfig.SessionExpiry)
|
||||
}
|
||||
auth := auth.NewAuth(authConfig, docker)
|
||||
|
||||
// Create providers service
|
||||
providers := providers.NewProviders(types.OAuthConfig{})
|
||||
|
||||
@@ -1 +1 @@
|
||||
v3.2.0
|
||||
v3.2.1
|
||||
@@ -1,38 +1,64 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"tinyauth/internal/docker"
|
||||
"tinyauth/internal/types"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func NewAuth(docker *docker.Docker, userList types.Users, oauthWhitelist []string, sessionExpiry int) *Auth {
|
||||
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
|
||||
return &Auth{
|
||||
Docker: docker,
|
||||
Users: userList,
|
||||
OAuthWhitelist: oauthWhitelist,
|
||||
SessionExpiry: sessionExpiry,
|
||||
Config: config,
|
||||
Docker: docker,
|
||||
LoginAttempts: make(map[string]*types.LoginAttempt),
|
||||
}
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Users types.Users
|
||||
Docker *docker.Docker
|
||||
OAuthWhitelist []string
|
||||
SessionExpiry int
|
||||
Config types.AuthConfig
|
||||
Docker *docker.Docker
|
||||
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 {
|
||||
// Loop through users and return the user if the username matches
|
||||
for _, user := range auth.Users {
|
||||
for _, user := range auth.Config.Users {
|
||||
if user.Username == username {
|
||||
return &user
|
||||
}
|
||||
@@ -45,14 +71,78 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool {
|
||||
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 {
|
||||
// If the whitelist is empty, allow all emails
|
||||
if len(auth.OAuthWhitelist) == 0 {
|
||||
if len(auth.Config.OauthWhitelist) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Loop through the whitelist and return true if the email matches
|
||||
for _, email := range auth.OAuthWhitelist {
|
||||
for _, email := range auth.Config.OauthWhitelist {
|
||||
if email == emailSrc {
|
||||
return true
|
||||
}
|
||||
@@ -62,11 +152,15 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) {
|
||||
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
|
||||
log.Debug().Msg("Creating session cookie")
|
||||
|
||||
// Get session
|
||||
sessions := sessions.Default(c)
|
||||
session, err := auth.GetSession(c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get session")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug().Msg("Setting session cookie")
|
||||
|
||||
@@ -76,54 +170,73 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
|
||||
if data.TotpPending {
|
||||
sessionExpiry = 3600
|
||||
} else {
|
||||
sessionExpiry = auth.SessionExpiry
|
||||
sessionExpiry = auth.Config.SessionExpiry
|
||||
}
|
||||
|
||||
// Set data
|
||||
sessions.Set("username", data.Username)
|
||||
sessions.Set("provider", data.Provider)
|
||||
sessions.Set("expiry", time.Now().Add(time.Duration(sessionExpiry)*time.Second).Unix())
|
||||
sessions.Set("totpPending", data.TotpPending)
|
||||
session.Values["username"] = data.Username
|
||||
session.Values["provider"] = data.Provider
|
||||
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
|
||||
session.Values["totpPending"] = data.TotpPending
|
||||
session.Values["redirectURI"] = data.RedirectURI
|
||||
|
||||
// Save session
|
||||
sessions.Save()
|
||||
err = session.Save(c.Request, c.Writer)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save session")
|
||||
return err
|
||||
}
|
||||
|
||||
// Return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Auth) DeleteSessionCookie(c *gin.Context) {
|
||||
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
||||
log.Debug().Msg("Deleting session cookie")
|
||||
|
||||
// Get session
|
||||
sessions := sessions.Default(c)
|
||||
session, err := auth.GetSession(c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get session")
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear session
|
||||
sessions.Clear()
|
||||
// Delete all values in the session
|
||||
for key := range session.Values {
|
||||
delete(session.Values, key)
|
||||
}
|
||||
|
||||
// Save session
|
||||
sessions.Save()
|
||||
err = session.Save(c.Request, c.Writer)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save session")
|
||||
return err
|
||||
}
|
||||
|
||||
// Return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie {
|
||||
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
||||
log.Debug().Msg("Getting session cookie")
|
||||
|
||||
// Get session
|
||||
sessions := sessions.Default(c)
|
||||
session, err := auth.GetSession(c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get session")
|
||||
return types.SessionCookie{}, err
|
||||
}
|
||||
|
||||
// Get data
|
||||
cookieUsername := sessions.Get("username")
|
||||
cookieProvider := sessions.Get("provider")
|
||||
cookieExpiry := sessions.Get("expiry")
|
||||
cookieTotpPending := sessions.Get("totpPending")
|
||||
// Get data from session
|
||||
username, usernameOk := session.Values["username"].(string)
|
||||
provider, providerOK := session.Values["provider"].(string)
|
||||
redirectURI, redirectOK := session.Values["redirectURI"].(string)
|
||||
expiry, expiryOk := session.Values["expiry"].(int64)
|
||||
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
|
||||
|
||||
// Convert interfaces to correct types
|
||||
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{}
|
||||
if !usernameOk || !providerOK || !expiryOk || !redirectOK || !totpPendingOk {
|
||||
log.Warn().Msg("Session cookie is missing data")
|
||||
return types.SessionCookie{}, nil
|
||||
}
|
||||
|
||||
// Check if the cookie has expired
|
||||
@@ -134,7 +247,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie {
|
||||
auth.DeleteSessionCookie(c)
|
||||
|
||||
// Return empty cookie
|
||||
return types.SessionCookie{}
|
||||
return types.SessionCookie{}, nil
|
||||
}
|
||||
|
||||
log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Msg("Parsed cookie")
|
||||
@@ -144,12 +257,13 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie {
|
||||
Username: username,
|
||||
Provider: provider,
|
||||
TotpPending: totpPending,
|
||||
}
|
||||
RedirectURI: redirectURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (auth *Auth) UserAuthConfigured() bool {
|
||||
// If there are users, return true
|
||||
return len(auth.Users) > 0
|
||||
return len(auth.Config.Users) > 0
|
||||
}
|
||||
|
||||
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) {
|
||||
@@ -184,6 +298,8 @@ func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bo
|
||||
if slices.Contains(labels.Users, context.Username) {
|
||||
return true, nil
|
||||
}
|
||||
} else {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Not allowed
|
||||
|
||||
147
internal/auth/auth_test.go
Normal file
147
internal/auth/auth_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"strings"
|
||||
"tinyauth/internal/auth"
|
||||
@@ -249,12 +248,34 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
||||
|
||||
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
|
||||
user := h.Auth.GetUser(login.Username)
|
||||
|
||||
// User does not exist
|
||||
if user == nil {
|
||||
log.Debug().Str("username", login.Username).Msg("User not found")
|
||||
// Record failed login attempt
|
||||
h.Auth.RecordLoginAttempt(rateIdentifier, false)
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -267,6 +288,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
||||
// Check if password is correct
|
||||
if !h.Auth.CheckPassword(*user, login.Password) {
|
||||
log.Debug().Str("username", login.Username).Msg("Password incorrect")
|
||||
// Record failed login attempt
|
||||
h.Auth.RecordLoginAttempt(rateIdentifier, false)
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -276,6 +299,9 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
||||
|
||||
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
|
||||
if user.TotpSecret != "" {
|
||||
log.Debug().Msg("Totp enabled")
|
||||
@@ -393,9 +419,6 @@ func (h *Handlers) LogoutHandler(c *gin.Context) {
|
||||
|
||||
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
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
@@ -502,33 +525,9 @@ func (h *Handlers) OauthUrlHandler(c *gin.Context) {
|
||||
// Set redirect cookie if redirect URI is provided
|
||||
if redirectURI != "" {
|
||||
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
|
||||
c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", h.Config.Domain, h.Config.CookieSecure, true)
|
||||
}
|
||||
|
||||
// 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
|
||||
if request.Provider == "tailscale" {
|
||||
// Build tailscale query
|
||||
queries, err := query.Values(types.TailscaleQuery{
|
||||
Code: (1000 + rand.IntN(9000)),
|
||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||
RedirectURI: redirectURI,
|
||||
})
|
||||
|
||||
// Handle error
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to build queries")
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Return tailscale URL (immidiately redirects to the callback)
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "OK",
|
||||
"url": fmt.Sprintf("%s/api/oauth/callback/tailscale?%s", h.Config.AppURL, queries.Encode()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Return auth URL
|
||||
@@ -624,28 +623,25 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
|
||||
|
||||
log.Debug().Msg("Email whitelisted")
|
||||
|
||||
// Create session cookie
|
||||
// Get redirect URI
|
||||
cookie, err := h.Auth.GetSessionCookie(c)
|
||||
|
||||
// Create session cookie (also cleans up redirect cookie)
|
||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||
Username: email,
|
||||
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 err != nil {
|
||||
c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
|
||||
}
|
||||
|
||||
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)
|
||||
log.Debug().Str("redirectURI", cookie.RedirectURI).Msg("Got redirect URI")
|
||||
|
||||
// Build query
|
||||
queries, err := query.Values(types.LoginQuery{
|
||||
RedirectURI: redirectURI,
|
||||
RedirectURI: cookie.RedirectURI,
|
||||
})
|
||||
|
||||
log.Debug().Msg("Got redirect query")
|
||||
|
||||
@@ -23,7 +23,7 @@ type Hooks struct {
|
||||
|
||||
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
||||
// Get session cookie and basic auth
|
||||
cookie := hooks.Auth.GetSessionCookie(c)
|
||||
cookie, err := hooks.Auth.GetSessionCookie(c)
|
||||
basic := hooks.Auth.GetBasicAuth(c)
|
||||
|
||||
// Check if basic auth is set
|
||||
@@ -46,6 +46,19 @@ 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
|
||||
if cookie.TotpPending {
|
||||
log.Debug().Msg("Totp pending")
|
||||
|
||||
@@ -17,11 +17,10 @@ func NewProviders(config types.OAuthConfig) *Providers {
|
||||
}
|
||||
|
||||
type Providers struct {
|
||||
Config types.OAuthConfig
|
||||
Github *oauth.OAuth
|
||||
Google *oauth.OAuth
|
||||
Tailscale *oauth.OAuth
|
||||
Generic *oauth.OAuth
|
||||
Config types.OAuthConfig
|
||||
Github *oauth.OAuth
|
||||
Google *oauth.OAuth
|
||||
Generic *oauth.OAuth
|
||||
}
|
||||
|
||||
func (providers *Providers) Init() {
|
||||
@@ -59,22 +58,6 @@ func (providers *Providers) Init() {
|
||||
providers.Google.Init()
|
||||
}
|
||||
|
||||
if providers.Config.TailscaleClientId != "" && providers.Config.TailscaleClientSecret != "" {
|
||||
log.Info().Msg("Initializing Tailscale OAuth")
|
||||
|
||||
// Create a new oauth provider with the tailscale config
|
||||
providers.Tailscale = oauth.NewOAuth(oauth2.Config{
|
||||
ClientID: providers.Config.TailscaleClientId,
|
||||
ClientSecret: providers.Config.TailscaleClientSecret,
|
||||
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/tailscale", providers.Config.AppURL),
|
||||
Scopes: TailscaleScopes(),
|
||||
Endpoint: TailscaleEndpoint,
|
||||
})
|
||||
|
||||
// Initialize the oauth provider
|
||||
providers.Tailscale.Init()
|
||||
}
|
||||
|
||||
// If we have a client id and secret for generic oauth, initialize the oauth provider
|
||||
if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
|
||||
log.Info().Msg("Initializing Generic OAuth")
|
||||
@@ -103,8 +86,6 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
|
||||
return providers.Github
|
||||
case "google":
|
||||
return providers.Google
|
||||
case "tailscale":
|
||||
return providers.Tailscale
|
||||
case "generic":
|
||||
return providers.Generic
|
||||
default:
|
||||
@@ -161,30 +142,6 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
||||
|
||||
log.Debug().Msg("Got email from google")
|
||||
|
||||
// Return the email
|
||||
return email, nil
|
||||
case "tailscale":
|
||||
// If the tailscale provider is not configured, return an error
|
||||
if providers.Tailscale == nil {
|
||||
log.Debug().Msg("Tailscale provider not configured")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Get the client from the tailscale provider
|
||||
client := providers.Tailscale.GetClient()
|
||||
|
||||
log.Debug().Msg("Got client from tailscale")
|
||||
|
||||
// Get the email from the tailscale provider
|
||||
email, err := GetTailscaleEmail(client)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debug().Msg("Got email from tailscale")
|
||||
|
||||
// Return the email
|
||||
return email, nil
|
||||
case "generic":
|
||||
@@ -225,9 +182,6 @@ func (provider *Providers) GetConfiguredProviders() []string {
|
||||
if provider.Google != nil {
|
||||
providers = append(providers, "google")
|
||||
}
|
||||
if provider.Tailscale != nil {
|
||||
providers = append(providers, "tailscale")
|
||||
}
|
||||
if provider.Generic != nil {
|
||||
providers = append(providers, "generic")
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// The tailscale email is the loginName
|
||||
type TailscaleUser struct {
|
||||
LoginName string `json:"loginName"`
|
||||
}
|
||||
|
||||
// The response from the tailscale user info endpoint
|
||||
type TailscaleUserInfoResponse struct {
|
||||
Users []TailscaleUser `json:"users"`
|
||||
}
|
||||
|
||||
// The scopes required for the tailscale provider
|
||||
func TailscaleScopes() []string {
|
||||
return []string{"users:read"}
|
||||
}
|
||||
|
||||
// The tailscale endpoint
|
||||
var TailscaleEndpoint = oauth2.Endpoint{
|
||||
TokenURL: "https://api.tailscale.com/api/v2/oauth/token",
|
||||
}
|
||||
|
||||
func GetTailscaleEmail(client *http.Client) (string, error) {
|
||||
// Get the user info from tailscale using the oauth http client
|
||||
res, err := client.Get("https://api.tailscale.com/api/v2/tailnet/-/users")
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debug().Msg("Got response from tailscale")
|
||||
|
||||
// Read the body of the response
|
||||
body, err := io.ReadAll(res.Body)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debug().Msg("Read body from tailscale")
|
||||
|
||||
// Parse the body into a user struct
|
||||
var users TailscaleUserInfoResponse
|
||||
|
||||
// Unmarshal the body into the user struct
|
||||
err = json.Unmarshal(body, &users)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debug().Msg("Parsed users from tailscale")
|
||||
|
||||
// Return the email of the first user
|
||||
return users.Users[0].LoginName, nil
|
||||
}
|
||||
54
internal/types/api.go
Normal file
54
internal/types/api.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package types
|
||||
|
||||
// LoginQuery is the query parameters for the login endpoint
|
||||
type LoginQuery struct {
|
||||
RedirectURI string `url:"redirect_uri"`
|
||||
}
|
||||
|
||||
// LoginRequest is the request body for the login endpoint
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// OAuthRequest is the request for the OAuth endpoint
|
||||
type OAuthRequest struct {
|
||||
Provider string `uri:"provider" binding:"required"`
|
||||
}
|
||||
|
||||
// UnauthorizedQuery is the query parameters for the unauthorized endpoint
|
||||
type UnauthorizedQuery struct {
|
||||
Username string `url:"username"`
|
||||
Resource string `url:"resource"`
|
||||
}
|
||||
|
||||
// Proxy is the uri parameters for the proxy endpoint
|
||||
type Proxy struct {
|
||||
Proxy string `uri:"proxy" binding:"required"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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
|
||||
type TotpRequest struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
76
internal/types/config.go
Normal file
76
internal/types/config.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package types
|
||||
|
||||
// Config is the configuration for the tinyauth server
|
||||
type Config struct {
|
||||
Port int `mapstructure:"port" validate:"required"`
|
||||
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
||||
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
||||
SecretFile string `mapstructure:"secret-file"`
|
||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||
Users string `mapstructure:"users"`
|
||||
UsersFile string `mapstructure:"users-file"`
|
||||
CookieSecure bool `mapstructure:"cookie-secure"`
|
||||
GithubClientId string `mapstructure:"github-client-id"`
|
||||
GithubClientSecret string `mapstructure:"github-client-secret"`
|
||||
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
|
||||
GoogleClientId string `mapstructure:"google-client-id"`
|
||||
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
||||
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
|
||||
GenericClientId string `mapstructure:"generic-client-id"`
|
||||
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
||||
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
|
||||
GenericScopes string `mapstructure:"generic-scopes"`
|
||||
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
||||
GenericTokenURL string `mapstructure:"generic-token-url"`
|
||||
GenericUserURL string `mapstructure:"generic-user-url"`
|
||||
GenericName string `mapstructure:"generic-name"`
|
||||
DisableContinue bool `mapstructure:"disable-continue"`
|
||||
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
||||
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"`
|
||||
LoginTimeout int `mapstructure:"login-timeout"`
|
||||
LoginMaxRetries int `mapstructure:"login-max-retries"`
|
||||
}
|
||||
|
||||
// Server configuration
|
||||
type HandlersConfig struct {
|
||||
AppURL string
|
||||
DisableContinue bool
|
||||
GenericName string
|
||||
Title string
|
||||
}
|
||||
|
||||
// OAuthConfig is the configuration for the providers
|
||||
type OAuthConfig struct {
|
||||
GithubClientId string
|
||||
GithubClientSecret string
|
||||
GoogleClientId string
|
||||
GoogleClientSecret string
|
||||
GenericClientId string
|
||||
GenericClientSecret string
|
||||
GenericScopes []string
|
||||
GenericAuthURL string
|
||||
GenericTokenURL string
|
||||
GenericUserURL string
|
||||
AppURL string
|
||||
}
|
||||
|
||||
// APIConfig is the configuration for the API
|
||||
type APIConfig struct {
|
||||
Port int
|
||||
Address string
|
||||
}
|
||||
|
||||
// AuthConfig is the configuration for the auth service
|
||||
type AuthConfig struct {
|
||||
Users Users
|
||||
OauthWhitelist []string
|
||||
SessionExpiry int
|
||||
Secret string
|
||||
CookieSecure bool
|
||||
Domain string
|
||||
LoginTimeout int
|
||||
LoginMaxRetries int
|
||||
}
|
||||
@@ -1,17 +1,9 @@
|
||||
package types
|
||||
|
||||
import "tinyauth/internal/oauth"
|
||||
|
||||
// LoginQuery is the query parameters for the login endpoint
|
||||
type LoginQuery struct {
|
||||
RedirectURI string `url:"redirect_uri"`
|
||||
}
|
||||
|
||||
// LoginRequest is the request body for the login endpoint
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
import (
|
||||
"time"
|
||||
"tinyauth/internal/oauth"
|
||||
)
|
||||
|
||||
// User is the struct for a user
|
||||
type User struct {
|
||||
@@ -23,39 +15,27 @@ type User struct {
|
||||
// Users is a list of users
|
||||
type Users []User
|
||||
|
||||
// Config is the configuration for the tinyauth server
|
||||
type Config struct {
|
||||
Port int `mapstructure:"port" validate:"required"`
|
||||
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
||||
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
||||
SecretFile string `mapstructure:"secret-file"`
|
||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||
Users string `mapstructure:"users"`
|
||||
UsersFile string `mapstructure:"users-file"`
|
||||
CookieSecure bool `mapstructure:"cookie-secure"`
|
||||
GithubClientId string `mapstructure:"github-client-id"`
|
||||
GithubClientSecret string `mapstructure:"github-client-secret"`
|
||||
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
|
||||
GoogleClientId string `mapstructure:"google-client-id"`
|
||||
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
||||
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
|
||||
TailscaleClientId string `mapstructure:"tailscale-client-id"`
|
||||
TailscaleClientSecret string `mapstructure:"tailscale-client-secret"`
|
||||
TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"`
|
||||
GenericClientId string `mapstructure:"generic-client-id"`
|
||||
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
||||
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
|
||||
GenericScopes string `mapstructure:"generic-scopes"`
|
||||
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
||||
GenericTokenURL string `mapstructure:"generic-token-url"`
|
||||
GenericUserURL string `mapstructure:"generic-user-url"`
|
||||
GenericName string `mapstructure:"generic-name"`
|
||||
DisableContinue bool `mapstructure:"disable-continue"`
|
||||
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
||||
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"`
|
||||
// OAuthProviders is the struct for the OAuth providers
|
||||
type OAuthProviders struct {
|
||||
Github *oauth.OAuth
|
||||
Google *oauth.OAuth
|
||||
Microsoft *oauth.OAuth
|
||||
}
|
||||
|
||||
// SessionCookie is the cookie for the session (exculding the expiry)
|
||||
type SessionCookie struct {
|
||||
Username string
|
||||
Provider string
|
||||
TotpPending bool
|
||||
RedirectURI string
|
||||
}
|
||||
|
||||
// TinyauthLabels is the labels for the tinyauth container
|
||||
type TinyauthLabels struct {
|
||||
OAuthWhitelist []string
|
||||
Users []string
|
||||
Allowed string
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// UserContext is the context for the user
|
||||
@@ -67,108 +47,9 @@ type UserContext struct {
|
||||
TotpPending bool
|
||||
}
|
||||
|
||||
// APIConfig is the configuration for the API
|
||||
type APIConfig struct {
|
||||
Port int
|
||||
Address string
|
||||
Secret string
|
||||
CookieSecure bool
|
||||
SessionExpiry int
|
||||
Domain string
|
||||
}
|
||||
|
||||
// OAuthConfig is the configuration for the providers
|
||||
type OAuthConfig struct {
|
||||
GithubClientId string
|
||||
GithubClientSecret string
|
||||
GoogleClientId string
|
||||
GoogleClientSecret string
|
||||
TailscaleClientId string
|
||||
TailscaleClientSecret string
|
||||
GenericClientId string
|
||||
GenericClientSecret string
|
||||
GenericScopes []string
|
||||
GenericAuthURL string
|
||||
GenericTokenURL string
|
||||
GenericUserURL string
|
||||
AppURL string
|
||||
}
|
||||
|
||||
// OAuthRequest is the request for the OAuth endpoint
|
||||
type OAuthRequest struct {
|
||||
Provider string `uri:"provider" binding:"required"`
|
||||
}
|
||||
|
||||
// OAuthProviders is the struct for the OAuth providers
|
||||
type OAuthProviders struct {
|
||||
Github *oauth.OAuth
|
||||
Google *oauth.OAuth
|
||||
Microsoft *oauth.OAuth
|
||||
}
|
||||
|
||||
// UnauthorizedQuery is the query parameters for the unauthorized endpoint
|
||||
type UnauthorizedQuery struct {
|
||||
Username string `url:"username"`
|
||||
Resource string `url:"resource"`
|
||||
}
|
||||
|
||||
// SessionCookie is the cookie for the session (exculding the expiry)
|
||||
type SessionCookie struct {
|
||||
Username string
|
||||
Provider string
|
||||
TotpPending bool
|
||||
}
|
||||
|
||||
// TinyauthLabels is the labels for the tinyauth container
|
||||
type TinyauthLabels struct {
|
||||
OAuthWhitelist []string
|
||||
Users []string
|
||||
Allowed string
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// TailscaleQuery is the query parameters for the tailscale endpoint
|
||||
type TailscaleQuery struct {
|
||||
Code int `url:"code"`
|
||||
}
|
||||
|
||||
// Proxy is the uri parameters for the proxy endpoint
|
||||
type Proxy struct {
|
||||
Proxy string `uri:"proxy" binding:"required"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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
|
||||
type TotpRequest struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// Server configuration
|
||||
type HandlersConfig struct {
|
||||
AppURL string
|
||||
Domain string
|
||||
CookieSecure bool
|
||||
DisableContinue bool
|
||||
GenericName string
|
||||
Title string
|
||||
// LoginAttempt tracks information about login attempts for rate limiting
|
||||
type LoginAttempt struct {
|
||||
FailedAttempts int
|
||||
LastAttempt time.Time
|
||||
LockedUntil time.Time
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func GetSecret(conf string, file string) string {
|
||||
}
|
||||
|
||||
// Return the contents of the file
|
||||
return contents
|
||||
return ParseSecretFile(contents)
|
||||
}
|
||||
|
||||
// Get the users from the config or file
|
||||
@@ -213,7 +213,7 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
|
||||
|
||||
// Check if any of the OAuth providers are configured based on the client id and secret
|
||||
func OAuthConfigured(config types.Config) bool {
|
||||
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "") || (config.TailscaleClientId != "" && config.TailscaleClientSecret != "")
|
||||
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "")
|
||||
}
|
||||
|
||||
// Filter helper function
|
||||
@@ -241,23 +241,21 @@ func ParseUser(user string) (types.User, error) {
|
||||
return types.User{}, errors.New("invalid user format")
|
||||
}
|
||||
|
||||
// Check if the user has a totp secret
|
||||
if len(userSplit) == 2 {
|
||||
// Check for empty username or password
|
||||
if userSplit[1] == "" || userSplit[0] == "" {
|
||||
// Check for empty strings
|
||||
for _, userPart := range userSplit {
|
||||
if strings.TrimSpace(userPart) == "" {
|
||||
return types.User{}, errors.New("invalid user format")
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user has a totp secret
|
||||
if len(userSplit) == 2 {
|
||||
return types.User{
|
||||
Username: userSplit[0],
|
||||
Password: userSplit[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check for empty username, password or totp secret
|
||||
if userSplit[2] == "" || userSplit[1] == "" || userSplit[0] == "" {
|
||||
return types.User{}, errors.New("invalid user format")
|
||||
}
|
||||
|
||||
// Return the user struct
|
||||
return types.User{
|
||||
Username: userSplit[0],
|
||||
@@ -265,3 +263,23 @@ func ParseUser(user string) (types.User, error) {
|
||||
TotpSecret: userSplit[2],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Parse secret file
|
||||
func ParseSecretFile(contents string) string {
|
||||
// Split to lines
|
||||
lines := strings.Split(contents, "\n")
|
||||
|
||||
// Loop through the lines
|
||||
for _, line := range lines {
|
||||
// Check if the line is empty
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Return the line
|
||||
return strings.TrimSpace(line)
|
||||
}
|
||||
|
||||
// Return an empty string
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -123,7 +124,7 @@ func TestGetSecret(t *testing.T) {
|
||||
expected := "test"
|
||||
|
||||
// Create file
|
||||
err := os.WriteFile(file, []byte(expected), 0644)
|
||||
err := os.WriteFile(file, []byte(fmt.Sprintf("\n\n \n\n\n %s \n\n \n ", expected)), 0644)
|
||||
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user