Compare commits

..

2 Commits

Author SHA1 Message Date
Stavros
df10eb65ce chore: tidy go mod 2025-04-03 15:45:03 +03:00
Stavros
8bf5a6067e wip 2025-04-03 15:44:47 +03:00
59 changed files with 623 additions and 869 deletions

View File

@@ -12,6 +12,9 @@ GITHUB_CLIENT_SECRET_FILE=github_client_secret_file
GOOGLE_CLIENT_ID=google_client_id GOOGLE_CLIENT_ID=google_client_id
GOOGLE_CLIENT_SECRET=google_client_secret GOOGLE_CLIENT_SECRET=google_client_secret
GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file
TAILSCALE_CLIENT_ID=tailscale_client_id
TAILSCALE_CLIENT_SECRET=tailscale_client_secret
TAILSCALE_CLIENT_SECRET_FILE=tailscale__client_secret_file
GENERIC_CLIENT_ID=generic_client_id GENERIC_CLIENT_ID=generic_client_id
GENERIC_CLIENT_SECRET=generic_client_secret GENERIC_CLIENT_SECRET=generic_client_secret
GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file
@@ -23,7 +26,5 @@ DISABLE_CONTINUE=false
OAUTH_WHITELIST= OAUTH_WHITELIST=
GENERIC_NAME=My OAuth GENERIC_NAME=My OAuth
SESSION_EXPIRY=7200 SESSION_EXPIRY=7200
LOGIN_TIMEOUT=300
LOGIN_MAX_RETRIES=5
LOG_LEVEL=0 LOG_LEVEL=0
APP_TITLE=Tinyauth SSO APP_TITLE=Tinyauth SSO

View File

