mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-11-03 15:45:51 +00:00
Compare commits
23 Commits
feat/multi
...
v3.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
939919df39 | ||
|
|
a579cf37ce | ||
|
|
2647aa07b4 | ||
|
|
f68c580e11 | ||
|
|
9b39a2b856 | ||
|
|
6d17ce699a | ||
|
|
20dbb35d44 | ||
|
|
36d9dd7354 | ||
|
|
5129f9bff8 | ||
|
|
496a56676d | ||
|
|
57e25524c7 | ||
|
|
614a9b468a | ||
|
|
94a5359080 | ||
|
|
38c5cd7b32 | ||
|
|
c664be5cc5 | ||
|
|
bafcb9a867 | ||
|
|
d322c13791 | ||
|
|
8e84e59c2f | ||
|
|
bd7e160e10 | ||
|
|
df849d5a5c | ||
|
|
5cf4e208c6 | ||
|
|
07ddd4f917 | ||
|
|
98abe514e1 |
@@ -26,5 +26,7 @@ DISABLE_CONTINUE=false
|
|||||||
OAUTH_WHITELIST=
|
OAUTH_WHITELIST=
|
||||||
GENERIC_NAME=My OAuth
|
GENERIC_NAME=My OAuth
|
||||||
SESSION_EXPIRY=7200
|
SESSION_EXPIRY=7200
|
||||||
|
LOGIN_TIMEOUT=300
|
||||||
|
LOGIN_MAX_RETRIES=5
|
||||||
LOG_LEVEL=0
|
LOG_LEVEL=0
|
||||||
APP_TITLE=Tinyauth SSO
|
APP_TITLE=Tinyauth SSO
|
||||||
105
.github/workflows/release.yml
vendored
105
.github/workflows/release.yml
vendored
@@ -6,7 +6,85 @@ on:
|
|||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
jobs:
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -51,7 +129,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
build-arm:
|
image-build-arm:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -96,11 +174,11 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
merge:
|
image-merge:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build
|
- image-build
|
||||||
- build-arm
|
- image-build-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@@ -134,3 +212,20 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
$(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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- i18n_v*
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -16,7 +16,53 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
get-branches:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
i18n-branches: ${{ steps.get-branches.outputs.result }}
|
||||||
|
steps:
|
||||||
|
- name: Get branches
|
||||||
|
id: get-branches
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: repos } = await github.rest.repos.listBranches({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
})
|
||||||
|
|
||||||
|
const i18nBranches = repos.filter((branch) => branch.name.startsWith("i18n_v"))
|
||||||
|
const i18nBranchNames = i18nBranches.map((branch) => branch.name)
|
||||||
|
|
||||||
|
return i18nBranchNames
|
||||||
|
|
||||||
|
get-translations:
|
||||||
|
needs: get-branches
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
branch: ${{ fromJson(needs.get-branches.outputs.i18n-branches) }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.branch }}
|
||||||
|
|
||||||
|
- name: Get translation version
|
||||||
|
id: get-version
|
||||||
|
run: |
|
||||||
|
branch=${{ matrix.branch }}
|
||||||
|
version=${branch#i18n_}
|
||||||
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload translations
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ steps.get-version.outputs.version }}
|
||||||
|
path: frontend/src/lib/i18n/locales
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
needs: get-translations
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -25,10 +71,14 @@ jobs:
|
|||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v4
|
uses: actions/configure-pages@v4
|
||||||
|
|
||||||
- name: Move translations
|
- name: Prepare output directory
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist/i18n/
|
||||||
mv frontend/src/lib/i18n/locales dist/i18n
|
|
||||||
|
- name: Download translations
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist/i18n/
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
|||||||
@@ -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.
|
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]
|
> [!WARNING]
|
||||||
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
> 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.
|
- **Freepik** for providing the police hat and badge.
|
||||||
- **Renee French** for the original gopher logo.
|
- **Renee French** for the original gopher logo.
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#steveiliop56/tinyauth&Date)
|
||||||
|
|||||||
BIN
assets/login.png
Normal file
BIN
assets/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
32
cmd/root.go
32
cmd/root.go
@@ -2,7 +2,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -94,10 +93,8 @@ var rootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create handlers config
|
// Create handlers config
|
||||||
serverConfig := types.HandlersConfig{
|
handlersConfig := types.HandlersConfig{
|
||||||
AppURL: config.AppURL,
|
AppURL: config.AppURL,
|
||||||
Domain: fmt.Sprintf(".%s", domain),
|
|
||||||
CookieSecure: config.CookieSecure,
|
|
||||||
DisableContinue: config.DisableContinue,
|
DisableContinue: config.DisableContinue,
|
||||||
Title: config.Title,
|
Title: config.Title,
|
||||||
GenericName: config.GenericName,
|
GenericName: config.GenericName,
|
||||||
@@ -109,6 +106,18 @@ var rootCmd = &cobra.Command{
|
|||||||
Address: config.Address,
|
Address: config.Address,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create auth config
|
||||||
|
authConfig := types.AuthConfig{
|
||||||
|
Users: users,
|
||||||
|
OauthWhitelist: oauthWhitelist,
|
||||||
|
Secret: config.Secret,
|
||||||
|
CookieSecure: config.CookieSecure,
|
||||||
|
SessionExpiry: config.SessionExpiry,
|
||||||
|
Domain: domain,
|
||||||
|
LoginTimeout: config.LoginTimeout,
|
||||||
|
LoginMaxRetries: config.LoginMaxRetries,
|
||||||
|
}
|
||||||
|
|
||||||
// Create docker service
|
// Create docker service
|
||||||
docker := docker.NewDocker()
|
docker := docker.NewDocker()
|
||||||
|
|
||||||
@@ -117,14 +126,7 @@ var rootCmd = &cobra.Command{
|
|||||||
HandleError(err, "Failed to initialize docker")
|
HandleError(err, "Failed to initialize docker")
|
||||||
|
|
||||||
// Create auth service
|
// Create auth service
|
||||||
auth := auth.NewAuth(types.AuthConfig{
|
auth := auth.NewAuth(authConfig, docker)
|
||||||
Domain: domain,
|
|
||||||
Secret: config.Secret,
|
|
||||||
SessionExpiry: config.SessionExpiry,
|
|
||||||
CookieSecure: config.CookieSecure,
|
|
||||||
Users: users,
|
|
||||||
OAuthWhitelist: oauthWhitelist,
|
|
||||||
}, docker)
|
|
||||||
|
|
||||||
// Create OAuth providers service
|
// Create OAuth providers service
|
||||||
providers := providers.NewProviders(oauthConfig)
|
providers := providers.NewProviders(oauthConfig)
|
||||||
@@ -136,7 +138,7 @@ var rootCmd = &cobra.Command{
|
|||||||
hooks := hooks.NewHooks(auth, providers)
|
hooks := hooks.NewHooks(auth, providers)
|
||||||
|
|
||||||
// Create handlers
|
// Create handlers
|
||||||
handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker)
|
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
||||||
|
|
||||||
// Create API
|
// Create API
|
||||||
api := api.NewAPI(apiConfig, handlers)
|
api := api.NewAPI(apiConfig, handlers)
|
||||||
@@ -201,6 +203,8 @@ func init() {
|
|||||||
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
||||||
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
||||||
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
|
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
|
||||||
|
rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).")
|
||||||
|
rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).")
|
||||||
rootCmd.Flags().Int("log-level", 1, "Log level.")
|
rootCmd.Flags().Int("log-level", 1, "Log level.")
|
||||||
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
|
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
|
||||||
|
|
||||||
@@ -235,6 +239,8 @@ func init() {
|
|||||||
viper.BindEnv("session-expiry", "SESSION_EXPIRY")
|
viper.BindEnv("session-expiry", "SESSION_EXPIRY")
|
||||||
viper.BindEnv("log-level", "LOG_LEVEL")
|
viper.BindEnv("log-level", "LOG_LEVEL")
|
||||||
viper.BindEnv("app-title", "APP_TITLE")
|
viper.BindEnv("app-title", "APP_TITLE")
|
||||||
|
viper.BindEnv("login-timeout", "LOGIN_TIMEOUT")
|
||||||
|
viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES")
|
||||||
|
|
||||||
// Bind flags to viper
|
// Bind flags to viper
|
||||||
viper.BindPFlags(rootCmd.Flags())
|
viper.BindPFlags(rootCmd.Flags())
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const LoginForm = (props: LoginFormProps) => {
|
|||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t("loginUsername")}
|
label={t("loginUsername")}
|
||||||
placeholder="user@example.com"
|
placeholder="username"
|
||||||
required
|
required
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
key={form.key("username")}
|
key={form.key("username")}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { useColorScheme } from "@mantine/hooks";
|
||||||
import type { SVGProps } from "react";
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
|
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -9,47 +11,17 @@ export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
height={24}
|
height={24}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<style>{".st0{opacity:0.2;fill:#CCCAC9;}.st1{fill:#FFFFFF;}"}</style>
|
{colorScheme === "dark" ? (
|
||||||
<g>
|
<>
|
||||||
<g>
|
<path xmlns="http://www.w3.org/2000/svg" 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.2s28.6 63.9 63.8 63.9m191.6 0c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m0 193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m189.2-193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9" fill="#ffffff"/>
|
||||||
<path
|
<path xmlns="http://www.w3.org/2000/svg" d="M65.6 127.7c35.3 0 63.9-28.6 63.9-63.9S100.9 0 65.6 0 1.8 28.6 1.8 63.9s28.6 63.8 63.8 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.8 28.7-63.8 63.9S30.4 512 65.6 512m191.6-384.3c35.3 0 63.9-28.6 63.9-63.9S292.5 0 257.2 0s-63.9 28.6-63.9 63.9 28.6 63.8 63.9 63.8m189.2 0c35.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.9s28.6 63.8 63.9 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9" fill="#CCCAC9" opacity="0.2"/>
|
||||||
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
|
<path xmlns="http://www.w3.org/2000/svg" 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.2s28.6 63.9 63.8 63.9m191.6 0c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m0 193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m189.2-193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9"/>
|
||||||
className="st1"
|
<path xmlns="http://www.w3.org/2000/svg" d="M65.6 127.7c35.3 0 63.9-28.6 63.9-63.9S100.9 0 65.6 0 1.8 28.6 1.8 63.9s28.6 63.8 63.8 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.8 28.7-63.8 63.9S30.4 512 65.6 512m191.6-384.3c35.3 0 63.9-28.6 63.9-63.9S292.5 0 257.2 0s-63.9 28.6-63.9 63.9 28.6 63.8 63.9 63.8m189.2 0c35.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.9s28.6 63.8 63.9 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9" opacity=".2"/>
|
||||||
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>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ i18n
|
|||||||
],
|
],
|
||||||
backendOptions: [
|
backendOptions: [
|
||||||
{
|
{
|
||||||
loadPath: "https://cdn.tinyauth.app/i18n/{{lng}}.json",
|
loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Willkommen zurück, logge dich ein mit",
|
||||||
"loginDivider": "Or continue with password",
|
"loginDivider": "Oder mit Passwort fortfahren",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Benutzername",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Passwort",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Anmelden",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Login fehlgeschlagen",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"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",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Είσοδος",
|
"loginSubmit": "Είσοδος",
|
||||||
"loginFailTitle": "Αποτυχία σύνδεσης",
|
"loginFailTitle": "Αποτυχία σύνδεσης",
|
||||||
"loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης",
|
"loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης",
|
||||||
|
"loginFailRateLimit": "Αποτύχατε να συνδεθείτε πάρα πολλές φορές, παρακαλώ προσπαθήστε ξανά αργότερα",
|
||||||
"loginSuccessTitle": "Συνδεδεμένος",
|
"loginSuccessTitle": "Συνδεδεμένος",
|
||||||
"loginSuccessSubtitle": "Καλώς ήρθατε!",
|
"loginSuccessSubtitle": "Καλώς ήρθατε!",
|
||||||
"loginOauthFailTitle": "Εσωτερικό σφάλμα",
|
"loginOauthFailTitle": "Εσωτερικό σφάλμα",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας",
|
"totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας",
|
||||||
"totpTitle": "Εισάγετε τον κωδικό TOTP",
|
"totpTitle": "Εισάγετε τον κωδικό TOTP",
|
||||||
"unauthorizedTitle": "Μη εξουσιοδοτημένο",
|
"unauthorizedTitle": "Μη εξουσιοδοτημένο",
|
||||||
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να έχει πρόσβαση στον πόρο <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να συνδεθεί.",
|
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.",
|
||||||
"unauthorizedButton": "Προσπαθήστε ξανά"
|
"unauthorizedButton": "Προσπαθήστε ξανά"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Se connecter",
|
"loginSubmit": "Se connecter",
|
||||||
"loginFailTitle": "Échec de la connexion",
|
"loginFailTitle": "Échec de la connexion",
|
||||||
"loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe",
|
"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é",
|
"loginSuccessTitle": "Connecté",
|
||||||
"loginSuccessSubtitle": "Bienvenue!",
|
"loginSuccessSubtitle": "Bienvenue!",
|
||||||
"loginOauthFailTitle": "Erreur interne",
|
"loginOauthFailTitle": "Erreur interne",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirection vers votre application",
|
"totpSuccessSubtitle": "Redirection vers votre application",
|
||||||
"totpTitle": "Saisissez votre code TOTP",
|
"totpTitle": "Saisissez votre code TOTP",
|
||||||
"unauthorizedTitle": "Non autorisé",
|
"unauthorizedTitle": "Non autorisé",
|
||||||
"unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.",
|
"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 {{username}} n'est pas autorisé à se connecter.",
|
"unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à se connecter.",
|
||||||
"unauthorizedButton": "Réessayer"
|
"unauthorizedButton": "Réessayer"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"loginTitle": "Welcome back, login with",
|
"loginTitle": "Welkom terug, log in met",
|
||||||
"loginDivider": "Or continue with password",
|
"loginDivider": "Of ga door met wachtwoord",
|
||||||
"loginUsername": "Username",
|
"loginUsername": "Gebruikersnaam",
|
||||||
"loginPassword": "Password",
|
"loginPassword": "Wachtwoord",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Log in",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Mislukt om in te loggen",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Gelieve uw gebruikersnaam en wachtwoord te controleren",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
"loginDivider": "Lub kontynuuj z hasłem",
|
"loginDivider": "Lub kontynuuj z hasłem",
|
||||||
"loginUsername": "Nazwa użytkownika",
|
"loginUsername": "Nazwa użytkownika",
|
||||||
"loginPassword": "Hasło",
|
"loginPassword": "Hasło",
|
||||||
"loginSubmit": "Login",
|
"loginSubmit": "Zaloguj się",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Nie udało się zalogować",
|
||||||
"loginFailSubtitle": "Sprawdź swoją nazwę użytkownika i hasło",
|
"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",
|
"loginSuccessTitle": "Zalogowano",
|
||||||
"loginSuccessSubtitle": "Witaj ponownie!",
|
"loginSuccessSubtitle": "Witaj ponownie!",
|
||||||
"loginOauthFailTitle": "Wewnętrzny błąd",
|
"loginOauthFailTitle": "Wewnętrzny błąd",
|
||||||
@@ -21,9 +22,9 @@
|
|||||||
"continueTitle": "Kontynuuj",
|
"continueTitle": "Kontynuuj",
|
||||||
"continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.",
|
"continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.",
|
||||||
"internalErrorTitle": "Wewnętrzny błąd serwera",
|
"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",
|
"internalErrorButton": "Spróbuj ponownie",
|
||||||
"logoutFailTitle": "Failed to log out",
|
"logoutFailTitle": "Nie udało się wylogować",
|
||||||
"logoutFailSubtitle": "Spróbuj ponownie",
|
"logoutFailSubtitle": "Spróbuj ponownie",
|
||||||
"logoutSuccessTitle": "Wylogowano",
|
"logoutSuccessTitle": "Wylogowano",
|
||||||
"logoutSuccessSubtitle": "Zostałeś wylogowany",
|
"logoutSuccessSubtitle": "Zostałeś wylogowany",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Przekierowywanie do aplikacji",
|
"totpSuccessSubtitle": "Przekierowywanie do aplikacji",
|
||||||
"totpTitle": "Wprowadź kod TOTP",
|
"totpTitle": "Wprowadź kod TOTP",
|
||||||
"unauthorizedTitle": "Nieautoryzowany",
|
"unauthorizedTitle": "Nieautoryzowany",
|
||||||
"unauthorizedResourceSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
|
"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 {{username}} nie jest upoważniony do logowania.",
|
"unaothorizedLoginSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do logowania.",
|
||||||
"unauthorizedButton": "Spróbuj ponownie"
|
"unauthorizedButton": "Spróbuj ponownie"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"loginSubmit": "Login",
|
"loginSubmit": "Login",
|
||||||
"loginFailTitle": "Failed to log in",
|
"loginFailTitle": "Failed to log in",
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
"loginFailSubtitle": "Please check your username and password",
|
||||||
|
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
||||||
"loginSuccessTitle": "Logged in",
|
"loginSuccessTitle": "Logged in",
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
"loginSuccessSubtitle": "Welcome back!",
|
||||||
"loginOauthFailTitle": "Internal error",
|
"loginOauthFailTitle": "Internal error",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
"totpSuccessSubtitle": "Redirecting to your app",
|
||||||
"totpTitle": "Enter your TOTP code",
|
"totpTitle": "Enter your TOTP code",
|
||||||
"unauthorizedTitle": "Unauthorized",
|
"unauthorizedTitle": "Unauthorized",
|
||||||
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
|
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
||||||
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
|
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
||||||
"unauthorizedButton": "Try again"
|
"unauthorizedButton": "Try again"
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<MantineProvider forceColorScheme="dark">
|
<MantineProvider defaultColorScheme="auto">
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<AppContextProvider>
|
<AppContextProvider>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Paper, Title, Text, Divider } from "@mantine/core";
|
import { Paper, Title, Text, Divider } from "@mantine/core";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios, { type AxiosError } from "axios";
|
||||||
import { useUserContext } from "../context/user-context";
|
import { useUserContext } from "../context/user-context";
|
||||||
import { Navigate } from "react-router";
|
import { Navigate } from "react-router";
|
||||||
import { Layout } from "../components/layouts/layout";
|
import { Layout } from "../components/layouts/layout";
|
||||||
@@ -33,7 +33,17 @@ export const LoginPage = () => {
|
|||||||
mutationFn: (login: LoginFormValues) => {
|
mutationFn: (login: LoginFormValues) => {
|
||||||
return axios.post("/api/login", login);
|
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({
|
notifications.show({
|
||||||
title: t("loginFailTitle"),
|
title: t("loginFailTitle"),
|
||||||
message: t("loginFailSubtitle"),
|
message: t("loginFailSubtitle"),
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const UnauthorizedPage = () => {
|
|||||||
<Trans
|
<Trans
|
||||||
i18nKey="unauthorizedLoginSubtitle"
|
i18nKey="unauthorizedLoginSubtitle"
|
||||||
t={t}
|
t={t}
|
||||||
|
components={{ Code: <Code /> }}
|
||||||
values={{ username }}
|
values={{ username }}
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ import (
|
|||||||
|
|
||||||
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
|
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
|
||||||
return &API{
|
return &API{
|
||||||
Handlers: handlers,
|
|
||||||
Config: config,
|
Config: config,
|
||||||
|
Handlers: handlers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
|
Config types.APIConfig
|
||||||
Router *gin.Engine
|
Router *gin.Engine
|
||||||
Handlers *handlers.Handlers
|
Handlers *handlers.Handlers
|
||||||
Config types.APIConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) Init() {
|
func (api *API) Init() {
|
||||||
|
|||||||
@@ -19,12 +19,6 @@ import (
|
|||||||
"github.com/magiconair/properties/assert"
|
"github.com/magiconair/properties/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User
|
|
||||||
var User = types.User{
|
|
||||||
Username: "user",
|
|
||||||
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple API config for tests
|
// Simple API config for tests
|
||||||
var apiConfig = types.APIConfig{
|
var apiConfig = types.APIConfig{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
@@ -34,8 +28,6 @@ var apiConfig = types.APIConfig{
|
|||||||
// Simple handlers config for tests
|
// Simple handlers config for tests
|
||||||
var handlersConfig = types.HandlersConfig{
|
var handlersConfig = types.HandlersConfig{
|
||||||
AppURL: "http://localhost:8080",
|
AppURL: "http://localhost:8080",
|
||||||
Domain: ".localhost",
|
|
||||||
CookieSecure: false,
|
|
||||||
DisableContinue: false,
|
DisableContinue: false,
|
||||||
Title: "Tinyauth",
|
Title: "Tinyauth",
|
||||||
GenericName: "Generic",
|
GenericName: "Generic",
|
||||||
@@ -43,19 +35,24 @@ var handlersConfig = types.HandlersConfig{
|
|||||||
|
|
||||||
// Simple auth config for tests
|
// Simple auth config for tests
|
||||||
var authConfig = types.AuthConfig{
|
var authConfig = types.AuthConfig{
|
||||||
Domain: "localhost",
|
Users: types.Users{},
|
||||||
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
OauthWhitelist: []string{},
|
||||||
CookieSecure: false,
|
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
||||||
SessionExpiry: 3600,
|
CookieSecure: false,
|
||||||
Users: types.Users{
|
SessionExpiry: 3600,
|
||||||
User,
|
LoginTimeout: 0,
|
||||||
},
|
LoginMaxRetries: 0,
|
||||||
OAuthWhitelist: []string{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookie
|
// Cookie
|
||||||
var cookie string
|
var cookie string
|
||||||
|
|
||||||
|
// User
|
||||||
|
var user = types.User{
|
||||||
|
Username: "user",
|
||||||
|
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
|
||||||
|
}
|
||||||
|
|
||||||
// We need all this to be able to test the API
|
// We need all this to be able to test the API
|
||||||
func getAPI(t *testing.T) *api.API {
|
func getAPI(t *testing.T) *api.API {
|
||||||
// Create docker service
|
// Create docker service
|
||||||
@@ -70,6 +67,12 @@ func getAPI(t *testing.T) *api.API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create auth service
|
// Create auth service
|
||||||
|
authConfig.Users = types.Users{
|
||||||
|
{
|
||||||
|
Username: user.Username,
|
||||||
|
Password: user.Password,
|
||||||
|
},
|
||||||
|
}
|
||||||
auth := auth.NewAuth(authConfig, docker)
|
auth := auth.NewAuth(authConfig, docker)
|
||||||
|
|
||||||
// Create providers service
|
// Create providers service
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"tinyauth/internal/docker"
|
"tinyauth/internal/docker"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
@@ -17,14 +19,41 @@ import (
|
|||||||
|
|
||||||
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
|
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
|
||||||
return &Auth{
|
return &Auth{
|
||||||
Docker: docker,
|
Config: config,
|
||||||
Config: config,
|
Docker: docker,
|
||||||
|
LoginAttempts: make(map[string]*types.LoginAttempt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
Docker *docker.Docker
|
Config types.AuthConfig
|
||||||
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 {
|
func (auth *Auth) GetUser(username string) *types.User {
|
||||||
@@ -42,14 +71,78 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool {
|
|||||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsAccountLocked checks if a username or IP is locked due to too many failed login attempts
|
||||||
|
func (auth *Auth) IsAccountLocked(identifier string) (bool, int) {
|
||||||
|
auth.LoginMutex.RLock()
|
||||||
|
defer auth.LoginMutex.RUnlock()
|
||||||
|
|
||||||
|
// Return false if rate limiting is not configured
|
||||||
|
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the identifier exists in the map
|
||||||
|
attempt, exists := auth.LoginAttempts[identifier]
|
||||||
|
if !exists {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// If account is locked, check if lock time has expired
|
||||||
|
if attempt.LockedUntil.After(time.Now()) {
|
||||||
|
// Calculate remaining lockout time in seconds
|
||||||
|
remaining := int(time.Until(attempt.LockedUntil).Seconds())
|
||||||
|
return true, remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock has expired
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordLoginAttempt records a login attempt for rate limiting
|
||||||
|
func (auth *Auth) RecordLoginAttempt(identifier string, success bool) {
|
||||||
|
// Skip if rate limiting is not configured
|
||||||
|
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.LoginMutex.Lock()
|
||||||
|
defer auth.LoginMutex.Unlock()
|
||||||
|
|
||||||
|
// Get current attempt record or create a new one
|
||||||
|
attempt, exists := auth.LoginAttempts[identifier]
|
||||||
|
if !exists {
|
||||||
|
attempt = &types.LoginAttempt{}
|
||||||
|
auth.LoginAttempts[identifier] = attempt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last attempt time
|
||||||
|
attempt.LastAttempt = time.Now()
|
||||||
|
|
||||||
|
// If successful login, reset failed attempts
|
||||||
|
if success {
|
||||||
|
attempt.FailedAttempts = 0
|
||||||
|
attempt.LockedUntil = time.Time{} // Reset lock time
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment failed attempts
|
||||||
|
attempt.FailedAttempts++
|
||||||
|
|
||||||
|
// If max retries reached, lock the account
|
||||||
|
if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
|
||||||
|
attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
|
||||||
|
log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
||||||
// If the whitelist is empty, allow all emails
|
// If the whitelist is empty, allow all emails
|
||||||
if len(auth.Config.OAuthWhitelist) == 0 {
|
if len(auth.Config.OauthWhitelist) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through the whitelist and return true if the email matches
|
// Loop through the whitelist and return true if the email matches
|
||||||
for _, email := range auth.Config.OAuthWhitelist {
|
for _, email := range auth.Config.OauthWhitelist {
|
||||||
if email == emailSrc {
|
if email == emailSrc {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -59,33 +152,13 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) GetCookieStore() *sessions.CookieStore {
|
|
||||||
// Create a new cookie store
|
|
||||||
store := sessions.NewCookieStore([]byte(auth.Config.Secret))
|
|
||||||
|
|
||||||
// Configure the cookie store
|
|
||||||
store.Options = &sessions.Options{
|
|
||||||
Path: "/",
|
|
||||||
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
|
|
||||||
Secure: auth.Config.CookieSecure,
|
|
||||||
MaxAge: auth.Config.SessionExpiry,
|
|
||||||
HttpOnly: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the cookie store
|
|
||||||
return store
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
|
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
|
||||||
log.Debug().Msg("Creating session cookie")
|
log.Debug().Msg("Creating session cookie")
|
||||||
|
|
||||||
// Get cookie store
|
|
||||||
store := auth.GetCookieStore()
|
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
sessions, err := store.Get(c.Request, "tinyauth")
|
session, err := auth.GetSession(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get session")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,15 +174,16 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set data
|
// Set data
|
||||||
sessions.Values["username"] = data.Username
|
session.Values["username"] = data.Username
|
||||||
sessions.Values["provider"] = data.Provider
|
session.Values["provider"] = data.Provider
|
||||||
sessions.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
|
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
|
||||||
sessions.Values["totpPending"] = data.TotpPending
|
session.Values["totpPending"] = data.TotpPending
|
||||||
|
session.Values["redirectURI"] = data.RedirectURI
|
||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
err = sessions.Save(c.Request, c.Writer)
|
err = session.Save(c.Request, c.Writer)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to save session")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,25 +194,22 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
|
|||||||
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
||||||
log.Debug().Msg("Deleting session cookie")
|
log.Debug().Msg("Deleting session cookie")
|
||||||
|
|
||||||
// Get cookie store
|
|
||||||
store := auth.GetCookieStore()
|
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
sessions, err := store.Get(c.Request, "tinyauth")
|
session, err := auth.GetSession(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get session")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear session
|
// Delete all values in the session
|
||||||
for key := range sessions.Values {
|
for key := range session.Values {
|
||||||
delete(sessions.Values, key)
|
delete(session.Values, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
err = sessions.Save(c.Request, c.Writer)
|
err = session.Save(c.Request, c.Writer)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to save session")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,31 +220,22 @@ func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
|||||||
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
||||||
log.Debug().Msg("Getting session cookie")
|
log.Debug().Msg("Getting session cookie")
|
||||||
|
|
||||||
// Get cookie store
|
|
||||||
store := auth.GetCookieStore()
|
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
sessions, err := store.Get(c.Request, "tinyauth")
|
session, err := auth.GetSession(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get session")
|
||||||
return types.SessionCookie{}, err
|
return types.SessionCookie{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get data
|
// Get data from session
|
||||||
cookieUsername := sessions.Values["username"]
|
username, usernameOk := session.Values["username"].(string)
|
||||||
cookieProvider := sessions.Values["provider"]
|
provider, providerOK := session.Values["provider"].(string)
|
||||||
cookieExpiry := sessions.Values["expiry"]
|
redirectURI, redirectOK := session.Values["redirectURI"].(string)
|
||||||
cookieTotpPending := sessions.Values["totpPending"]
|
expiry, expiryOk := session.Values["expiry"].(int64)
|
||||||
|
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
|
||||||
|
|
||||||
// Convert interfaces to correct types
|
if !usernameOk || !providerOK || !expiryOk || !redirectOK || !totpPendingOk {
|
||||||
username, usernameOk := cookieUsername.(string)
|
log.Warn().Msg("Session cookie is missing data")
|
||||||
provider, providerOk := cookieProvider.(string)
|
|
||||||
expiry, expiryOk := cookieExpiry.(int64)
|
|
||||||
totpPending, totpPendingOk := cookieTotpPending.(bool)
|
|
||||||
|
|
||||||
// Check if the cookie is invalid
|
|
||||||
if !usernameOk || !providerOk || !expiryOk || !totpPendingOk {
|
|
||||||
log.Warn().Msg("Session cookie invalid")
|
|
||||||
return types.SessionCookie{}, nil
|
return types.SessionCookie{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +257,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
|
|||||||
Username: username,
|
Username: username,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
TotpPending: totpPending,
|
TotpPending: totpPending,
|
||||||
|
RedirectURI: redirectURI,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +298,8 @@ func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bo
|
|||||||
if slices.Contains(labels.Users, context.Username) {
|
if slices.Contains(labels.Users, context.Username) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not allowed
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,20 +19,20 @@ import (
|
|||||||
|
|
||||||
func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
|
func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
|
||||||
return &Handlers{
|
return &Handlers{
|
||||||
|
Config: config,
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
Hooks: hooks,
|
Hooks: hooks,
|
||||||
Providers: providers,
|
Providers: providers,
|
||||||
Docker: docker,
|
Docker: docker,
|
||||||
Config: config,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
|
Config types.HandlersConfig
|
||||||
Auth *auth.Auth
|
Auth *auth.Auth
|
||||||
Hooks *hooks.Hooks
|
Hooks *hooks.Hooks
|
||||||
Providers *providers.Providers
|
Providers *providers.Providers
|
||||||
Docker *docker.Docker
|
Docker *docker.Docker
|
||||||
Config types.HandlersConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) AuthHandler(c *gin.Context) {
|
func (h *Handlers) AuthHandler(c *gin.Context) {
|
||||||
@@ -249,12 +249,34 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Msg("Got login request")
|
log.Debug().Msg("Got login request")
|
||||||
|
|
||||||
|
// Get client IP for rate limiting
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
|
// Create an identifier for rate limiting (username or IP if username doesn't exist yet)
|
||||||
|
rateIdentifier := login.Username
|
||||||
|
if rateIdentifier == "" {
|
||||||
|
rateIdentifier = clientIP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the account is locked due to too many failed attempts
|
||||||
|
locked, remainingTime := h.Auth.IsAccountLocked(rateIdentifier)
|
||||||
|
if locked {
|
||||||
|
log.Warn().Str("identifier", rateIdentifier).Int("remaining_seconds", remainingTime).Msg("Account is locked due to too many failed login attempts")
|
||||||
|
c.JSON(429, gin.H{
|
||||||
|
"status": 429,
|
||||||
|
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get user based on username
|
// Get user based on username
|
||||||
user := h.Auth.GetUser(login.Username)
|
user := h.Auth.GetUser(login.Username)
|
||||||
|
|
||||||
// User does not exist
|
// User does not exist
|
||||||
if user == nil {
|
if user == nil {
|
||||||
log.Debug().Str("username", login.Username).Msg("User not found")
|
log.Debug().Str("username", login.Username).Msg("User not found")
|
||||||
|
// Record failed login attempt
|
||||||
|
h.Auth.RecordLoginAttempt(rateIdentifier, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -267,6 +289,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
|||||||
// Check if password is correct
|
// Check if password is correct
|
||||||
if !h.Auth.CheckPassword(*user, login.Password) {
|
if !h.Auth.CheckPassword(*user, login.Password) {
|
||||||
log.Debug().Str("username", login.Username).Msg("Password incorrect")
|
log.Debug().Str("username", login.Username).Msg("Password incorrect")
|
||||||
|
// Record failed login attempt
|
||||||
|
h.Auth.RecordLoginAttempt(rateIdentifier, false)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -276,6 +300,9 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Msg("Password correct, checking totp")
|
log.Debug().Msg("Password correct, checking totp")
|
||||||
|
|
||||||
|
// Record successful login attempt (will reset failed attempt counter)
|
||||||
|
h.Auth.RecordLoginAttempt(rateIdentifier, true)
|
||||||
|
|
||||||
// Check if user has totp enabled
|
// Check if user has totp enabled
|
||||||
if user.TotpSecret != "" {
|
if user.TotpSecret != "" {
|
||||||
log.Debug().Msg("Totp enabled")
|
log.Debug().Msg("Totp enabled")
|
||||||
@@ -393,9 +420,6 @@ func (h *Handlers) LogoutHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Msg("Cleaning up redirect cookie")
|
log.Debug().Msg("Cleaning up redirect cookie")
|
||||||
|
|
||||||
// Clean up redirect cookie if it exists
|
|
||||||
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
|
|
||||||
|
|
||||||
// Return logged out
|
// Return logged out
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -502,7 +526,9 @@ func (h *Handlers) OauthUrlHandler(c *gin.Context) {
|
|||||||
// Set redirect cookie if redirect URI is provided
|
// Set redirect cookie if redirect URI is provided
|
||||||
if redirectURI != "" {
|
if redirectURI != "" {
|
||||||
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
|
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
|
||||||
c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", h.Config.Domain, h.Config.CookieSecure, true)
|
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
|
RedirectURI: redirectURI,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it
|
// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it
|
||||||
@@ -624,28 +650,25 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Msg("Email whitelisted")
|
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{
|
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
Username: email,
|
Username: email,
|
||||||
Provider: providerName.Provider,
|
Provider: providerName.Provider,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get redirect URI
|
|
||||||
redirectURI, err := c.Cookie("tinyauth_redirect_uri")
|
|
||||||
|
|
||||||
// If it is empty it means that no redirect_uri was provided to the login screen so we just log in
|
// If it is empty it means that no redirect_uri was provided to the login screen so we just log in
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
|
c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI")
|
log.Debug().Str("redirectURI", cookie.RedirectURI).Msg("Got redirect URI")
|
||||||
|
|
||||||
// Clean up redirect cookie since we already have the value
|
|
||||||
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
|
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
queries, err := query.Values(types.LoginQuery{
|
queries, err := query.Values(types.LoginQuery{
|
||||||
RedirectURI: redirectURI,
|
RedirectURI: cookie.RedirectURI,
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Debug().Msg("Got redirect query")
|
log.Debug().Msg("Got redirect query")
|
||||||
|
|||||||
@@ -24,12 +24,6 @@ type Hooks struct {
|
|||||||
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
||||||
// Get session cookie and basic auth
|
// Get session cookie and basic auth
|
||||||
cookie, err := hooks.Auth.GetSessionCookie(c)
|
cookie, err := hooks.Auth.GetSessionCookie(c)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to get session cookie")
|
|
||||||
return types.UserContext{}
|
|
||||||
}
|
|
||||||
|
|
||||||
basic := hooks.Auth.GetBasicAuth(c)
|
basic := hooks.Auth.GetBasicAuth(c)
|
||||||
|
|
||||||
// Check if basic auth is set
|
// Check if basic auth is set
|
||||||
@@ -52,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
|
// Check if session cookie has totp pending
|
||||||
if cookie.TotpPending {
|
if cookie.TotpPending {
|
||||||
log.Debug().Msg("Totp pending")
|
log.Debug().Msg("Totp pending")
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ func NewOAuth(config oauth2.Config) *OAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OAuth struct {
|
type OAuth struct {
|
||||||
Verifier string
|
Config oauth2.Config
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Token *oauth2.Token
|
Token *oauth2.Token
|
||||||
Config oauth2.Config
|
Verifier string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oauth *OAuth) Init() {
|
func (oauth *OAuth) Init() {
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ func NewProviders(config types.OAuthConfig) *Providers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Providers struct {
|
type Providers struct {
|
||||||
|
Config types.OAuthConfig
|
||||||
Github *oauth.OAuth
|
Github *oauth.OAuth
|
||||||
Google *oauth.OAuth
|
Google *oauth.OAuth
|
||||||
Tailscale *oauth.OAuth
|
Tailscale *oauth.OAuth
|
||||||
Generic *oauth.OAuth
|
Generic *oauth.OAuth
|
||||||
Config types.OAuthConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (providers *Providers) Init() {
|
func (providers *Providers) Init() {
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ type UserContextResponse struct {
|
|||||||
TotpPending bool `json:"totpPending"`
|
TotpPending bool `json:"totpPending"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App Context is the response for the app context endpoint
|
||||||
|
type AppContext struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
ConfiguredProviders []string `json:"configuredProviders"`
|
||||||
|
DisableContinue bool `json:"disableContinue"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
GenericName string `json:"genericName"`
|
||||||
|
}
|
||||||
|
|
||||||
// Totp request is the request for the totp endpoint
|
// Totp request is the request for the totp endpoint
|
||||||
type TotpRequest struct {
|
type TotpRequest struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
|
|||||||
@@ -33,12 +33,16 @@ type Config struct {
|
|||||||
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
|
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
|
||||||
Title string `mapstructure:"app-title"`
|
Title string `mapstructure:"app-title"`
|
||||||
EnvFile string `mapstructure:"env-file"`
|
EnvFile string `mapstructure:"env-file"`
|
||||||
|
LoginTimeout int `mapstructure:"login-timeout"`
|
||||||
|
LoginMaxRetries int `mapstructure:"login-max-retries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIConfig is the configuration for the API
|
// Server configuration
|
||||||
type APIConfig struct {
|
type HandlersConfig struct {
|
||||||
Port int
|
AppURL string
|
||||||
Address string
|
DisableContinue bool
|
||||||
|
GenericName string
|
||||||
|
Title string
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuthConfig is the configuration for the providers
|
// OAuthConfig is the configuration for the providers
|
||||||
@@ -58,22 +62,20 @@ type OAuthConfig struct {
|
|||||||
AppURL string
|
AppURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server configuration
|
// APIConfig is the configuration for the API
|
||||||
type HandlersConfig struct {
|
type APIConfig struct {
|
||||||
AppURL string
|
Port int
|
||||||
Domain string
|
Address string
|
||||||
CookieSecure bool
|
|
||||||
DisableContinue bool
|
|
||||||
GenericName string
|
|
||||||
Title string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth configuration
|
// AuthConfig is the configuration for the auth service
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
Domain string
|
Users Users
|
||||||
Secret string
|
OauthWhitelist []string
|
||||||
CookieSecure bool
|
SessionExpiry int
|
||||||
SessionExpiry int
|
Secret string
|
||||||
Users Users
|
CookieSecure bool
|
||||||
OAuthWhitelist []string
|
Domain string
|
||||||
|
LoginTimeout int
|
||||||
|
LoginMaxRetries int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import "tinyauth/internal/oauth"
|
import (
|
||||||
|
"time"
|
||||||
|
"tinyauth/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
// User is the struct for a user
|
// User is the struct for a user
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -24,6 +27,7 @@ type SessionCookie struct {
|
|||||||
Username string
|
Username string
|
||||||
Provider string
|
Provider string
|
||||||
TotpPending bool
|
TotpPending bool
|
||||||
|
RedirectURI string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TinyauthLabels is the labels for the tinyauth container
|
// TinyauthLabels is the labels for the tinyauth container
|
||||||
@@ -43,12 +47,9 @@ type UserContext struct {
|
|||||||
TotpPending bool
|
TotpPending bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// App Context is the response for the app context endpoint
|
// LoginAttempt tracks information about login attempts for rate limiting
|
||||||
type AppContext struct {
|
type LoginAttempt struct {
|
||||||
Status int `json:"status"`
|
FailedAttempts int
|
||||||
Message string `json:"message"`
|
LastAttempt time.Time
|
||||||
ConfiguredProviders []string `json:"configuredProviders"`
|
LockedUntil time.Time
|
||||||
DisableContinue bool `json:"disableContinue"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
GenericName string `json:"genericName"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user