@@ -6,85 +6,7 @@ on:
- "v*" - "v*"
jobs: jobs:
binary-build: 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
@@ -129,7 +51,7 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
image-build-arm: build-arm:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
steps: steps:
- name: Checkout - name: Checkout
@@ -174,11 +96,11 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
image-merge: merge:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- image-build - build
- image-build-arm - build-arm
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -212,20 +134,3 @@ 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/*

View File

@@ -3,7 +3,7 @@ name: Publish translations
on: on:
push: push:
branches: branches:
- i18n_v* - main
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -16,53 +16,7 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
get-branches:
runs-on: ubuntu-latest
outputs:
i18n-branches: ${{ steps.get-branches.outputs.result }}
steps:
- name: Get branches
id: get-branches
uses: actions/github-script@v7
with:
script: |
const { data: repos } = await github.rest.repos.listBranches({
owner: context.repo.owner,
repo: context.repo.repo,
})
const i18nBranches = repos.filter((branch) => branch.name.startsWith("i18n_v"))
const i18nBranchNames = i18nBranches.map((branch) => branch.name)
return i18nBranchNames
get-translations:
needs: get-branches
runs-on: ubuntu-latest
strategy:
matrix:
branch: ${{ fromJson(needs.get-branches.outputs.i18n-branches) }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ matrix.branch }}
- name: Get translation version
id: get-version
run: |
branch=${{ matrix.branch }}
version=${branch#i18n_}
echo "version=$version" >> $GITHUB_OUTPUT
- name: Upload translations
uses: actions/upload-artifact@v4
with:
name: ${{ steps.get-version.outputs.version }}
path: frontend/src/lib/i18n/locales
build: build:
needs: get-translations
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -71,14 +25,10 @@ jobs:
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v4 uses: actions/configure-pages@v4
- name: Prepare output directory - name: Move translations
run: | run: |
mkdir -p dist/i18n/ mkdir -p dist
mv frontend/src/lib/i18n/locales dist/i18n
- name: Download translations
uses: actions/download-artifact@v4
with:
path: dist/i18n/
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v3

View File

@@ -17,8 +17,6 @@
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.
![Login](assets/screenshot.png)
> [!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.
@@ -63,7 +61,3 @@ Credits for the logo of this app go to:
- **Freepik** for providing the police hat and badge. - **Freepik** for providing the police hat and badge.
- **Renee French** for the original gopher logo. - **Renee French** for the original gopher logo.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=steveiliop56/tinyauth&type=Date)](https://www.star-history.com/#steveiliop56/tinyauth&Date)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -2,6 +2,7 @@ package cmd
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"strings" "strings"
"time" "time"
@@ -42,6 +43,7 @@ var rootCmd = &cobra.Command{
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile) config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile) config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile) config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
config.TailscaleClientSecret = utils.GetSecret(config.TailscaleClientSecret, config.TailscaleClientSecretFile)
// Validate config // Validate config
validator := validator.New() validator := validator.New()
@@ -76,22 +78,26 @@ var rootCmd = &cobra.Command{
// Create OAuth config // Create OAuth config
oauthConfig := types.OAuthConfig{ oauthConfig := types.OAuthConfig{
GithubClientId: config.GithubClientId, GithubClientId: config.GithubClientId,
GithubClientSecret: config.GithubClientSecret, GithubClientSecret: config.GithubClientSecret,
GoogleClientId: config.GoogleClientId, GoogleClientId: config.GoogleClientId,
GoogleClientSecret: config.GoogleClientSecret, GoogleClientSecret: config.GoogleClientSecret,
GenericClientId: config.GenericClientId, TailscaleClientId: config.TailscaleClientId,
GenericClientSecret: config.GenericClientSecret, TailscaleClientSecret: config.TailscaleClientSecret,
GenericScopes: strings.Split(config.GenericScopes, ","), GenericClientId: config.GenericClientId,
GenericAuthURL: config.GenericAuthURL, GenericClientSecret: config.GenericClientSecret,
GenericTokenURL: config.GenericTokenURL, GenericScopes: strings.Split(config.GenericScopes, ","),
GenericUserURL: config.GenericUserURL, GenericAuthURL: config.GenericAuthURL,
AppURL: config.AppURL, GenericTokenURL: config.GenericTokenURL,
GenericUserURL: config.GenericUserURL,
AppURL: config.AppURL,
} }
// Create handlers config // Create handlers config
handlersConfig := types.HandlersConfig{ serverConfig := types.HandlersConfig{
AppURL: config.AppURL, AppURL: config.AppURL,
Domain: fmt.Sprintf(".%s", domain),
CookieSecure: config.CookieSecure,
DisableContinue: config.DisableContinue, DisableContinue: config.DisableContinue,
Title: config.Title, Title: config.Title,
GenericName: config.GenericName, GenericName: config.GenericName,
@@ -103,18 +109,6 @@ var rootCmd = &cobra.Command{
Address: config.Address, Address: config.Address,
} }
// Create auth config
authConfig := types.AuthConfig{
Users: users,
OauthWhitelist: oauthWhitelist,
Secret: config.Secret,
CookieSecure: config.CookieSecure,
SessionExpiry: config.SessionExpiry,
Domain: domain,
LoginTimeout: config.LoginTimeout,
LoginMaxRetries: config.LoginMaxRetries,
}
// Create docker service // Create docker service
docker := docker.NewDocker() docker := docker.NewDocker()
@@ -123,7 +117,14 @@ var rootCmd = &cobra.Command{
HandleError(err, "Failed to initialize docker") HandleError(err, "Failed to initialize docker")
// Create auth service // Create auth service
auth := auth.NewAuth(authConfig, docker) auth := auth.NewAuth(types.AuthConfig{
Domain: domain,
Secret: config.Secret,
SessionExpiry: config.SessionExpiry,
CookieSecure: config.CookieSecure,
Users: users,
OAuthWhitelist: oauthWhitelist,
}, docker)
// Create OAuth providers service // Create OAuth providers service
providers := providers.NewProviders(oauthConfig) providers := providers.NewProviders(oauthConfig)
@@ -135,7 +136,7 @@ var rootCmd = &cobra.Command{
hooks := hooks.NewHooks(auth, providers) hooks := hooks.NewHooks(auth, providers)
// Create handlers // Create handlers
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker)
// Create API // Create API
api := api.NewAPI(apiConfig, handlers) api := api.NewAPI(apiConfig, handlers)
@@ -186,6 +187,9 @@ func init() {
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.") rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.") rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.") rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
rootCmd.Flags().String("tailscale-client-id", "", "Tailscale OAuth client ID.")
rootCmd.Flags().String("tailscale-client-secret", "", "Tailscale OAuth client secret.")
rootCmd.Flags().String("tailscale-client-secret-file", "", "Tailscale OAuth client secret file.")
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.") rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.") rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.") rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
@@ -197,8 +201,6 @@ func init() {
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.") rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.") rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.") rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).")
rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).")
rootCmd.Flags().Int("log-level", 1, "Log level.") rootCmd.Flags().Int("log-level", 1, "Log level.")
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
@@ -217,6 +219,9 @@ func init() {
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID") viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET") viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE") viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
viper.BindEnv("tailscale-client-id", "TAILSCALE_CLIENT_ID")
viper.BindEnv("tailscale-client-secret", "TAILSCALE_CLIENT_SECRET")
viper.BindEnv("tailscale-client-secret-file", "TAILSCALE_CLIENT_SECRET_FILE")
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID") viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET") viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE") viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
@@ -230,8 +235,6 @@ func init() {
viper.BindEnv("session-expiry", "SESSION_EXPIRY") viper.BindEnv("session-expiry", "SESSION_EXPIRY")
viper.BindEnv("log-level", "LOG_LEVEL") viper.BindEnv("log-level", "LOG_LEVEL")
viper.BindEnv("app-title", "APP_TITLE") viper.BindEnv("app-title", "APP_TITLE")
viper.BindEnv("login-timeout", "LOGIN_TIMEOUT")
viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES")
// Bind flags to viper // Bind flags to viper
viper.BindPFlags(rootCmd.Flags()) viper.BindPFlags(rootCmd.Flags())

View File

@@ -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="username" placeholder="user@example.com"
required required
disabled={isLoading} disabled={isLoading}
key={form.key("username")} key={form.key("username")}

View File

@@ -2,6 +2,7 @@ import { Grid, Button } from "@mantine/core";
import { GithubIcon } from "../../icons/github"; import { GithubIcon } from "../../icons/github";
import { GoogleIcon } from "../../icons/google"; import { GoogleIcon } from "../../icons/google";
import { OAuthIcon } from "../../icons/oauth"; import { OAuthIcon } from "../../icons/oauth";
import { TailscaleIcon } from "../../icons/tailscale";
interface OAuthButtonsProps { interface OAuthButtonsProps {
oauthProviders: string[]; oauthProviders: string[];
@@ -40,6 +41,19 @@ export const OAuthButtons = (props: OAuthButtonsProps) => {
</Button> </Button>
</Grid.Col> </Grid.Col>
)} )}
{oauthProviders.includes("tailscale") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={<TailscaleIcon style={{ width: 14, height: 14 }} />}
variant="default"
onClick={() => mutate("tailscale")}
loading={isLoading}
>
Tailscale
</Button>
</Grid.Col>
)}
{oauthProviders.includes("generic") && ( {oauthProviders.includes("generic") && (
<Grid.Col span="content"> <Grid.Col span="content">
<Button <Button

View File

@@ -0,0 +1,55 @@
import type { SVGProps } from "react";
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
width={24}
height={24}
{...props}
>
<style>{".st0{opacity:0.2;fill:#CCCAC9;}.st1{fill:#FFFFFF;}"}</style>
<g>
<g>
<path
className="st0"
d="M65.6,127.7c35.3,0,63.9-28.6,63.9-63.9S100.9,0,65.6,0S1.8,28.6,1.8,63.9S30.4,127.7,65.6,127.7z"
/>
<path
className="st1"
d="M65.6,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,219,1.8,254.2S30.4,318.1,65.6,318.1z"
/>
<path
className="st0"
d="M65.6,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9S1.8,412.9,1.8,448.1S30.4,512,65.6,512z"
/>
<path
className="st1"
d="M257.2,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,318.1,257.2,318.1z"
/>
<path
className="st1"
d="M257.2,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S221.9,512,257.2,512z"
/>
<path
className="st0"
d="M257.2,127.7c35.3,0,63.9-28.6,63.9-63.9S292.5,0,257.2,0s-63.9,28.6-63.9,63.9S221.9,127.7,257.2,127.7z"
/>
<path
className="st0"
d="M446.4,127.7c35.3,0,63.9-28.6,63.9-63.9S481.6,0,446.4,0c-35.3,0-63.9,28.6-63.9,63.9S411.1,127.7,446.4,127.7z"
/>
<path
className="st1"
d="M446.4,318.1c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,318.1,446.4,318.1z"
/>
<path
className="st0"
d="M446.4,512c35.3,0,63.9-28.6,63.9-63.9s-28.6-63.9-63.9-63.9s-63.9,28.6-63.9,63.9S411.1,512,446.4,512z"
/>
</g>
</g>
</svg>
);
}

View File

@@ -28,7 +28,7 @@ i18n
], ],
backendOptions: [ backendOptions: [
{ {
loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json", loadPath: "https://cdn.tinyauth.app/i18n/{{lng}}.json",
}, },
], ],
}, },

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -1,46 +1,45 @@
{ {
"loginTitle": "مرحبا بعودتك، قم بتسجيل الدخول باستخدام", "loginTitle": "Welcome back, login with",
"loginDivider": "أو المتابعة بكلمة المرور", "loginDivider": "Or continue with password",
"loginUsername": "اسم المستخدم", "loginUsername": "Username",
"loginPassword": "كلمة المرور", "loginPassword": "Password",
"loginSubmit": "تسجيل الدخول", "loginSubmit": "Login",
"loginFailTitle": "فشل تسجيل الدخول", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "الرجاء التحقق من اسم المستخدم وكلمة المرور", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "فشلت في تسجيل الدخول عدة مرات، الرجاء المحاولة مرة أخرى لاحقا", "loginSuccessTitle": "Logged in",
"loginSuccessTitle": "تم تسجيل الدخول", "loginSuccessSubtitle": "Welcome back!",
"loginSuccessSubtitle": "مرحبا بعودتك!", "loginOauthFailTitle": "Internal error",
"loginOauthFailTitle": "خطأ داخلي", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthFailSubtitle": "فشل في الحصول على رابط OAuth", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessTitle": "إعادة توجيه", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthSuccessSubtitle": "إعادة توجيه إلى مزود OAuth الخاص بك", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingTitle": "إعادة توجيه...", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectingSubtitle": "يجب إعادة توجيهك إلى التطبيق قريبا", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectTitle": "إعادة توجيه غير صالحة", "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInvalidRedirectSubtitle": "رابط إعادة التوجيه غير صالح", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectTitle": "إعادة توجيه غير آمنة", "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
"continueInsecureRedirectSubtitle": "أنت تحاول إعادة التوجيه من <Code>https</Code> إلى <Code>http</Code>، هل أنت متأكد أنك تريد المتابعة؟", "continueTitle": "Continue",
"continueTitle": "متابعة", "continueSubtitle": "Click the button to continue to your app.",
"continueSubtitle": "انقر الزر للمتابعة إلى التطبيق الخاص بك.", "internalErrorTitle": "Internal Server Error",
"internalErrorTitle": "خطأ داخلي في الخادم", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorSubtitle": "حدث خطأ على الخادم ولا يمكن حاليا تلبية طلبك.", "internalErrorButton": "Try again",
"internalErrorButton": "حاول مجددا", "logoutFailTitle": "Failed to log out",
"logoutFailTitle": "فشل تسجيل الخروج", "logoutFailSubtitle": "Please try again",
"logoutFailSubtitle": "يرجى إعادة المحاولة", "logoutSuccessTitle": "Logged out",
"logoutSuccessTitle": "تم تسجيل الخروج", "logoutSuccessSubtitle": "You have been logged out",
"logoutSuccessSubtitle": "تم تسجيل خروجك", "logoutTitle": "Logout",
"logoutTitle": "تسجيل الخروج", "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
"logoutUsernameSubtitle": "أنت حاليا مسجل الدخول ك <Code>{{username}}</Code>، انقر الزر أدناه لتسجيل الخروج.", "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
"logoutOauthSubtitle": "أنت حاليا مسجل الدخول ك <Code>{{username}}</Code> باستخدام مزود OAuth {{provider}} ، انقر الزر أدناه لتسجيل الخروج.", "notFoundTitle": "Page not found",
"notFoundTitle": "الصفحة غير موجودة", "notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundSubtitle": "الصفحة التي تبحث عنها غير موجودة.", "notFoundButton": "Go home",
"notFoundButton": "انتقل إلى الرئيسية", "totpFailTitle": "Failed to verify code",
"totpFailTitle": "فشل في التحقق من الرمز", "totpFailSubtitle": "Please check your code and try again",
"totpFailSubtitle": "الرجاء التحقق من الرمز الخاص بك وحاول مرة أخرى", "totpSuccessTitle": "Verified",
"totpSuccessTitle": "تم التحقق", "totpSuccessSubtitle": "Redirecting to your app",
"totpSuccessSubtitle": "إعادة توجيه إلى تطبيقك", "totpTitle": "Enter your TOTP code",
"totpTitle": "أدخل رمز TOTP الخاص بك", "unauthorizedTitle": "Unauthorized",
"unauthorizedTitle": "غير مرخص", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unauthorizedResourceSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بالوصول إلى المورد <Code>{{resource}}</Code>.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unaothorizedLoginSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بتسجيل الدخول.", "unauthorizedButton": "Try again"
"unauthorizedButton": "حاول مجددا"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -1,27 +1,26 @@
{ {
"loginTitle": "Willkommen zurück, logge dich ein mit", "loginTitle": "Welcome back, login with",
"loginDivider": "Oder mit Passwort fortfahren", "loginDivider": "Or continue with password",
"loginUsername": "Benutzername", "loginUsername": "Username",
"loginPassword": "Passwort", "loginPassword": "Password",
"loginSubmit": "Anmelden", "loginSubmit": "Login",
"loginFailTitle": "Login fehlgeschlagen", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Bitte überprüfe deinen Benutzernamen und Passwort", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "Sie konnten sich zu oft nicht einloggen, bitte versuchen Sie es später erneut",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Willkommen zurück!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Interner Fehler", "loginOauthFailTitle": "Internal error",
"loginOauthFailSubtitle": "Fehler beim Abrufen der OAuth-URL", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"continueRedirectingTitle": "Redirecting...", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueInvalidRedirectTitle": "Ungültige Weiterleitung", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectSubtitle": "Die Weiterleitungs-URL ist ungültig", "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInsecureRedirectTitle": "Unsichere Weiterleitung", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?", "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
"continueTitle": "Weiter", "continueTitle": "Continue",
"continueSubtitle": "Click the button to continue to your app.", "continueSubtitle": "Click the button to continue to your app.",
"internalErrorTitle": "Interner Serverfehler", "internalErrorTitle": "Internal Server Error",
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorButton": "Try again", "internalErrorButton": "Try again",
"logoutFailTitle": "Failed to log out", "logoutFailTitle": "Failed to log out",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Είσοδος", "loginSubmit": "Είσοδος",
"loginFailTitle": "Αποτυχία σύνδεσης", "loginFailTitle": "Αποτυχία σύνδεσης",
"loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης", "loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης",
"loginFailRateLimit": "Αποτύχατε να συνδεθείτε πάρα πολλές φορές, παρακαλώ προσπαθήστε ξανά αργότερα",
"loginSuccessTitle": "Συνδεδεμένος", "loginSuccessTitle": "Συνδεδεμένος",
"loginSuccessSubtitle": "Καλώς ήρθατε!", "loginSuccessSubtitle": "Καλώς ήρθατε!",
"loginOauthFailTitle": "Εσωτερικό σφάλμα", "loginOauthFailTitle": "Εσωτερικό σφάλμα",
@@ -40,7 +39,7 @@
"totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας", "totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας",
"totpTitle": "Εισάγετε τον κωδικό TOTP", "totpTitle": "Εισάγετε τον κωδικό TOTP",
"unauthorizedTitle": "Μη εξουσιοδοτημένο", "unauthorizedTitle": "Μη εξουσιοδοτημένο",
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να έχει πρόσβαση στον πόρο <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.", "unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να συνδεθεί.",
"unauthorizedButton": "Προσπαθήστε ξανά" "unauthorizedButton": "Προσπαθήστε ξανά"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"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",
@@ -40,7 +39,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 <Code>{{username}}</Code> n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à se connecter.", "unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à se connecter.",
"unauthorizedButton": "Réessayer" "unauthorizedButton": "Réessayer"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -1,46 +1,45 @@
{ {
"loginTitle": "Welkom terug, log in met", "loginTitle": "Welcome back, login with",
"loginDivider": "Of ga door met wachtwoord", "loginDivider": "Or continue with password",
"loginUsername": "Gebruikersnaam", "loginUsername": "Username",
"loginPassword": "Wachtwoord", "loginPassword": "Password",
"loginSubmit": "Log in", "loginSubmit": "Login",
"loginFailTitle": "Mislukt om in te loggen", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Controleer je gebruikersnaam en wachtwoord", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "Inloggen te vaak mislukt, probeer het later opnieuw", "loginSuccessTitle": "Logged in",
"loginSuccessTitle": "Ingelogd", "loginSuccessSubtitle": "Welcome back!",
"loginSuccessSubtitle": "Welkom terug!", "loginOauthFailTitle": "Internal error",
"loginOauthFailTitle": "Interne fout", "loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthFailSubtitle": "Fout bij het ophalen van OAuth URL", "loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessTitle": "Omleiden", "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthSuccessSubtitle": "Omleiden naar je OAuth provider", "continueRedirectingTitle": "Redirecting...",
"continueRedirectingTitle": "Omleiden...", "continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectingSubtitle": "Je wordt naar de app doorgestuurd", "continueInvalidRedirectTitle": "Invalid redirect",
"continueInvalidRedirectTitle": "Ongeldige omleiding", "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
"continueInvalidRedirectSubtitle": "De omleidings-URL is ongeldig", "continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectTitle": "Onveilige doorverwijzing", "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
"continueInsecureRedirectSubtitle": "Je probeert door te verwijzen van <Code>https</Code> naar <Code>http</Code>, weet je zeker dat je wilt doorgaan?", "continueTitle": "Continue",
"continueTitle": "Ga verder", "continueSubtitle": "Click the button to continue to your app.",
"continueSubtitle": "Klik op de knop om door te gaan naar de app.", "internalErrorTitle": "Internal Server Error",
"internalErrorTitle": "Interne server fout", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorSubtitle": "Er is een fout opgetreden op de server en het kan momenteel niet voldoen aan je verzoek.", "internalErrorButton": "Try again",
"internalErrorButton": "Opnieuw proberen", "logoutFailTitle": "Failed to log out",
"logoutFailTitle": "Afmelden mislukt", "logoutFailSubtitle": "Please try again",
"logoutFailSubtitle": "Probeer het opnieuw", "logoutSuccessTitle": "Logged out",
"logoutSuccessTitle": "Afgemeld", "logoutSuccessSubtitle": "You have been logged out",
"logoutSuccessSubtitle": "Je bent afgemeld", "logoutTitle": "Logout",
"logoutTitle": "Afmelden", "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
"logoutUsernameSubtitle": "Je bent momenteel ingelogd als <Code>{{username}}</Code>, klik op de knop hieronder om uit te loggen.", "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
"logoutOauthSubtitle": "Je bent momenteel ingelogd als <Code>{{username}}</Code> met behulp van de {{provider}} OAuth provider, klik op de knop hieronder om uit te loggen.", "notFoundTitle": "Page not found",
"notFoundTitle": "Pagina niet gevonden", "notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundSubtitle": "De pagina die je zoekt bestaat niet.", "notFoundButton": "Go home",
"notFoundButton": "Naar startpagina", "totpFailTitle": "Failed to verify code",
"totpFailTitle": "Verifiëren van code mislukt", "totpFailSubtitle": "Please check your code and try again",
"totpFailSubtitle": "Controleer je code en probeer het opnieuw", "totpSuccessTitle": "Verified",
"totpSuccessTitle": "Geverifiëerd", "totpSuccessSubtitle": "Redirecting to your app",
"totpSuccessSubtitle": "Omleiden naar je app", "totpTitle": "Enter your TOTP code",
"totpTitle": "Voer je TOTP-code in", "unauthorizedTitle": "Unauthorized",
"unauthorizedTitle": "Ongeautoriseerd", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unauthorizedResourceSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> heeft geen toegang tot de bron <Code>{{resource}}</Code>.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unaothorizedLoginSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> is niet gemachtigd om in te loggen.", "unauthorizedButton": "Try again"
"unauthorizedButton": "Opnieuw proberen"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -3,10 +3,9 @@
"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": "Zaloguj się", "loginSubmit": "Login",
"loginFailTitle": "Nie udało się zalogować", "loginFailTitle": "Failed to log in",
"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",
@@ -22,9 +21,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": "Wystąpił błąd na serwerze i obecnie nie można obsłużyć tego żądania.", "internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorButton": "Spróbuj ponownie", "internalErrorButton": "Spróbuj ponownie",
"logoutFailTitle": "Nie udało się wylogować", "logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Spróbuj ponownie", "logoutFailSubtitle": "Spróbuj ponownie",
"logoutSuccessTitle": "Wylogowano", "logoutSuccessTitle": "Wylogowano",
"logoutSuccessSubtitle": "Zostałeś wylogowany", "logoutSuccessSubtitle": "Zostałeś wylogowany",
@@ -40,7 +39,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 <Code>{{username}}</Code> nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do logowania.", "unaothorizedLoginSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do logowania.",
"unauthorizedButton": "Spróbuj ponownie" "unauthorizedButton": "Spróbuj ponownie"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -6,7 +6,6 @@
"loginSubmit": "Login", "loginSubmit": "Login",
"loginFailTitle": "Failed to log in", "loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password", "loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in", "loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!", "loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error", "loginOauthFailTitle": "Internal error",
@@ -40,7 +39,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 <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedButton": "Try again" "unauthorizedButton": "Try again"
} }

View File

@@ -29,7 +29,7 @@ const queryClient = new QueryClient({
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<MantineProvider defaultColorScheme="auto"> <MantineProvider forceColorScheme="dark">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Notifications /> <Notifications />
<AppContextProvider> <AppContextProvider>

View File

@@ -1,7 +1,7 @@
import { Paper, Title, Text, Divider } from "@mantine/core"; import { Paper, Title, Text, Divider } from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import axios, { type AxiosError } from "axios"; import axios from "axios";
import { useUserContext } from "../context/user-context"; import { useUserContext } from "../context/user-context";
import { Navigate } from "react-router"; import { Navigate } from "react-router";
import { Layout } from "../components/layouts/layout"; import { Layout } from "../components/layouts/layout";
@@ -33,17 +33,7 @@ export const LoginPage = () => {
mutationFn: (login: LoginFormValues) => { mutationFn: (login: LoginFormValues) => {
return axios.post("/api/login", login); return axios.post("/api/login", login);
}, },
onError: (data: AxiosError) => { onError: () => {
if (data.response) {
if (data.response.status === 429) {
notifications.show({
title: t("loginFailTitle"),
message: t("loginFailRateLimit"),
color: "red",
});
return;
}
}
notifications.show({ notifications.show({
title: t("loginFailTitle"), title: t("loginFailTitle"),
message: t("loginFailSubtitle"), message: t("loginFailSubtitle"),

View File

@@ -37,7 +37,6 @@ export const UnauthorizedPage = () => {
<Trans <Trans
i18nKey="unauthorizedLoginSubtitle" i18nKey="unauthorizedLoginSubtitle"
t={t} t={t}
components={{ Code: <Code /> }}
values={{ username }} values={{ username }}
/> />
</Text> </Text>

View File

@@ -17,15 +17,15 @@ import (
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API { func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
return &API{ return &API{
Config: config,
Handlers: handlers, Handlers: handlers,
Config: config,
} }
} }
type API struct { type API struct {
Config types.APIConfig
Router *gin.Engine Router *gin.Engine
Handlers *handlers.Handlers Handlers *handlers.Handlers
Config types.APIConfig
} }
func (api *API) Init() { func (api *API) Init() {

View File

@@ -19,6 +19,12 @@ import (
"github.com/magiconair/properties/assert" "github.com/magiconair/properties/assert"
) )
// User
var User = types.User{
Username: "user",
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
}
// Simple API config for tests // Simple API config for tests
var apiConfig = types.APIConfig{ var apiConfig = types.APIConfig{
Port: 8080, Port: 8080,
@@ -28,6 +34,8 @@ var apiConfig = types.APIConfig{
// Simple handlers config for tests // Simple handlers config for tests
var handlersConfig = types.HandlersConfig{ var handlersConfig = types.HandlersConfig{
AppURL: "http://localhost:8080", AppURL: "http://localhost:8080",
Domain: ".localhost",
CookieSecure: false,
DisableContinue: false, DisableContinue: false,
Title: "Tinyauth", Title: "Tinyauth",
GenericName: "Generic", GenericName: "Generic",
@@ -35,24 +43,19 @@ var handlersConfig = types.HandlersConfig{
// Simple auth config for tests // Simple auth config for tests
var authConfig = types.AuthConfig{ var authConfig = types.AuthConfig{
Users: types.Users{}, Domain: "localhost",
OauthWhitelist: []string{}, Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long CookieSecure: false,
CookieSecure: false, SessionExpiry: 3600,
SessionExpiry: 3600, Users: types.Users{
LoginTimeout: 0, User,
LoginMaxRetries: 0, },
OAuthWhitelist: []string{},
} }
// Cookie // Cookie
var cookie string var cookie string
// User
var user = types.User{
Username: "user",
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
}
// We need all this to be able to test the API // We need all this to be able to test the API
func getAPI(t *testing.T) *api.API { func getAPI(t *testing.T) *api.API {
// Create docker service // Create docker service
@@ -67,12 +70,6 @@ func getAPI(t *testing.T) *api.API {
} }
// Create auth service // Create auth service
authConfig.Users = types.Users{
{
Username: user.Username,
Password: user.Password,
},
}
auth := auth.NewAuth(authConfig, docker) auth := auth.NewAuth(authConfig, docker)
// Create providers service // Create providers service

View File

@@ -1 +1 @@
v3.2.1 v3.2.0

View File

@@ -2,11 +2,9 @@ package auth
import ( import (
"fmt" "fmt"
"net/http"
"regexp" "regexp"
"slices" "slices"
"strings" "strings"
"sync"
"time" "time"
"tinyauth/internal/docker" "tinyauth/internal/docker"
"tinyauth/internal/types" "tinyauth/internal/types"
@@ -19,41 +17,14 @@ import (
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth { func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
return &Auth{ return &Auth{
Config: config, Docker: docker,
Docker: docker, Config: config,
LoginAttempts: make(map[string]*types.LoginAttempt),
} }
} }
type Auth struct { type Auth struct {
Config types.AuthConfig Docker *docker.Docker
Docker *docker.Docker Config types.AuthConfig
LoginAttempts map[string]*types.LoginAttempt
LoginMutex sync.RWMutex
}
func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) {
// Create cookie store
store := sessions.NewCookieStore([]byte(auth.Config.Secret))
// Configure cookie store
store.Options = &sessions.Options{
Path: "/",
MaxAge: auth.Config.SessionExpiry,
Secure: auth.Config.CookieSecure,
HttpOnly: true,
SameSite: http.SameSiteDefaultMode,
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
}
// Get session
session, err := store.Get(c.Request, "tinyauth")
if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return nil, err
}
return session, nil
} }
func (auth *Auth) GetUser(username string) *types.User { func (auth *Auth) GetUser(username string) *types.User {
@@ -71,78 +42,14 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
} }
// IsAccountLocked checks if a username or IP is locked due to too many failed login attempts
func (auth *Auth) IsAccountLocked(identifier string) (bool, int) {
auth.LoginMutex.RLock()
defer auth.LoginMutex.RUnlock()
// Return false if rate limiting is not configured
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
return false, 0
}
// Check if the identifier exists in the map
attempt, exists := auth.LoginAttempts[identifier]
if !exists {
return false, 0
}
// If account is locked, check if lock time has expired
if attempt.LockedUntil.After(time.Now()) {
// Calculate remaining lockout time in seconds
remaining := int(time.Until(attempt.LockedUntil).Seconds())
return true, remaining
}
// Lock has expired
return false, 0
}
// RecordLoginAttempt records a login attempt for rate limiting
func (auth *Auth) RecordLoginAttempt(identifier string, success bool) {
// Skip if rate limiting is not configured
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
return
}
auth.LoginMutex.Lock()
defer auth.LoginMutex.Unlock()
// Get current attempt record or create a new one
attempt, exists := auth.LoginAttempts[identifier]
if !exists {
attempt = &types.LoginAttempt{}
auth.LoginAttempts[identifier] = attempt
}
// Update last attempt time
attempt.LastAttempt = time.Now()
// If successful login, reset failed attempts
if success {
attempt.FailedAttempts = 0
attempt.LockedUntil = time.Time{} // Reset lock time
return
}
// Increment failed attempts
attempt.FailedAttempts++
// If max retries reached, lock the account
if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
}
}
func (auth *Auth) EmailWhitelisted(emailSrc string) bool { func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
// If the whitelist is empty, allow all emails // If the whitelist is empty, allow all emails
if len(auth.Config.OauthWhitelist) == 0 { if len(auth.Config.OAuthWhitelist) == 0 {
return true return true
} }
// Loop through the whitelist and return true if the email matches // Loop through the whitelist and return true if the email matches
for _, email := range auth.Config.OauthWhitelist { for _, email := range auth.Config.OAuthWhitelist {
if email == emailSrc { if email == emailSrc {
return true return true
} }
@@ -152,13 +59,33 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
return false return false
} }
func (auth *Auth) GetCookieStore() *sessions.CookieStore {
// Create a new cookie store
store := sessions.NewCookieStore([]byte(auth.Config.Secret))
// Configure the cookie store
store.Options = &sessions.Options{
Path: "/",
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
Secure: auth.Config.CookieSecure,
MaxAge: auth.Config.SessionExpiry,
HttpOnly: true,
}
// Set the cookie store
return store
}
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error { func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
log.Debug().Msg("Creating session cookie") log.Debug().Msg("Creating session cookie")
// Get cookie store
store := auth.GetCookieStore()
// Get session // Get session
session, err := auth.GetSession(c) sessions, err := store.Get(c.Request, "tinyauth")
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return err return err
} }
@@ -174,16 +101,15 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
} }
// Set data // Set data
session.Values["username"] = data.Username sessions.Values["username"] = data.Username
session.Values["provider"] = data.Provider sessions.Values["provider"] = data.Provider
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix() sessions.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
session.Values["totpPending"] = data.TotpPending sessions.Values["totpPending"] = data.TotpPending
session.Values["redirectURI"] = data.RedirectURI
// Save session // Save session
err = session.Save(c.Request, c.Writer) err = sessions.Save(c.Request, c.Writer)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to save session")
return err return err
} }
@@ -194,22 +120,25 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error { func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
log.Debug().Msg("Deleting session cookie") log.Debug().Msg("Deleting session cookie")
// Get cookie store
store := auth.GetCookieStore()
// Get session // Get session
session, err := auth.GetSession(c) sessions, err := store.Get(c.Request, "tinyauth")
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return err return err
} }
// Delete all values in the session // Clear session
for key := range session.Values { for key := range sessions.Values {
delete(session.Values, key) delete(sessions.Values, key)
} }
// Save session // Save session
err = session.Save(c.Request, c.Writer) err = sessions.Save(c.Request, c.Writer)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to save session")
return err return err
} }
@@ -220,22 +149,31 @@ func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) { func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
log.Debug().Msg("Getting session cookie") log.Debug().Msg("Getting session cookie")
// Get cookie store
store := auth.GetCookieStore()
// Get session // Get session
session, err := auth.GetSession(c) sessions, err := store.Get(c.Request, "tinyauth")
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return types.SessionCookie{}, err return types.SessionCookie{}, err
} }
// Get data from session // Get data
username, usernameOk := session.Values["username"].(string) cookieUsername := sessions.Values["username"]
provider, providerOK := session.Values["provider"].(string) cookieProvider := sessions.Values["provider"]
redirectURI, redirectOK := session.Values["redirectURI"].(string) cookieExpiry := sessions.Values["expiry"]
expiry, expiryOk := session.Values["expiry"].(int64) cookieTotpPending := sessions.Values["totpPending"]
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
if !usernameOk || !providerOK || !expiryOk || !redirectOK || !totpPendingOk { // Convert interfaces to correct types
log.Warn().Msg("Session cookie is missing data") username, usernameOk := cookieUsername.(string)
provider, providerOk := cookieProvider.(string)
expiry, expiryOk := cookieExpiry.(int64)
totpPending, totpPendingOk := cookieTotpPending.(bool)
// Check if the cookie is invalid
if !usernameOk || !providerOk || !expiryOk || !totpPendingOk {
log.Warn().Msg("Session cookie invalid")
return types.SessionCookie{}, nil return types.SessionCookie{}, nil
} }
@@ -257,7 +195,6 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
Username: username, Username: username,
Provider: provider, Provider: provider,
TotpPending: totpPending, TotpPending: totpPending,
RedirectURI: redirectURI,
}, nil }, nil
} }
@@ -298,8 +235,6 @@ 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

View File

@@ -1,147 +0,0 @@
package auth_test
import (
"testing"
"time"
"tinyauth/internal/auth"
"tinyauth/internal/docker"
"tinyauth/internal/types"
)
var config = types.AuthConfig{
Users: types.Users{},
OauthWhitelist: []string{},
SessionExpiry: 3600,
}
func TestLoginRateLimiting(t *testing.T) {
// Initialize a new auth service with 3 max retries and 5 seconds timeout
config.LoginMaxRetries = 3
config.LoginTimeout = 5
authService := auth.NewAuth(config, &docker.Docker{})
// Test identifier
identifier := "test_user"
// Test successful login - should not lock account
t.Log("Testing successful login")
authService.RecordLoginAttempt(identifier, true)
locked, _ := authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should not be locked after successful login")
}
// Test 2 failed attempts - should not lock account yet
t.Log("Testing 2 failed login attempts")
authService.RecordLoginAttempt(identifier, false)
authService.RecordLoginAttempt(identifier, false)
locked, _ = authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should not be locked after only 2 failed attempts")
}
// Add one more failed attempt (total 3) - should lock account with maxRetries=3
t.Log("Testing 3 failed login attempts")
authService.RecordLoginAttempt(identifier, false)
locked, remainingTime := authService.IsAccountLocked(identifier)
if !locked {
t.Fatalf("Account should be locked after reaching max retries")
}
if remainingTime <= 0 || remainingTime > 5 {
t.Fatalf("Expected remaining time between 1-5 seconds, got %d", remainingTime)
}
// Test reset after waiting for timeout - use 1 second timeout for fast testing
t.Log("Testing unlocking after timeout")
// Reinitialize auth service with a shorter timeout for testing
config.LoginTimeout = 1
config.LoginMaxRetries = 3
authService = auth.NewAuth(config, &docker.Docker{})
// Add enough failed attempts to lock the account
for i := 0; i < 3; i++ {
authService.RecordLoginAttempt(identifier, false)
}
// Verify it's locked
locked, _ = authService.IsAccountLocked(identifier)
if !locked {
t.Fatalf("Account should be locked initially")
}
// Wait a bit and verify it gets unlocked after timeout
time.Sleep(1500 * time.Millisecond) // Wait longer than the timeout
locked, _ = authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should be unlocked after timeout period")
}
// Test disabled rate limiting
t.Log("Testing disabled rate limiting")
config.LoginMaxRetries = 0
config.LoginTimeout = 0
authService = auth.NewAuth(config, &docker.Docker{})
for i := 0; i < 10; i++ {
authService.RecordLoginAttempt(identifier, false)
}
locked, _ = authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should not be locked when rate limiting is disabled")
}
}
func TestConcurrentLoginAttempts(t *testing.T) {
// Initialize a new auth service with 2 max retries and 5 seconds timeout
config.LoginMaxRetries = 2
config.LoginTimeout = 5
authService := auth.NewAuth(config, &docker.Docker{})
// Test multiple identifiers
identifiers := []string{"user1", "user2", "user3"}
// Test that locking one identifier doesn't affect others
t.Log("Testing multiple identifiers")
// Add enough failed attempts to lock first user (2 attempts with maxRetries=2)
authService.RecordLoginAttempt(identifiers[0], false)
authService.RecordLoginAttempt(identifiers[0], false)
// Check if first user is locked
locked, _ := authService.IsAccountLocked(identifiers[0])
if !locked {
t.Fatalf("User1 should be locked after reaching max retries")
}
// Check that other users are not affected
for i := 1; i < len(identifiers); i++ {
locked, _ := authService.IsAccountLocked(identifiers[i])
if locked {
t.Fatalf("User%d should not be locked", i+1)
}
}
// Test successful login after failed attempts (but before lock)
t.Log("Testing successful login after failed attempts but before lock")
// One failed attempt for user2
authService.RecordLoginAttempt(identifiers[1], false)
// Successful login should reset the counter
authService.RecordLoginAttempt(identifiers[1], true)
// Now try a failed login again - should not be locked as counter was reset
authService.RecordLoginAttempt(identifiers[1], false)
locked, _ = authService.IsAccountLocked(identifiers[1])
if locked {
t.Fatalf("User2 should not be locked after successful login reset")
}
}

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"fmt" "fmt"
"math/rand/v2"
"net/http" "net/http"
"strings" "strings"
"tinyauth/internal/auth" "tinyauth/internal/auth"
@@ -18,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) {
@@ -248,34 +249,12 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
log.Debug().Msg("Got login request") log.Debug().Msg("Got login request")
// Get client IP for rate limiting
clientIP := c.ClientIP()
// Create an identifier for rate limiting (username or IP if username doesn't exist yet)
rateIdentifier := login.Username
if rateIdentifier == "" {
rateIdentifier = clientIP
}
// Check if the account is locked due to too many failed attempts
locked, remainingTime := h.Auth.IsAccountLocked(rateIdentifier)
if locked {
log.Warn().Str("identifier", rateIdentifier).Int("remaining_seconds", remainingTime).Msg("Account is locked due to too many failed login attempts")
c.JSON(429, gin.H{
"status": 429,
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime),
})
return
}
// Get user based on username // Get user based on username
user := h.Auth.GetUser(login.Username) user := h.Auth.GetUser(login.Username)
// User does not exist // User does not exist
if user == nil { if user == nil {
log.Debug().Str("username", login.Username).Msg("User not found") log.Debug().Str("username", login.Username).Msg("User not found")
// Record failed login attempt
h.Auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -288,8 +267,6 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
// Check if password is correct // Check if password is correct
if !h.Auth.CheckPassword(*user, login.Password) { if !h.Auth.CheckPassword(*user, login.Password) {
log.Debug().Str("username", login.Username).Msg("Password incorrect") log.Debug().Str("username", login.Username).Msg("Password incorrect")
// Record failed login attempt
h.Auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -299,9 +276,6 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
log.Debug().Msg("Password correct, checking totp") log.Debug().Msg("Password correct, checking totp")
// Record successful login attempt (will reset failed attempt counter)
h.Auth.RecordLoginAttempt(rateIdentifier, true)
// Check if user has totp enabled // Check if user has totp enabled
if user.TotpSecret != "" { if user.TotpSecret != "" {
log.Debug().Msg("Totp enabled") log.Debug().Msg("Totp enabled")
@@ -419,6 +393,9 @@ func (h *Handlers) LogoutHandler(c *gin.Context) {
log.Debug().Msg("Cleaning up redirect cookie") log.Debug().Msg("Cleaning up redirect cookie")
// Clean up redirect cookie if it exists
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
// Return logged out // Return logged out
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
@@ -525,9 +502,33 @@ func (h *Handlers) OauthUrlHandler(c *gin.Context) {
// Set redirect cookie if redirect URI is provided // Set redirect cookie if redirect URI is provided
if redirectURI != "" { if redirectURI != "" {
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie") log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
h.Auth.CreateSessionCookie(c, &types.SessionCookie{ c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", h.Config.Domain, h.Config.CookieSecure, true)
RedirectURI: redirectURI, }
// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it
if request.Provider == "tailscale" {
// Build tailscale query
queries, err := query.Values(types.TailscaleQuery{
Code: (1000 + rand.IntN(9000)),
}) })
// Handle error
if err != nil {
log.Error().Err(err).Msg("Failed to build queries")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
// Return tailscale URL (immidiately redirects to the callback)
c.JSON(200, gin.H{
"status": 200,
"message": "OK",
"url": fmt.Sprintf("%s/api/oauth/callback/tailscale?%s", h.Config.AppURL, queries.Encode()),
})
return
} }
// Return auth URL // Return auth URL
@@ -623,25 +624,28 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
log.Debug().Msg("Email whitelisted") log.Debug().Msg("Email whitelisted")
// Get redirect URI // Create session cookie
cookie, err := h.Auth.GetSessionCookie(c)
// Create session cookie (also cleans up redirect cookie)
h.Auth.CreateSessionCookie(c, &types.SessionCookie{ h.Auth.CreateSessionCookie(c, &types.SessionCookie{
Username: email, Username: email,
Provider: providerName.Provider, Provider: providerName.Provider,
}) })
// Get redirect URI
redirectURI, err := c.Cookie("tinyauth_redirect_uri")
// If it is empty it means that no redirect_uri was provided to the login screen so we just log in // If it is empty it means that no redirect_uri was provided to the login screen so we just log in
if err != nil { if err != nil {
c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL) c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
} }
log.Debug().Str("redirectURI", cookie.RedirectURI).Msg("Got redirect URI") log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI")
// Clean up redirect cookie since we already have the value
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
// Build query // Build query
queries, err := query.Values(types.LoginQuery{ queries, err := query.Values(types.LoginQuery{
RedirectURI: cookie.RedirectURI, RedirectURI: redirectURI,
}) })
log.Debug().Msg("Got redirect query") log.Debug().Msg("Got redirect query")

View File

@@ -24,6 +24,12 @@ type Hooks struct {
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
// Get session cookie and basic auth // Get session cookie and basic auth
cookie, err := hooks.Auth.GetSessionCookie(c) cookie, err := hooks.Auth.GetSessionCookie(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get session cookie")
return types.UserContext{}
}
basic := hooks.Auth.GetBasicAuth(c) basic := hooks.Auth.GetBasicAuth(c)
// Check if basic auth is set // Check if basic auth is set
@@ -46,19 +52,6 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
} }
// Check cookie error after basic auth
if err != nil {
log.Error().Err(err).Msg("Failed to get session cookie")
// Return empty context
return types.UserContext{
Username: "",
IsLoggedIn: false,
OAuth: false,
Provider: "",
TotpPending: false,
}
}
// Check if session cookie has totp pending // Check if session cookie has totp pending
if cookie.TotpPending { if cookie.TotpPending {
log.Debug().Msg("Totp pending") log.Debug().Msg("Totp pending")

View File

@@ -14,10 +14,10 @@ func NewOAuth(config oauth2.Config) *OAuth {
} }
type OAuth struct { type OAuth struct {
Config oauth2.Config Verifier string
Context context.Context Context context.Context
Token *oauth2.Token Token *oauth2.Token
Verifier string Config oauth2.Config
} }
func (oauth *OAuth) Init() { func (oauth *OAuth) Init() {

View File

@@ -17,10 +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
Generic *oauth.OAuth Generic *oauth.OAuth
Config types.OAuthConfig
} }
func (providers *Providers) Init() { func (providers *Providers) Init() {
@@ -58,6 +59,22 @@ func (providers *Providers) Init() {
providers.Google.Init() providers.Google.Init()
} }
if providers.Config.TailscaleClientId != "" && providers.Config.TailscaleClientSecret != "" {
log.Info().Msg("Initializing Tailscale OAuth")
// Create a new oauth provider with the tailscale config
providers.Tailscale = oauth.NewOAuth(oauth2.Config{
ClientID: providers.Config.TailscaleClientId,
ClientSecret: providers.Config.TailscaleClientSecret,
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/tailscale", providers.Config.AppURL),
Scopes: TailscaleScopes(),
Endpoint: TailscaleEndpoint,
})
// Initialize the oauth provider
providers.Tailscale.Init()
}
// If we have a client id and secret for generic oauth, initialize the oauth provider // If we have a client id and secret for generic oauth, initialize the oauth provider
if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" { if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
log.Info().Msg("Initializing Generic OAuth") log.Info().Msg("Initializing Generic OAuth")
@@ -86,6 +103,8 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
return providers.Github return providers.Github
case "google": case "google":
return providers.Google return providers.Google
case "tailscale":
return providers.Tailscale
case "generic": case "generic":
return providers.Generic return providers.Generic
default: default:
@@ -142,6 +161,30 @@ func (providers *Providers) GetUser(provider string) (string, error) {
log.Debug().Msg("Got email from google") log.Debug().Msg("Got email from google")
// Return the email
return email, nil
case "tailscale":
// If the tailscale provider is not configured, return an error
if providers.Tailscale == nil {
log.Debug().Msg("Tailscale provider not configured")
return "", nil
}
// Get the client from the tailscale provider
client := providers.Tailscale.GetClient()
log.Debug().Msg("Got client from tailscale")
// Get the email from the tailscale provider
email, err := GetTailscaleEmail(client)
// Check if there was an error
if err != nil {
return "", err
}
log.Debug().Msg("Got email from tailscale")
// Return the email // Return the email
return email, nil return email, nil
case "generic": case "generic":
@@ -182,6 +225,9 @@ func (provider *Providers) GetConfiguredProviders() []string {
if provider.Google != nil { if provider.Google != nil {
providers = append(providers, "google") providers = append(providers, "google")
} }
if provider.Tailscale != nil {
providers = append(providers, "tailscale")
}
if provider.Generic != nil { if provider.Generic != nil {
providers = append(providers, "generic") providers = append(providers, "generic")
} }

View File

@@ -0,0 +1,68 @@
package providers
import (
"encoding/json"
"io"
"net/http"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"
)
// The tailscale email is the loginName
type TailscaleUser struct {
LoginName string `json:"loginName"`
}
// The response from the tailscale user info endpoint
type TailscaleUserInfoResponse struct {
Users []TailscaleUser `json:"users"`
}
// The scopes required for the tailscale provider
func TailscaleScopes() []string {
return []string{"users:read"}
}
// The tailscale endpoint
var TailscaleEndpoint = oauth2.Endpoint{
TokenURL: "https://api.tailscale.com/api/v2/oauth/token",
}
func GetTailscaleEmail(client *http.Client) (string, error) {
// Get the user info from tailscale using the oauth http client
res, err := client.Get("https://api.tailscale.com/api/v2/tailnet/-/users")
// Check if there was an error
if err != nil {
return "", err
}
log.Debug().Msg("Got response from tailscale")
// Read the body of the response
body, err := io.ReadAll(res.Body)
// Check if there was an error
if err != nil {
return "", err
}
log.Debug().Msg("Read body from tailscale")
// Parse the body into a user struct
var users TailscaleUserInfoResponse
// Unmarshal the body into the user struct
err = json.Unmarshal(body, &users)
// Check if there was an error
if err != nil {
return "", err
}
log.Debug().Msg("Parsed users from tailscale")
// Return the email of the first user
return users.Users[0].LoginName, nil
}

View File

@@ -22,6 +22,11 @@ type UnauthorizedQuery struct {
Resource string `url:"resource"` Resource string `url:"resource"`
} }
// TailscaleQuery is the query parameters for the tailscale endpoint
type TailscaleQuery struct {
Code int `url:"code"`
}
// Proxy is the uri parameters for the proxy endpoint // Proxy is the uri parameters for the proxy endpoint
type Proxy struct { type Proxy struct {
Proxy string `uri:"proxy" binding:"required"` Proxy string `uri:"proxy" binding:"required"`
@@ -38,16 +43,6 @@ type UserContextResponse struct {
TotpPending bool `json:"totpPending"` TotpPending bool `json:"totpPending"`
} }
// App Context is the response for the app context endpoint
type AppContext struct {
Status int `json:"status"`
Message string `json:"message"`
ConfiguredProviders []string `json:"configuredProviders"`
DisableContinue bool `json:"disableContinue"`
Title string `json:"title"`
GenericName string `json:"genericName"`
}
// Totp request is the request for the totp endpoint // Totp request is the request for the totp endpoint
type TotpRequest struct { type TotpRequest struct {
Code string `json:"code"` Code string `json:"code"`

View File

@@ -2,59 +2,37 @@ package types
// Config is the configuration for the tinyauth server // Config is the configuration for the tinyauth server
type Config struct { type Config struct {
Port int `mapstructure:"port" validate:"required"` Port int `mapstructure:"port" validate:"required"`
Address string `validate:"required,ip4_addr" mapstructure:"address"` Address string `validate:"required,ip4_addr" mapstructure:"address"`
Secret string `validate:"required,len=32" mapstructure:"secret"` Secret string `validate:"required,len=32" mapstructure:"secret"`
SecretFile string `mapstructure:"secret-file"` SecretFile string `mapstructure:"secret-file"`
AppURL string `validate:"required,url" mapstructure:"app-url"` AppURL string `validate:"required,url" mapstructure:"app-url"`
Users string `mapstructure:"users"` Users string `mapstructure:"users"`
UsersFile string `mapstructure:"users-file"` UsersFile string `mapstructure:"users-file"`
CookieSecure bool `mapstructure:"cookie-secure"` CookieSecure bool `mapstructure:"cookie-secure"`
GithubClientId string `mapstructure:"github-client-id"` GithubClientId string `mapstructure:"github-client-id"`
GithubClientSecret string `mapstructure:"github-client-secret"` GithubClientSecret string `mapstructure:"github-client-secret"`
GithubClientSecretFile string `mapstructure:"github-client-secret-file"` GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
GoogleClientId string `mapstructure:"google-client-id"` GoogleClientId string `mapstructure:"google-client-id"`
GoogleClientSecret string `mapstructure:"google-client-secret"` GoogleClientSecret string `mapstructure:"google-client-secret"`
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"` GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
GenericClientId string `mapstructure:"generic-client-id"` TailscaleClientId string `mapstructure:"tailscale-client-id"`
GenericClientSecret string `mapstructure:"generic-client-secret"` TailscaleClientSecret string `mapstructure:"tailscale-client-secret"`
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"` TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"`
GenericScopes string `mapstructure:"generic-scopes"` GenericClientId string `mapstructure:"generic-client-id"`
GenericAuthURL string `mapstructure:"generic-auth-url"` GenericClientSecret string `mapstructure:"generic-client-secret"`
GenericTokenURL string `mapstructure:"generic-token-url"` GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
GenericUserURL string `mapstructure:"generic-user-url"` GenericScopes string `mapstructure:"generic-scopes"`
GenericName string `mapstructure:"generic-name"` GenericAuthURL string `mapstructure:"generic-auth-url"`
DisableContinue bool `mapstructure:"disable-continue"` GenericTokenURL string `mapstructure:"generic-token-url"`
OAuthWhitelist string `mapstructure:"oauth-whitelist"` GenericUserURL string `mapstructure:"generic-user-url"`
SessionExpiry int `mapstructure:"session-expiry"` GenericName string `mapstructure:"generic-name"`
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"` DisableContinue bool `mapstructure:"disable-continue"`
Title string `mapstructure:"app-title"` OAuthWhitelist string `mapstructure:"oauth-whitelist"`
EnvFile string `mapstructure:"env-file"` SessionExpiry int `mapstructure:"session-expiry"`
LoginTimeout int `mapstructure:"login-timeout"` LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
LoginMaxRetries int `mapstructure:"login-max-retries"` Title string `mapstructure:"app-title"`
} EnvFile string `mapstructure:"env-file"`
// Server configuration
type HandlersConfig struct {
AppURL string
DisableContinue bool
GenericName string
Title string
}
// OAuthConfig is the configuration for the providers
type OAuthConfig struct {
GithubClientId string
GithubClientSecret string
GoogleClientId string
GoogleClientSecret string
GenericClientId string
GenericClientSecret string
GenericScopes []string
GenericAuthURL string
GenericTokenURL string
GenericUserURL string
AppURL string
} }
// APIConfig is the configuration for the API // APIConfig is the configuration for the API
@@ -63,14 +41,39 @@ type APIConfig struct {
Address string Address string
} }
// AuthConfig is the configuration for the auth service // OAuthConfig is the configuration for the providers
type AuthConfig struct { type OAuthConfig struct {
Users Users GithubClientId string
OauthWhitelist []string GithubClientSecret string
SessionExpiry int GoogleClientId string
Secret string GoogleClientSecret string
CookieSecure bool TailscaleClientId string
Domain string TailscaleClientSecret string
LoginTimeout int GenericClientId string
LoginMaxRetries int GenericClientSecret string
GenericScopes []string
GenericAuthURL string
GenericTokenURL string
GenericUserURL string
AppURL string
}
// Server configuration
type HandlersConfig struct {
AppURL string
Domain string
CookieSecure bool
DisableContinue bool
GenericName string
Title string
}
// Auth configuration
type AuthConfig struct {
Domain string
Secret string
CookieSecure bool
SessionExpiry int
Users Users
OAuthWhitelist []string
} }

View File

@@ -1,9 +1,6 @@
package types package types
import ( import "tinyauth/internal/oauth"
"time"
"tinyauth/internal/oauth"
)
// User is the struct for a user // User is the struct for a user
type User struct { type User struct {
@@ -27,7 +24,6 @@ type SessionCookie struct {
Username string Username string
Provider string Provider string
TotpPending bool TotpPending bool
RedirectURI string
} }
// TinyauthLabels is the labels for the tinyauth container // TinyauthLabels is the labels for the tinyauth container
@@ -47,9 +43,12 @@ type UserContext struct {
TotpPending bool TotpPending bool
} }
// LoginAttempt tracks information about login attempts for rate limiting // App Context is the response for the app context endpoint
type LoginAttempt struct { type AppContext struct {
FailedAttempts int Status int `json:"status"`
LastAttempt time.Time Message string `json:"message"`
LockedUntil time.Time ConfiguredProviders []string `json:"configuredProviders"`
DisableContinue bool `json:"disableContinue"`
Title string `json:"title"`
GenericName string `json:"genericName"`
} }

View File

@@ -130,7 +130,7 @@ func GetSecret(conf string, file string) string {
} }
// Return the contents of the file // Return the contents of the file
return ParseSecretFile(contents) return contents
} }
// Get the users from the config or file // Get the users from the config or file
@@ -213,7 +213,7 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
// Check if any of the OAuth providers are configured based on the client id and secret // Check if any of the OAuth providers are configured based on the client id and secret
func OAuthConfigured(config types.Config) bool { func OAuthConfigured(config types.Config) bool {
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "") return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "") || (config.TailscaleClientId != "" && config.TailscaleClientSecret != "")
} }
// Filter helper function // Filter helper function
@@ -241,21 +241,23 @@ func ParseUser(user string) (types.User, error) {
return types.User{}, errors.New("invalid user format") return types.User{}, errors.New("invalid user format")
} }
// Check for empty strings
for _, userPart := range userSplit {
if strings.TrimSpace(userPart) == "" {
return types.User{}, errors.New("invalid user format")
}
}
// Check if the user has a totp secret // Check if the user has a totp secret
if len(userSplit) == 2 { if len(userSplit) == 2 {
// Check for empty username or password
if userSplit[1] == "" || userSplit[0] == "" {
return types.User{}, errors.New("invalid user format")
}
return types.User{ return types.User{
Username: userSplit[0], Username: userSplit[0],
Password: userSplit[1], Password: userSplit[1],
}, nil }, nil
} }
// Check for empty username, password or totp secret
if userSplit[2] == "" || userSplit[1] == "" || userSplit[0] == "" {
return types.User{}, errors.New("invalid user format")
}
// Return the user struct // Return the user struct
return types.User{ return types.User{
Username: userSplit[0], Username: userSplit[0],
@@ -263,23 +265,3 @@ func ParseUser(user string) (types.User, error) {
TotpSecret: userSplit[2], TotpSecret: userSplit[2],
}, nil }, nil
} }
// Parse secret file
func ParseSecretFile(contents string) string {
// Split to lines
lines := strings.Split(contents, "\n")
// Loop through the lines
for _, line := range lines {
// Check if the line is empty
if strings.TrimSpace(line) == "" {
continue
}
// Return the line
return strings.TrimSpace(line)
}
// Return an empty string
return ""
}

View File

@@ -1,7 +1,6 @@
package utils_test package utils_test
import ( import (
"fmt"
"os" "os"
"reflect" "reflect"
"testing" "testing"
@@ -124,7 +123,7 @@ func TestGetSecret(t *testing.T) {
expected := "test" expected := "test"
// Create file // Create file
err := os.WriteFile(file, []byte(fmt.Sprintf("\n\n \n\n\n %s \n\n \n ", expected)), 0644) err := os.WriteFile(file, []byte(expected), 0644)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {