Compare commits
1 Commits
v3.2.0
...
feat/swagg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07b57fb0ca |
@@ -26,7 +26,5 @@ DISABLE_CONTINUE=false
|
|||||||
OAUTH_WHITELIST=
|
OAUTH_WHITELIST=
|
||||||
GENERIC_NAME=My OAuth
|
GENERIC_NAME=My OAuth
|
||||||
SESSION_EXPIRY=7200
|
SESSION_EXPIRY=7200
|
||||||
LOGIN_TIMEOUT=300
|
|
||||||
LOGIN_MAX_RETRIES=5
|
|
||||||
LOG_LEVEL=0
|
LOG_LEVEL=0
|
||||||
APP_TITLE=Tinyauth SSO
|
APP_TITLE=Tinyauth SSO
|
||||||
6
.github/workflows/ci.yml
vendored
@@ -26,17 +26,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
cd frontend
|
cd site
|
||||||
bun install
|
bun install
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: |
|
run: |
|
||||||
cd frontend
|
cd site
|
||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
- name: Copy frontend
|
- name: Copy frontend
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r site/dist internal/assets/dist
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|||||||
111
.github/workflows/release.yml
vendored
@@ -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
|
||||||
@@ -203,29 +125,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}},prefix=v
|
type=semver,pattern=v{{version}}
|
||||||
type=semver,pattern={{major}},prefix=v
|
type=semver,pattern=v{{major}}
|
||||||
type=semver,pattern={{major}}.{{minor}},prefix=v
|
type=semver,pattern=v{{major}}.{{minor}}
|
||||||
|
|
||||||
- name: Create manifest list and push
|
- name: Create manifest list and push
|
||||||
working-directory: ${{ runner.temp }}/digests
|
working-directory: ${{ runner.temp }}/digests
|
||||||
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/*
|
|
||||||
|
|||||||
98
.github/workflows/translations.yml
vendored
@@ -1,98 +0,0 @@
|
|||||||
name: Publish translations
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- i18n_v*
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: pages
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
get-branches:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
i18n-branches: ${{ steps.get-branches.outputs.result }}
|
|
||||||
steps:
|
|
||||||
- name: Get branches
|
|
||||||
id: get-branches
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { data: repos } = await github.rest.repos.listBranches({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
})
|
|
||||||
|
|
||||||
const i18nBranches = repos.filter((branch) => branch.name.startsWith("i18n_v"))
|
|
||||||
const i18nBranchNames = i18nBranches.map((branch) => branch.name)
|
|
||||||
|
|
||||||
return i18nBranchNames
|
|
||||||
|
|
||||||
get-translations:
|
|
||||||
needs: get-branches
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
branch: ${{ fromJson(needs.get-branches.outputs.i18n-branches) }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ matrix.branch }}
|
|
||||||
|
|
||||||
- name: Get translation version
|
|
||||||
id: get-version
|
|
||||||
run: |
|
|
||||||
branch=${{ matrix.branch }}
|
|
||||||
version=${branch#i18n_}
|
|
||||||
echo "version=$version" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Upload translations
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ steps.get-version.outputs.version }}
|
|
||||||
path: frontend/src/lib/i18n/locales
|
|
||||||
|
|
||||||
build:
|
|
||||||
needs: get-translations
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Pages
|
|
||||||
uses: actions/configure-pages@v4
|
|
||||||
|
|
||||||
- name: Prepare output directory
|
|
||||||
run: |
|
|
||||||
mkdir -p dist/i18n/
|
|
||||||
|
|
||||||
- name: Download translations
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: dist/i18n/
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: dist
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Deploy
|
|
||||||
steps:
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
@@ -20,22 +20,22 @@ cd tinyauth
|
|||||||
|
|
||||||
## Install requirements
|
## Install requirements
|
||||||
|
|
||||||
Although you will not need the requirements in your machine since the development will happen in docker, I still recommend to install them because this way you will not have import errors, to install the go requirements, run:
|
Although you will not need the requirements in your machine since the development will happen in docker, I still recommend to install them because this way you will not have errors, to install the go requirements, run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go mod tidy
|
go mod tidy
|
||||||
```
|
```
|
||||||
|
|
||||||
You also need to download the frontend dependencies, this can be done like so:
|
You also need to download the frontend requirements, this can be done like so:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd frontend/
|
cd site/
|
||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create your `.env` file
|
## Create your `.env` file
|
||||||
|
|
||||||
In order to configure the app you need to create an environment file, this can be done by copying the `.env.example` file to `.env` and modifying the environment variables inside to suit your needs.
|
In order to ocnfigure the app you need to create an environment file, this can be done by copying the `.env.example` file to `.env` and modifying the environment variables inside to suit your needs.
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
|
|||||||
33
Dockerfile
@@ -1,22 +1,22 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM oven/bun:1.1.45-alpine AS frontend-builder
|
FROM oven/bun:1.1.45-alpine AS site-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /site
|
||||||
|
|
||||||
COPY ./frontend/package.json ./
|
COPY ./site/package.json ./
|
||||||
COPY ./frontend/bun.lockb ./
|
COPY ./site/bun.lockb ./
|
||||||
|
|
||||||
RUN bun install
|
RUN bun install
|
||||||
|
|
||||||
COPY ./frontend/public ./public
|
COPY ./site/public ./public
|
||||||
COPY ./frontend/src ./src
|
COPY ./site/src ./src
|
||||||
COPY ./frontend/eslint.config.js ./
|
COPY ./site/eslint.config.js ./
|
||||||
COPY ./frontend/index.html ./
|
COPY ./site/index.html ./
|
||||||
COPY ./frontend/tsconfig.json ./
|
COPY ./site/tsconfig.json ./
|
||||||
COPY ./frontend/tsconfig.app.json ./
|
COPY ./site/tsconfig.app.json ./
|
||||||
COPY ./frontend/tsconfig.node.json ./
|
COPY ./site/tsconfig.node.json ./
|
||||||
COPY ./frontend/vite.config.ts ./
|
COPY ./site/vite.config.ts ./
|
||||||
COPY ./frontend/postcss.config.cjs ./
|
COPY ./site/postcss.config.cjs ./
|
||||||
|
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ RUN go mod download
|
|||||||
COPY ./main.go ./
|
COPY ./main.go ./
|
||||||
COPY ./cmd ./cmd
|
COPY ./cmd ./cmd
|
||||||
COPY ./internal ./internal
|
COPY ./internal ./internal
|
||||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
COPY --from=site-builder /site/dist ./internal/assets/dist
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w"
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w"
|
||||||
|
|
||||||
@@ -42,13 +42,8 @@ FROM alpine:3.21 AS runner
|
|||||||
|
|
||||||
WORKDIR /tinyauth
|
WORKDIR /tinyauth
|
||||||
|
|
||||||
RUN apk add --no-cache curl
|
|
||||||
|
|
||||||
COPY --from=builder /tinyauth/tinyauth ./
|
COPY --from=builder /tinyauth/tinyauth ./
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=5s \
|
|
||||||
CMD curl -f http://localhost:3000/api/healthcheck || exit 1
|
|
||||||
|
|
||||||
ENTRYPOINT ["./tinyauth"]
|
ENTRYPOINT ["./tinyauth"]
|
||||||
@@ -9,6 +9,7 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY ./cmd ./cmd
|
COPY ./cmd ./cmd
|
||||||
COPY ./internal ./internal
|
COPY ./internal ./internal
|
||||||
|
COPY ./docs ./docs
|
||||||
COPY ./main.go ./
|
COPY ./main.go ./
|
||||||
COPY ./air.toml ./
|
COPY ./air.toml ./
|
||||||
|
|
||||||
|
|||||||
13
README.md
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="Tinyauth" title="Tinyauth" width="256" src="frontend/public/logo.png">
|
<img alt="Tinyauth" title="Tinyauth" width="256" src="site/public/logo.png">
|
||||||
<h1>Tinyauth</h1>
|
<h1>Tinyauth</h1>
|
||||||
<p>The easiest way to secure your apps with a login screen.</p>
|
<p>The easiest way to secure your apps with a login screen.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -10,15 +10,12 @@
|
|||||||
<img alt="Commit activity" src="https://img.shields.io/github/commit-activity/w/steveiliop56/tinyauth">
|
<img alt="Commit activity" src="https://img.shields.io/github/commit-activity/w/steveiliop56/tinyauth">
|
||||||
<img alt="Issues" src="https://img.shields.io/github/issues/steveiliop56/tinyauth">
|
<img alt="Issues" src="https://img.shields.io/github/issues/steveiliop56/tinyauth">
|
||||||
<img alt="Tinyauth CI" src="https://github.com/steveiliop56/tinyauth/actions/workflows/ci.yml/badge.svg">
|
<img alt="Tinyauth CI" src="https://github.com/steveiliop56/tinyauth/actions/workflows/ci.yml/badge.svg">
|
||||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/tinyauth"><img src="https://badges.crowdin.net/tinyauth/localized.svg"></a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
|
Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
||||||
|
|
||||||
@@ -41,10 +38,6 @@ You can find documentation and guides on all available configuration of tinyauth
|
|||||||
|
|
||||||
All contributions to the codebase are welcome! If you have any recommendations on how to improve security or find a security issue in tinyauth please open an issue or pull request so it can be fixed as soon as possible!
|
All contributions to the codebase are welcome! If you have any recommendations on how to improve security or find a security issue in tinyauth please open an issue or pull request so it can be fixed as soon as possible!
|
||||||
|
|
||||||
## Localization
|
|
||||||
|
|
||||||
If you would like to help translating the project in more languages you can do so by visiting the [Crowdin](https://crowdin.com/project/tinyauth) page.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. For more information about the license check the [license](./LICENSE) file.
|
Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. For more information about the license check the [license](./LICENSE) file.
|
||||||
@@ -63,7 +56,3 @@ Credits for the logo of this app go to:
|
|||||||
|
|
||||||
- **Freepik** for providing the police hat and badge.
|
- **Freepik** for providing the police hat and badge.
|
||||||
- **Renee French** for the original gopher logo.
|
- **Renee French** for the original gopher logo.
|
||||||
|
|
||||||
## Star History
|
|
||||||
|
|
||||||
[](https://www.star-history.com/#steveiliop56/tinyauth&Date)
|
|
||||||
|
|||||||
1
air.toml
@@ -2,6 +2,7 @@ root = "/tinyauth"
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
|
pre_cmd = ["go mod tidy"]
|
||||||
cmd = "go build -o ./tmp/tinyauth ."
|
cmd = "go build -o ./tmp/tinyauth ."
|
||||||
bin = "tmp/tinyauth"
|
bin = "tmp/tinyauth"
|
||||||
include_ext = ["go"]
|
include_ext = ["go"]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"timestamp": "2025-03-10T19:00:00.000Z",
|
"timestamp": "2025-03-10T19:00:00.000Z",
|
||||||
"thumbnail": {
|
"thumbnail": {
|
||||||
"url": "https://github.com/steveiliop56/tinyauth/blob/main/frontend/public/logo.png?raw=true"
|
"url": "https://github.com/steveiliop56/tinyauth/blob/main/site/public/logo.png?raw=true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
BIN
assets/login.png
|
Before Width: | Height: | Size: 93 KiB |
87
cmd/root.go
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -34,8 +35,8 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
// Get config
|
// Get config
|
||||||
var config types.Config
|
var config types.Config
|
||||||
err := viper.Unmarshal(&config)
|
parseErr := viper.Unmarshal(&config)
|
||||||
HandleError(err, "Failed to parse config")
|
HandleError(parseErr, "Failed to parse config")
|
||||||
|
|
||||||
// Secrets
|
// Secrets
|
||||||
config.Secret = utils.GetSecret(config.Secret, config.SecretFile)
|
config.Secret = utils.GetSecret(config.Secret, config.SecretFile)
|
||||||
@@ -46,8 +47,8 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
// Validate config
|
// Validate config
|
||||||
validator := validator.New()
|
validator := validator.New()
|
||||||
err = validator.Struct(config)
|
validateErr := validator.Struct(config)
|
||||||
HandleError(err, "Failed to validate config")
|
HandleError(validateErr, "Failed to validate config")
|
||||||
|
|
||||||
// Logger
|
// Logger
|
||||||
log.Logger = log.Level(zerolog.Level(config.LogLevel))
|
log.Logger = log.Level(zerolog.Level(config.LogLevel))
|
||||||
@@ -55,8 +56,9 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
// Users
|
// Users
|
||||||
log.Info().Msg("Parsing users")
|
log.Info().Msg("Parsing users")
|
||||||
users, err := utils.GetUsers(config.Users, config.UsersFile)
|
users, usersErr := utils.GetUsers(config.Users, config.UsersFile)
|
||||||
HandleError(err, "Failed to parse users")
|
|
||||||
|
HandleError(usersErr, "Failed to parse users")
|
||||||
|
|
||||||
if len(users) == 0 && !utils.OAuthConfigured(config) {
|
if len(users) == 0 && !utils.OAuthConfigured(config) {
|
||||||
HandleError(errors.New("no users or OAuth configured"), "No users or OAuth configured")
|
HandleError(errors.New("no users or OAuth configured"), "No users or OAuth configured")
|
||||||
@@ -66,15 +68,8 @@ var rootCmd = &cobra.Command{
|
|||||||
oauthWhitelist := utils.Filter(strings.Split(config.OAuthWhitelist, ","), func(val string) bool {
|
oauthWhitelist := utils.Filter(strings.Split(config.OAuthWhitelist, ","), func(val string) bool {
|
||||||
return val != ""
|
return val != ""
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Debug().Msg("Parsed OAuth whitelist")
|
log.Debug().Msg("Parsed OAuth whitelist")
|
||||||
|
|
||||||
// Get domain
|
|
||||||
log.Debug().Msg("Getting domain")
|
|
||||||
domain, err := utils.GetUpperDomain(config.AppURL)
|
|
||||||
HandleError(err, "Failed to get upper domain")
|
|
||||||
log.Info().Str("domain", domain).Msg("Using domain for cookie store")
|
|
||||||
|
|
||||||
// Create OAuth config
|
// Create OAuth config
|
||||||
oauthConfig := types.OAuthConfig{
|
oauthConfig := types.OAuthConfig{
|
||||||
GithubClientId: config.GithubClientId,
|
GithubClientId: config.GithubClientId,
|
||||||
@@ -92,41 +87,17 @@ var rootCmd = &cobra.Command{
|
|||||||
AppURL: config.AppURL,
|
AppURL: config.AppURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create handlers config
|
log.Debug().Msg("Parsed OAuth config")
|
||||||
handlersConfig := types.HandlersConfig{
|
|
||||||
AppURL: config.AppURL,
|
|
||||||
DisableContinue: config.DisableContinue,
|
|
||||||
Title: config.Title,
|
|
||||||
GenericName: config.GenericName,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create api config
|
|
||||||
apiConfig := types.APIConfig{
|
|
||||||
Port: config.Port,
|
|
||||||
Address: config.Address,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create auth config
|
|
||||||
authConfig := types.AuthConfig{
|
|
||||||
Users: users,
|
|
||||||
OauthWhitelist: oauthWhitelist,
|
|
||||||
Secret: config.Secret,
|
|
||||||
CookieSecure: config.CookieSecure,
|
|
||||||
SessionExpiry: config.SessionExpiry,
|
|
||||||
Domain: domain,
|
|
||||||
LoginTimeout: config.LoginTimeout,
|
|
||||||
LoginMaxRetries: config.LoginMaxRetries,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create docker service
|
// Create docker service
|
||||||
docker := docker.NewDocker()
|
docker := docker.NewDocker()
|
||||||
|
|
||||||
// Initialize docker
|
// Initialize docker
|
||||||
err = docker.Init()
|
dockerErr := docker.Init()
|
||||||
HandleError(err, "Failed to initialize docker")
|
HandleError(dockerErr, "Failed to initialize docker")
|
||||||
|
|
||||||
// Create auth service
|
// Create auth service
|
||||||
auth := auth.NewAuth(authConfig, docker)
|
auth := auth.NewAuth(docker, users, oauthWhitelist, config.SessionExpiry)
|
||||||
|
|
||||||
// Create OAuth providers service
|
// Create OAuth providers service
|
||||||
providers := providers.NewProviders(oauthConfig)
|
providers := providers.NewProviders(oauthConfig)
|
||||||
@@ -137,11 +108,35 @@ var rootCmd = &cobra.Command{
|
|||||||
// Create hooks service
|
// Create hooks service
|
||||||
hooks := hooks.NewHooks(auth, providers)
|
hooks := hooks.NewHooks(auth, providers)
|
||||||
|
|
||||||
|
// Create doman
|
||||||
|
domain, domainErr := utils.GetRootURL(config.AppURL)
|
||||||
|
|
||||||
|
if domainErr != nil {
|
||||||
|
log.Fatal().Err(domainErr).Msg("Failed to get domain")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("domain", domain).Msg("Using domain for cookies")
|
||||||
|
|
||||||
|
// Create api config
|
||||||
|
apiConfig := types.APIConfig{
|
||||||
|
Port: config.Port,
|
||||||
|
Address: config.Address,
|
||||||
|
Secret: config.Secret,
|
||||||
|
AppURL: config.AppURL,
|
||||||
|
CookieSecure: config.CookieSecure,
|
||||||
|
DisableContinue: config.DisableContinue,
|
||||||
|
SessionExpiry: config.SessionExpiry,
|
||||||
|
Title: config.Title,
|
||||||
|
GenericName: config.GenericName,
|
||||||
|
Domain: fmt.Sprintf(".%s", domain),
|
||||||
|
}
|
||||||
|
|
||||||
// Create handlers
|
// Create handlers
|
||||||
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
apiHandlers := handlers.NewHandlers(apiConfig, auth, hooks)
|
||||||
|
|
||||||
// Create API
|
// Create API
|
||||||
api := api.NewAPI(apiConfig, handlers)
|
api := api.NewAPI(apiConfig, hooks, auth, providers, apiHandlers)
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
api.Init()
|
api.Init()
|
||||||
@@ -158,7 +153,7 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleError(err error, msg string) {
|
func HandleError(err error, msg string) {
|
||||||
// If error, log it and exit
|
// If error log it and exit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg(msg)
|
log.Fatal().Err(err).Msg(msg)
|
||||||
}
|
}
|
||||||
@@ -203,8 +198,6 @@ func init() {
|
|||||||
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
||||||
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
||||||
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
|
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
|
||||||
rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).")
|
|
||||||
rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).")
|
|
||||||
rootCmd.Flags().Int("log-level", 1, "Log level.")
|
rootCmd.Flags().Int("log-level", 1, "Log level.")
|
||||||
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
|
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
|
||||||
|
|
||||||
@@ -239,8 +232,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())
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
// Interactive flag
|
// Interactive flag
|
||||||
var interactive bool
|
var interactive bool
|
||||||
|
|
||||||
// Input user
|
// i stands for input
|
||||||
var iUser string
|
var iUser string
|
||||||
|
|
||||||
var GenerateCmd = &cobra.Command{
|
var GenerateCmd = &cobra.Command{
|
||||||
@@ -46,18 +46,18 @@ var GenerateCmd = &cobra.Command{
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Run form
|
// Run form
|
||||||
err := form.WithTheme(baseTheme).Run()
|
formErr := form.WithTheme(baseTheme).Run()
|
||||||
|
|
||||||
if err != nil {
|
if formErr != nil {
|
||||||
log.Fatal().Err(err).Msg("Form failed")
|
log.Fatal().Err(formErr).Msg("Form failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse user
|
// Parse user
|
||||||
user, err := utils.ParseUser(iUser)
|
user, parseErr := utils.ParseUser(iUser)
|
||||||
|
|
||||||
if err != nil {
|
if parseErr != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to parse user")
|
log.Fatal().Err(parseErr).Msg("Failed to parse user")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user was using docker escape
|
// Check if user was using docker escape
|
||||||
@@ -73,13 +73,13 @@ var GenerateCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate totp secret
|
// Generate totp secret
|
||||||
key, err := totp.Generate(totp.GenerateOpts{
|
key, keyErr := totp.Generate(totp.GenerateOpts{
|
||||||
Issuer: "Tinyauth",
|
Issuer: "Tinyauth",
|
||||||
AccountName: user.Username,
|
AccountName: user.Username,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if keyErr != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to generate totp secret")
|
log.Fatal().Err(keyErr).Msg("Failed to generate totp secret")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create secret
|
// Create secret
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interactive flag
|
|
||||||
var interactive bool
|
var interactive bool
|
||||||
|
|
||||||
// Docker flag
|
|
||||||
var docker bool
|
var docker bool
|
||||||
|
|
||||||
// i stands for input
|
// i stands for input
|
||||||
@@ -54,10 +51,10 @@ var CreateCmd = &cobra.Command{
|
|||||||
// Use simple theme
|
// Use simple theme
|
||||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||||
|
|
||||||
err := form.WithTheme(baseTheme).Run()
|
formErr := form.WithTheme(baseTheme).Run()
|
||||||
|
|
||||||
if err != nil {
|
if formErr != nil {
|
||||||
log.Fatal().Err(err).Msg("Form failed")
|
log.Fatal().Err(formErr).Msg("Form failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,10 +66,10 @@ var CreateCmd = &cobra.Command{
|
|||||||
log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user")
|
log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user")
|
||||||
|
|
||||||
// Hash password
|
// Hash password
|
||||||
password, err := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost)
|
password, passwordErr := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost)
|
||||||
|
|
||||||
if err != nil {
|
if passwordErr != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to hash password")
|
log.Fatal().Err(passwordErr).Msg("Failed to hash password")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert password to string
|
// Convert password to string
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interactive flag
|
|
||||||
var interactive bool
|
var interactive bool
|
||||||
|
|
||||||
// Docker flag
|
|
||||||
var docker bool
|
var docker bool
|
||||||
|
|
||||||
// i stands for input
|
// i stands for input
|
||||||
@@ -63,18 +60,18 @@ var VerifyCmd = &cobra.Command{
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Run form
|
// Run form
|
||||||
err := form.WithTheme(baseTheme).Run()
|
formErr := form.WithTheme(baseTheme).Run()
|
||||||
|
|
||||||
if err != nil {
|
if formErr != nil {
|
||||||
log.Fatal().Err(err).Msg("Form failed")
|
log.Fatal().Err(formErr).Msg("Form failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse user
|
// Parse user
|
||||||
user, err := utils.ParseUser(iUser)
|
user, userErr := utils.ParseUser(iUser)
|
||||||
|
|
||||||
if err != nil {
|
if userErr != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to parse user")
|
log.Fatal().Err(userErr).Msg("Failed to parse user")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare username
|
// Compare username
|
||||||
@@ -83,9 +80,9 @@ var VerifyCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compare password
|
// Compare password
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword))
|
verifyErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword))
|
||||||
|
|
||||||
if err != nil {
|
if verifyErr != nil {
|
||||||
log.Fatal().Msg("Ppassword is incorrect")
|
log.Fatal().Msg("Ppassword is incorrect")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +96,9 @@ var VerifyCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check totp code
|
// Check totp code
|
||||||
ok := totp.Validate(iTotp, user.TotpSecret)
|
totpOk := totp.Validate(iTotp, user.TotpSecret)
|
||||||
|
|
||||||
if !ok {
|
if !totpOk {
|
||||||
log.Fatal().Msg("Totp code incorrect")
|
log.Fatal().Msg("Totp code incorrect")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
12
crowdin.yml
@@ -1,12 +0,0 @@
|
|||||||
"base_path": "."
|
|
||||||
"base_url": "https://api.crowdin.com"
|
|
||||||
|
|
||||||
"preserve_hierarchy": true
|
|
||||||
|
|
||||||
files:
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"source": "/frontend/src/lib/i18n/locales/en.json",
|
|
||||||
"translation": "/frontend/src/lib/i18n/locales/%locale%.json",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -14,20 +14,22 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
|
traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
|
||||||
|
traefik.http.services.nginx.loadbalancer.server.port: 80
|
||||||
traefik.http.routers.nginx.middlewares: tinyauth
|
traefik.http.routers.nginx.middlewares: tinyauth
|
||||||
|
|
||||||
tinyauth-frontend:
|
tinyauth-frontend:
|
||||||
container_name: tinyauth-frontend
|
container_name: tinyauth-frontend
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: frontend/Dockerfile.dev
|
dockerfile: site/Dockerfile.dev
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend/src:/frontend/src
|
- ./site/src:/site/src
|
||||||
ports:
|
ports:
|
||||||
- 5173:5173
|
- 5173:5173
|
||||||
labels:
|
labels:
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
|
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
|
||||||
|
traefik.http.services.tinyauth.loadbalancer.server.port: 5173
|
||||||
|
|
||||||
tinyauth-backend:
|
tinyauth-backend:
|
||||||
container_name: tinyauth-backend
|
container_name: tinyauth-backend
|
||||||
@@ -38,8 +40,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./internal:/tinyauth/internal
|
- ./internal:/tinyauth/internal
|
||||||
- ./cmd:/tinyauth/cmd
|
- ./cmd:/tinyauth/cmd
|
||||||
|
- ./docs:/tinyauth/docs
|
||||||
- ./main.go:/tinyauth/main.go
|
- ./main.go:/tinyauth/main.go
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
|
traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
|
||||||
|
traefik.http.services.nginx.loadbalancer.server.port: 80
|
||||||
traefik.http.routers.nginx.middlewares: tinyauth
|
traefik.http.routers.nginx.middlewares: tinyauth
|
||||||
|
|
||||||
tinyauth:
|
tinyauth:
|
||||||
@@ -26,4 +27,5 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
|
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
|
||||||
|
traefik.http.services.tinyauth.loadbalancer.server.port: 3000
|
||||||
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik
|
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik
|
||||||
|
|||||||
92
docs/docs.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||||
|
package docs
|
||||||
|
|
||||||
|
import "github.com/swaggo/swag"
|
||||||
|
|
||||||
|
const docTemplate = `{
|
||||||
|
"schemes": {{ marshal .Schemes }},
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "{{escape .Description}}",
|
||||||
|
"title": "{{.Title}}",
|
||||||
|
"contact": {},
|
||||||
|
"version": "{{.Version}}"
|
||||||
|
},
|
||||||
|
"host": "{{.Host}}",
|
||||||
|
"basePath": "{{.BasePath}}",
|
||||||
|
"paths": {
|
||||||
|
"/auth/logout": {
|
||||||
|
"get": {
|
||||||
|
"description": "Log the user out by invalidating the session cookie",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Logout",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.SimpleResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/healthcheck": {
|
||||||
|
"get": {
|
||||||
|
"description": "Simple health check",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"health"
|
||||||
|
],
|
||||||
|
"summary": "Health Check",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.SimpleResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"types.SimpleResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "OK"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||||
|
var SwaggerInfo = &swag.Spec{
|
||||||
|
Version: "1.0",
|
||||||
|
Host: "",
|
||||||
|
BasePath: "/api",
|
||||||
|
Schemes: []string{},
|
||||||
|
Title: "Tinyauth API",
|
||||||
|
Description: "Documentation for the Tinyauth API",
|
||||||
|
InfoInstanceName: "swagger",
|
||||||
|
SwaggerTemplate: docTemplate,
|
||||||
|
LeftDelim: "{{",
|
||||||
|
RightDelim: "}}",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||||
|
}
|
||||||
67
docs/swagger.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "Documentation for the Tinyauth API",
|
||||||
|
"title": "Tinyauth API",
|
||||||
|
"contact": {},
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
"basePath": "/api",
|
||||||
|
"paths": {
|
||||||
|
"/auth/logout": {
|
||||||
|
"get": {
|
||||||
|
"description": "Log the user out by invalidating the session cookie",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Logout",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.SimpleResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/healthcheck": {
|
||||||
|
"get": {
|
||||||
|
"description": "Simple health check",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"health"
|
||||||
|
],
|
||||||
|
"summary": "Health Check",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.SimpleResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"types.SimpleResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "OK"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
docs/swagger.yaml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
basePath: /api
|
||||||
|
definitions:
|
||||||
|
types.SimpleResponse:
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
example: OK
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
example: 200
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
info:
|
||||||
|
contact: {}
|
||||||
|
description: Documentation for the Tinyauth API
|
||||||
|
title: Tinyauth API
|
||||||
|
version: "1.0"
|
||||||
|
paths:
|
||||||
|
/auth/logout:
|
||||||
|
get:
|
||||||
|
description: Log the user out by invalidating the session cookie
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/types.SimpleResponse'
|
||||||
|
summary: Logout
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/healthcheck:
|
||||||
|
get:
|
||||||
|
description: Simple health check
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/types.SimpleResponse'
|
||||||
|
summary: Health Check
|
||||||
|
tags:
|
||||||
|
- health
|
||||||
|
swagger: "2.0"
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
FROM oven/bun:1.1.45-alpine
|
|
||||||
|
|
||||||
WORKDIR /frontend
|
|
||||||
|
|
||||||
COPY ./frontend/package.json ./
|
|
||||||
COPY ./frontend/bun.lockb ./
|
|
||||||
|
|
||||||
RUN bun install
|
|
||||||
|
|
||||||
COPY ./frontend/public ./public
|
|
||||||
COPY ./frontend/src ./src
|
|
||||||
|
|
||||||
COPY ./frontend/eslint.config.js ./
|
|
||||||
COPY ./frontend/index.html ./
|
|
||||||
COPY ./frontend/tsconfig.json ./
|
|
||||||
COPY ./frontend/tsconfig.app.json ./
|
|
||||||
COPY ./frontend/tsconfig.node.json ./
|
|
||||||
COPY ./frontend/vite.config.ts ./
|
|
||||||
COPY ./frontend/postcss.config.cjs ./
|
|
||||||
|
|
||||||
EXPOSE 5173
|
|
||||||
|
|
||||||
ENTRYPOINT ["bun", "run", "dev"]
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { ComboboxItem, Select } from "@mantine/core";
|
|
||||||
import { useState } from "react";
|
|
||||||
import i18n from "../../lib/i18n/i18n";
|
|
||||||
import {
|
|
||||||
SupportedLanguage,
|
|
||||||
getLanguageName,
|
|
||||||
languages,
|
|
||||||
} from "../../lib/i18n/locales";
|
|
||||||
|
|
||||||
export const LanguageSelector = () => {
|
|
||||||
const [language, setLanguage] = useState<ComboboxItem>({
|
|
||||||
value: i18n.language,
|
|
||||||
label: getLanguageName(i18n.language as SupportedLanguage),
|
|
||||||
});
|
|
||||||
|
|
||||||
const languageOptions = Object.entries(languages).map(([code, name]) => ({
|
|
||||||
value: code,
|
|
||||||
label: name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const handleLanguageChange = (option: string) => {
|
|
||||||
i18n.changeLanguage(option as SupportedLanguage);
|
|
||||||
setLanguage({
|
|
||||||
value: option,
|
|
||||||
label: getLanguageName(option as SupportedLanguage),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
data={languageOptions}
|
|
||||||
value={language ? language.value : null}
|
|
||||||
onChange={(_value, option) => handleLanguageChange(option.value)}
|
|
||||||
allowDeselect={false}
|
|
||||||
pos="absolute"
|
|
||||||
right={10}
|
|
||||||
top={10}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Center, Flex } from "@mantine/core";
|
|
||||||
import { ReactNode } from "react";
|
|
||||||
import { LanguageSelector } from "../language-selector/language-selector";
|
|
||||||
|
|
||||||
export const Layout = ({ children }: { children: ReactNode }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LanguageSelector />
|
|
||||||
<Center style={{ minHeight: "100vh" }}>
|
|
||||||
<Flex direction="column" flex="1" maw={340}>
|
|
||||||
{children}
|
|
||||||
</Flex>
|
|
||||||
</Center>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { useColorScheme } from "@mantine/hooks";
|
|
||||||
import type { SVGProps } from "react";
|
|
||||||
|
|
||||||
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{colorScheme === "dark" ? (
|
|
||||||
<>
|
|
||||||
<path xmlns="http://www.w3.org/2000/svg" d="M65.6 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9S1.8 219 1.8 254.2s28.6 63.9 63.8 63.9m191.6 0c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m0 193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m189.2-193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9" fill="#ffffff"/>
|
|
||||||
<path xmlns="http://www.w3.org/2000/svg" d="M65.6 127.7c35.3 0 63.9-28.6 63.9-63.9S100.9 0 65.6 0 1.8 28.6 1.8 63.9s28.6 63.8 63.8 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.8 28.7-63.8 63.9S30.4 512 65.6 512m191.6-384.3c35.3 0 63.9-28.6 63.9-63.9S292.5 0 257.2 0s-63.9 28.6-63.9 63.9 28.6 63.8 63.9 63.8m189.2 0c35.3 0 63.9-28.6 63.9-63.9S481.6 0 446.4 0c-35.3 0-63.9 28.6-63.9 63.9s28.6 63.8 63.9 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9" fill="#CCCAC9" opacity="0.2"/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<path xmlns="http://www.w3.org/2000/svg" d="M65.6 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9S1.8 219 1.8 254.2s28.6 63.9 63.8 63.9m191.6 0c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m0 193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m189.2-193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9"/>
|
|
||||||
<path xmlns="http://www.w3.org/2000/svg" d="M65.6 127.7c35.3 0 63.9-28.6 63.9-63.9S100.9 0 65.6 0 1.8 28.6 1.8 63.9s28.6 63.8 63.8 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.8 28.7-63.8 63.9S30.4 512 65.6 512m191.6-384.3c35.3 0 63.9-28.6 63.9-63.9S292.5 0 257.2 0s-63.9 28.6-63.9 63.9 28.6 63.8 63.9 63.8m189.2 0c35.3 0 63.9-28.6 63.9-63.9S481.6 0 446.4 0c-35.3 0-63.9 28.6-63.9 63.9s28.6 63.8 63.9 63.8m0 384.3c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9" opacity=".2"/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import i18n from "i18next";
|
|
||||||
import { initReactI18next } from "react-i18next";
|
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
|
||||||
import ChainedBackend from "i18next-chained-backend";
|
|
||||||
import resourcesToBackend from "i18next-resources-to-backend";
|
|
||||||
import HttpBackend from "i18next-http-backend";
|
|
||||||
|
|
||||||
i18n
|
|
||||||
.use(ChainedBackend)
|
|
||||||
.use(LanguageDetector)
|
|
||||||
.use(initReactI18next)
|
|
||||||
.init({
|
|
||||||
fallbackLng: "en",
|
|
||||||
debug: import.meta.env.MODE === "development",
|
|
||||||
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
load: "currentOnly",
|
|
||||||
|
|
||||||
backend: {
|
|
||||||
backends: [
|
|
||||||
HttpBackend,
|
|
||||||
resourcesToBackend(
|
|
||||||
(language: string) => import(`./locales/${language}.json`),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
backendOptions: [
|
|
||||||
{
|
|
||||||
loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default i18n;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
export const languages = {
|
|
||||||
"af-ZA": "Afrikaans",
|
|
||||||
"ar-SA": "العربية",
|
|
||||||
"ca-ES": "Català",
|
|
||||||
"cs-CZ": "Čeština",
|
|
||||||
"da-DK": "Dansk",
|
|
||||||
"de-DE": "Deutsch",
|
|
||||||
"el-GR": "Ελληνικά",
|
|
||||||
"en-US": "English",
|
|
||||||
"es-ES": "Español",
|
|
||||||
"fi-FI": "Suomi",
|
|
||||||
"fr-FR": "Français",
|
|
||||||
"he-IL": "עברית",
|
|
||||||
"hu-HU": "Magyar",
|
|
||||||
"it-IT": "Italiano",
|
|
||||||
"ja-JP": "日本語",
|
|
||||||
"ko-KR": "한국어",
|
|
||||||
"nl-NL": "Nederlands",
|
|
||||||
"no-NO": "Norsk",
|
|
||||||
"pl-PL": "Polski",
|
|
||||||
"pt-BR": "Português",
|
|
||||||
"pt-PT": "Português",
|
|
||||||
"ro-RO": "Română",
|
|
||||||
"ru-RU": "Русский",
|
|
||||||
"sr-SP": "Српски",
|
|
||||||
"sv-SE": "Svenska",
|
|
||||||
"tr-TR": "Türkçe",
|
|
||||||
"uk-UA": "Українська",
|
|
||||||
"vi-VN": "Tiếng Việt",
|
|
||||||
"zh-CN": "中文",
|
|
||||||
"zh-TW": "中文"
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SupportedLanguage = keyof typeof languages;
|
|
||||||
|
|
||||||
export const getLanguageName = (language: SupportedLanguage): string => languages[language];
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Willkommen zurück, logge dich ein mit",
|
|
||||||
"loginDivider": "Oder mit Passwort fortfahren",
|
|
||||||
"loginUsername": "Benutzername",
|
|
||||||
"loginPassword": "Passwort",
|
|
||||||
"loginSubmit": "Anmelden",
|
|
||||||
"loginFailTitle": "Login fehlgeschlagen",
|
|
||||||
"loginFailSubtitle": "Bitte überprüfe deinen Benutzernamen und Passwort",
|
|
||||||
"loginFailRateLimit": "Sie konnten sich zu oft nicht einloggen, bitte versuchen Sie es später erneut",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Καλώς ήρθατε, συνδεθείτε με",
|
|
||||||
"loginDivider": "Ή συνεχίστε με κωδικό πρόσβασης",
|
|
||||||
"loginUsername": "Όνομα Χρήστη",
|
|
||||||
"loginPassword": "Κωδικός",
|
|
||||||
"loginSubmit": "Είσοδος",
|
|
||||||
"loginFailTitle": "Αποτυχία σύνδεσης",
|
|
||||||
"loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης",
|
|
||||||
"loginFailRateLimit": "Αποτύχατε να συνδεθείτε πάρα πολλές φορές, παρακαλώ προσπαθήστε ξανά αργότερα",
|
|
||||||
"loginSuccessTitle": "Συνδεδεμένος",
|
|
||||||
"loginSuccessSubtitle": "Καλώς ήρθατε!",
|
|
||||||
"loginOauthFailTitle": "Εσωτερικό σφάλμα",
|
|
||||||
"loginOauthFailSubtitle": "Αποτυχία λήψης OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Ανακατεύθυνση",
|
|
||||||
"loginOauthSuccessSubtitle": "Ανακατεύθυνση στον πάροχο OAuth σας",
|
|
||||||
"continueRedirectingTitle": "Ανακατεύθυνση...",
|
|
||||||
"continueRedirectingSubtitle": "Θα πρέπει να μεταφερθείτε σύντομα στην εφαρμογή σας",
|
|
||||||
"continueInvalidRedirectTitle": "Μη έγκυρη ανακατεύθυνση",
|
|
||||||
"continueInvalidRedirectSubtitle": "Το URL ανακατεύθυνσης δεν είναι έγκυρο",
|
|
||||||
"continueInsecureRedirectTitle": "Μη ασφαλής ανακατεύθυνση",
|
|
||||||
"continueInsecureRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε από <Code>https</Code> σε <Code>http</Code>, είστε σίγουροι ότι θέλετε να συνεχίσετε;",
|
|
||||||
"continueTitle": "Συνέχεια",
|
|
||||||
"continueSubtitle": "Κάντε κλικ στο κουμπί για να συνεχίσετε στην εφαρμογή σας.",
|
|
||||||
"internalErrorTitle": "Εσωτερικό Σφάλμα Διακομιστή",
|
|
||||||
"internalErrorSubtitle": "Παρουσιάστηκε σφάλμα στο διακομιστή και δεν μπορεί να εξυπηρετήσει το αίτημά σας.",
|
|
||||||
"internalErrorButton": "Προσπαθήστε ξανά",
|
|
||||||
"logoutFailTitle": "Αποτυχία αποσύνδεσης",
|
|
||||||
"logoutFailSubtitle": "Παρακαλώ δοκιμάστε ξανά",
|
|
||||||
"logoutSuccessTitle": "Αποσυνδεδεμένος",
|
|
||||||
"logoutSuccessSubtitle": "Έχετε αποσυνδεθεί",
|
|
||||||
"logoutTitle": "Αποσύνδεση",
|
|
||||||
"logoutUsernameSubtitle": "Αυτή τη στιγμή είστε συνδεδεμένοι ως <Code>{{username}}</Code>, κάντε κλικ στο παρακάτω κουμπί για να αποσυνδεθείτε.",
|
|
||||||
"logoutOauthSubtitle": "Αυτή τη στιγμή είστε συνδεδεμένοι ως <Code>{{username}}</Code> χρησιμοποιώντας την υπηρεσία παροχής {{provider}} OAuth, κάντε κλικ στο παρακάτω κουμπί για να αποσυνδεθείτε.",
|
|
||||||
"notFoundTitle": "Η σελίδα δε βρέθηκε",
|
|
||||||
"notFoundSubtitle": "Η σελίδα που ψάχνετε δεν υπάρχει.",
|
|
||||||
"notFoundButton": "Μετάβαση στην αρχική",
|
|
||||||
"totpFailTitle": "Αποτυχία επαλήθευσης κωδικού",
|
|
||||||
"totpFailSubtitle": "Παρακαλώ ελέγξτε τον κώδικά σας και προσπαθήστε ξανά",
|
|
||||||
"totpSuccessTitle": "Επαληθεύθηκε",
|
|
||||||
"totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας",
|
|
||||||
"totpTitle": "Εισάγετε τον κωδικό TOTP",
|
|
||||||
"unauthorizedTitle": "Μη εξουσιοδοτημένο",
|
|
||||||
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.",
|
|
||||||
"unauthorizedButton": "Προσπαθήστε ξανά"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Bienvenue, connectez-vous avec",
|
|
||||||
"loginDivider": "Ou continuez avec le mot de passe",
|
|
||||||
"loginUsername": "Nom d'utilisateur",
|
|
||||||
"loginPassword": "Mot de passe",
|
|
||||||
"loginSubmit": "Se connecter",
|
|
||||||
"loginFailTitle": "Échec de la connexion",
|
|
||||||
"loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe",
|
|
||||||
"loginFailRateLimit": "Vous n'avez pas pu vous connecter trop de fois, veuillez réessayer plus tard",
|
|
||||||
"loginSuccessTitle": "Connecté",
|
|
||||||
"loginSuccessSubtitle": "Bienvenue!",
|
|
||||||
"loginOauthFailTitle": "Erreur interne",
|
|
||||||
"loginOauthFailSubtitle": "Impossible d'obtenir l'URL OAuth",
|
|
||||||
"loginOauthSuccessTitle": "Redirection",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirection vers votre fournisseur OAuth",
|
|
||||||
"continueRedirectingTitle": "Redirection...",
|
|
||||||
"continueRedirectingSubtitle": "Vous devriez être redirigé vers l'application bientôt",
|
|
||||||
"continueInvalidRedirectTitle": "Redirection invalide",
|
|
||||||
"continueInvalidRedirectSubtitle": "L'URL de redirection est invalide",
|
|
||||||
"continueInsecureRedirectTitle": "Redirection non sécurisée",
|
|
||||||
"continueInsecureRedirectSubtitle": "Vous essayez de rediriger de <Code>https</Code> vers <Code>http</Code>, êtes-vous sûr de vouloir continuer ?",
|
|
||||||
"continueTitle": "Continuer",
|
|
||||||
"continueSubtitle": "Cliquez sur le bouton pour continuer vers votre application.",
|
|
||||||
"internalErrorTitle": "Erreur interne du serveur",
|
|
||||||
"internalErrorSubtitle": "Une erreur s'est produite sur le serveur et il ne peut actuellement pas répondre à votre demande.",
|
|
||||||
"internalErrorButton": "Réessayer",
|
|
||||||
"logoutFailTitle": "Échec de la déconnexion",
|
|
||||||
"logoutFailSubtitle": "Veuillez réessayer",
|
|
||||||
"logoutSuccessTitle": "Déconnecté",
|
|
||||||
"logoutSuccessSubtitle": "Vous avez été déconnecté",
|
|
||||||
"logoutTitle": "Déconnexion",
|
|
||||||
"logoutUsernameSubtitle": "Vous êtes actuellement connecté en tant que <Code>{{username}}</Code>, cliquez sur le bouton ci-dessous pour vous déconnecter.",
|
|
||||||
"logoutOauthSubtitle": "Vous êtes actuellement connecté en tant que <Code>{{username}}</Code> en utilisant le fournisseur OAuth {{provider}} , cliquez sur le bouton ci-dessous pour vous déconnecter.",
|
|
||||||
"notFoundTitle": "Page introuvable",
|
|
||||||
"notFoundSubtitle": "La page recherchée n'existe pas.",
|
|
||||||
"notFoundButton": "Retour à la page d'accueil",
|
|
||||||
"totpFailTitle": "Échec de la vérification du code",
|
|
||||||
"totpFailSubtitle": "Veuillez vérifier votre code et réessayer",
|
|
||||||
"totpSuccessTitle": "Vérifié",
|
|
||||||
"totpSuccessSubtitle": "Redirection vers votre application",
|
|
||||||
"totpTitle": "Saisissez votre code TOTP",
|
|
||||||
"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>.",
|
|
||||||
"unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à se connecter.",
|
|
||||||
"unauthorizedButton": "Réessayer"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welkom terug, log in met",
|
|
||||||
"loginDivider": "Of ga door met wachtwoord",
|
|
||||||
"loginUsername": "Gebruikersnaam",
|
|
||||||
"loginPassword": "Wachtwoord",
|
|
||||||
"loginSubmit": "Log in",
|
|
||||||
"loginFailTitle": "Mislukt om in te loggen",
|
|
||||||
"loginFailSubtitle": "Gelieve uw gebruikersnaam en wachtwoord te controleren",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Witaj ponownie, zaloguj się przez",
|
|
||||||
"loginDivider": "Lub kontynuuj z hasłem",
|
|
||||||
"loginUsername": "Nazwa użytkownika",
|
|
||||||
"loginPassword": "Hasło",
|
|
||||||
"loginSubmit": "Zaloguj się",
|
|
||||||
"loginFailTitle": "Nie udało się zalogować",
|
|
||||||
"loginFailSubtitle": "Sprawdź swoją nazwę użytkownika i hasło",
|
|
||||||
"loginFailRateLimit": "Nie udało się zalogować zbyt wiele razy, spróbuj ponownie później",
|
|
||||||
"loginSuccessTitle": "Zalogowano",
|
|
||||||
"loginSuccessSubtitle": "Witaj ponownie!",
|
|
||||||
"loginOauthFailTitle": "Wewnętrzny błąd",
|
|
||||||
"loginOauthFailSubtitle": "Nie udało się uzyskać adresu URL OAuth",
|
|
||||||
"loginOauthSuccessTitle": "Przekierowywanie",
|
|
||||||
"loginOauthSuccessSubtitle": "Przekierowywanie do Twojego dostawcy OAuth",
|
|
||||||
"continueRedirectingTitle": "Przekierowywanie...",
|
|
||||||
"continueRedirectingSubtitle": "Wkrótce powinieneś zostać przekierowany do aplikacji",
|
|
||||||
"continueInvalidRedirectTitle": "Nieprawidłowe przekierowanie",
|
|
||||||
"continueInvalidRedirectSubtitle": "Adres przekierowania jest nieprawidłowy",
|
|
||||||
"continueInsecureRedirectTitle": "Niezabezpieczone przekierowanie",
|
|
||||||
"continueInsecureRedirectSubtitle": "Próbujesz przekierować z <Code>https</Code> do <Code>http</Code>, czy na pewno chcesz kontynuować?",
|
|
||||||
"continueTitle": "Kontynuuj",
|
|
||||||
"continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.",
|
|
||||||
"internalErrorTitle": "Wewnętrzny błąd serwera",
|
|
||||||
"internalErrorSubtitle": "Wystąpił błąd na serwerze i obecnie nie można obsłużyć tego żądania.",
|
|
||||||
"internalErrorButton": "Spróbuj ponownie",
|
|
||||||
"logoutFailTitle": "Nie udało się wylogować",
|
|
||||||
"logoutFailSubtitle": "Spróbuj ponownie",
|
|
||||||
"logoutSuccessTitle": "Wylogowano",
|
|
||||||
"logoutSuccessSubtitle": "Zostałeś wylogowany",
|
|
||||||
"logoutTitle": "Wylogowanie",
|
|
||||||
"logoutUsernameSubtitle": "Jesteś aktualnie zalogowany jako <Code>{{username}}</Code>, kliknij przycisk poniżej, aby się wylogować.",
|
|
||||||
"logoutOauthSubtitle": "Jesteś obecnie zalogowany jako <Code>{{username}}</Code> przy użyciu providera OAuth {{provider}}, kliknij przycisk poniżej, aby się wylogować.",
|
|
||||||
"notFoundTitle": "Strona nie znaleziona",
|
|
||||||
"notFoundSubtitle": "Strona, której szukasz nie istnieje.",
|
|
||||||
"notFoundButton": "Wróć do strony głównej",
|
|
||||||
"totpFailTitle": "Nie udało się zweryfikować kodu",
|
|
||||||
"totpFailSubtitle": "Sprawdź swój kod i spróbuj ponownie",
|
|
||||||
"totpSuccessTitle": "Zweryfikowano",
|
|
||||||
"totpSuccessSubtitle": "Przekierowywanie do aplikacji",
|
|
||||||
"totpTitle": "Wprowadź kod TOTP",
|
|
||||||
"unauthorizedTitle": "Nieautoryzowany",
|
|
||||||
"unauthorizedResourceSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do logowania.",
|
|
||||||
"unauthorizedButton": "Spróbuj ponownie"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"loginTitle": "Welcome back, login with",
|
|
||||||
"loginDivider": "Or continue with password",
|
|
||||||
"loginUsername": "Username",
|
|
||||||
"loginPassword": "Password",
|
|
||||||
"loginSubmit": "Login",
|
|
||||||
"loginFailTitle": "Failed to log in",
|
|
||||||
"loginFailSubtitle": "Please check your username and password",
|
|
||||||
"loginFailRateLimit": "You failed to login too many times, please try again later",
|
|
||||||
"loginSuccessTitle": "Logged in",
|
|
||||||
"loginSuccessSubtitle": "Welcome back!",
|
|
||||||
"loginOauthFailTitle": "Internal error",
|
|
||||||
"loginOauthFailSubtitle": "Failed to get OAuth URL",
|
|
||||||
"loginOauthSuccessTitle": "Redirecting",
|
|
||||||
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
|
|
||||||
"continueRedirectingTitle": "Redirecting...",
|
|
||||||
"continueRedirectingSubtitle": "You should be redirected to the app soon",
|
|
||||||
"continueInvalidRedirectTitle": "Invalid redirect",
|
|
||||||
"continueInvalidRedirectSubtitle": "The redirect URL is invalid",
|
|
||||||
"continueInsecureRedirectTitle": "Insecure redirect",
|
|
||||||
"continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
|
|
||||||
"continueTitle": "Continue",
|
|
||||||
"continueSubtitle": "Click the button to continue to your app.",
|
|
||||||
"internalErrorTitle": "Internal Server Error",
|
|
||||||
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
|
|
||||||
"internalErrorButton": "Try again",
|
|
||||||
"logoutFailTitle": "Failed to log out",
|
|
||||||
"logoutFailSubtitle": "Please try again",
|
|
||||||
"logoutSuccessTitle": "Logged out",
|
|
||||||
"logoutSuccessSubtitle": "You have been logged out",
|
|
||||||
"logoutTitle": "Logout",
|
|
||||||
"logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
|
|
||||||
"logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
|
|
||||||
"notFoundTitle": "Page not found",
|
|
||||||
"notFoundSubtitle": "The page you are looking for does not exist.",
|
|
||||||
"notFoundButton": "Go home",
|
|
||||||
"totpFailTitle": "Failed to verify code",
|
|
||||||
"totpFailSubtitle": "Please check your code and try again",
|
|
||||||
"totpSuccessTitle": "Verified",
|
|
||||||
"totpSuccessSubtitle": "Redirecting to your app",
|
|
||||||
"totpTitle": "Enter your TOTP code",
|
|
||||||
"unauthorizedTitle": "Unauthorized",
|
|
||||||
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
|
|
||||||
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
|
|
||||||
"unauthorizedButton": "Try again"
|
|
||||||
}
|
|
||||||
43
go.mod
@@ -3,24 +3,36 @@ module tinyauth
|
|||||||
go 1.23.2
|
go 1.23.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gin-contrib/sessions v1.0.2
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-playground/validator/v10 v10.24.0
|
github.com/go-playground/validator/v10 v10.25.0
|
||||||
github.com/google/go-querystring v1.1.0
|
github.com/google/go-querystring v1.1.0
|
||||||
github.com/mdp/qrterminal/v3 v3.2.0
|
github.com/mdp/qrterminal/v3 v3.2.0
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
golang.org/x/crypto v0.32.0
|
github.com/swaggo/swag v1.16.4
|
||||||
|
golang.org/x/crypto v0.36.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.1 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
|
github.com/swaggo/files v1.0.1 // indirect
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||||
golang.org/x/term v0.28.0 // indirect
|
golang.org/x/term v0.30.0 // indirect
|
||||||
|
golang.org/x/tools v0.31.0 // indirect
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -30,8 +42,8 @@ require (
|
|||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/boombuler/barcode v1.0.2 // indirect
|
github.com/boombuler/barcode v1.0.2 // indirect
|
||||||
github.com/bytedance/sonic v1.12.7 // indirect
|
github.com/bytedance/sonic v1.13.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/catppuccin/go v0.2.0 // indirect
|
github.com/catppuccin/go v0.2.0 // indirect
|
||||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.1.0 // indirect
|
github.com/charmbracelet/bubbletea v1.1.0 // indirect
|
||||||
@@ -40,7 +52,7 @@ require (
|
|||||||
github.com/charmbracelet/x/ansi v0.2.3 // indirect
|
github.com/charmbracelet/x/ansi v0.2.3 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/docker v27.5.1+incompatible
|
github.com/docker/docker v27.5.1+incompatible
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
@@ -55,14 +67,15 @@ require (
|
|||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.4 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/gorilla/sessions v1.2.2
|
github.com/gorilla/sessions v1.2.2 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7
|
github.com/magiconair/properties v1.8.7
|
||||||
@@ -99,14 +112,14 @@ require (
|
|||||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/arch v0.13.0 // indirect
|
golang.org/x/arch v0.15.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/net v0.37.0 // indirect
|
||||||
golang.org/x/oauth2 v0.25.0
|
golang.org/x/oauth2 v0.25.0
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.12.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.3 // indirect
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
102
go.sum
@@ -1,5 +1,7 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||||
@@ -11,11 +13,11 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ
|
|||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
||||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
@@ -34,8 +36,8 @@ github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko
|
|||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||||
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
@@ -65,6 +67,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
|||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
|
||||||
|
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
|
||||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
@@ -74,16 +78,24 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
|||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||||
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
|
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||||
|
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
||||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
@@ -97,6 +109,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||||
|
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||||
@@ -107,13 +121,15 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -126,6 +142,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
|||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
@@ -218,12 +236,19 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||||
|
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||||
|
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||||
@@ -246,53 +271,74 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
|||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||||
|
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -304,8 +350,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:
|
|||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
||||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -3,31 +3,55 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"tinyauth/internal/assets"
|
"tinyauth/internal/assets"
|
||||||
|
"tinyauth/internal/auth"
|
||||||
"tinyauth/internal/handlers"
|
"tinyauth/internal/handlers"
|
||||||
|
"tinyauth/internal/hooks"
|
||||||
|
"tinyauth/internal/providers"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
|
||||||
|
docs "tinyauth/docs"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-contrib/sessions/cookie"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/go-querystring/query"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
swaggerfiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
|
func NewAPI(config types.APIConfig, hooks *hooks.Hooks, auth *auth.Auth, providers *providers.Providers, handlers *handlers.Handlers) *API {
|
||||||
return &API{
|
return &API{
|
||||||
Config: config,
|
Config: config,
|
||||||
Handlers: handlers,
|
Hooks: hooks,
|
||||||
|
Auth: auth,
|
||||||
|
Providers: providers,
|
||||||
|
Handlers: handlers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
Config types.APIConfig
|
Config types.APIConfig
|
||||||
Router *gin.Engine
|
Router *gin.Engine
|
||||||
Handlers *handlers.Handlers
|
Hooks *hooks.Hooks
|
||||||
|
Auth *auth.Auth
|
||||||
|
Providers *providers.Providers
|
||||||
|
Handlers *handlers.Handlers
|
||||||
|
Domain string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @title Tinyauth API
|
||||||
|
// @version 1.0
|
||||||
|
// @description Documentation for the Tinyauth API
|
||||||
|
// @BasePath /api
|
||||||
|
|
||||||
func (api *API) Init() {
|
func (api *API) Init() {
|
||||||
// Disable gin logs
|
// Disable gin logs
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
@@ -36,24 +60,48 @@ func (api *API) Init() {
|
|||||||
log.Debug().Msg("Setting up router")
|
log.Debug().Msg("Setting up router")
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(zerolog())
|
router.Use(zerolog())
|
||||||
|
router.RedirectTrailingSlash = true
|
||||||
|
|
||||||
// Read UI assets
|
// Read UI assets
|
||||||
log.Debug().Msg("Setting up assets")
|
log.Debug().Msg("Setting up assets")
|
||||||
dist, err := fs.Sub(assets.Assets, "dist")
|
dist, distErr := fs.Sub(assets.Assets, "dist")
|
||||||
|
|
||||||
if err != nil {
|
if distErr != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to get UI assets")
|
log.Fatal().Err(distErr).Msg("Failed to get UI assets")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create file server
|
// Create file server
|
||||||
log.Debug().Msg("Setting up file server")
|
log.Debug().Msg("Setting up file server")
|
||||||
fileServer := http.FileServer(http.FS(dist))
|
fileServer := http.FileServer(http.FS(dist))
|
||||||
|
|
||||||
|
// Setup cookie store
|
||||||
|
log.Debug().Msg("Setting up cookie store")
|
||||||
|
store := cookie.NewStore([]byte(api.Config.Secret))
|
||||||
|
|
||||||
|
// Use session middleware
|
||||||
|
store.Options(sessions.Options{
|
||||||
|
Domain: api.Domain,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: api.Config.CookieSecure,
|
||||||
|
MaxAge: api.Config.SessionExpiry,
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Use(sessions.Sessions("tinyauth", store))
|
||||||
|
|
||||||
|
// Configure swagger
|
||||||
|
docs.SwaggerInfo.BasePath = "/api"
|
||||||
|
|
||||||
|
// Swagger middleware
|
||||||
|
router.GET("/api/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||||
|
router.GET("/api/swagger", func(ctx *gin.Context) {
|
||||||
|
ctx.Redirect(http.StatusPermanentRedirect, "/api/swagger/index.html")
|
||||||
|
})
|
||||||
|
|
||||||
// UI middleware
|
// UI middleware
|
||||||
router.Use(func(c *gin.Context) {
|
router.Use(func(c *gin.Context) {
|
||||||
// If not an API request, serve the UI
|
// If not an API request, serve the UI
|
||||||
if !strings.HasPrefix(c.Request.URL.Path, "/api") {
|
if !strings.HasPrefix(c.Request.URL.Path, "/api") {
|
||||||
// Check if the file exists
|
|
||||||
_, err := fs.Stat(dist, strings.TrimPrefix(c.Request.URL.Path, "/"))
|
_, err := fs.Stat(dist, strings.TrimPrefix(c.Request.URL.Path, "/"))
|
||||||
|
|
||||||
// If the file doesn't exist, serve the index.html
|
// If the file doesn't exist, serve the index.html
|
||||||
@@ -74,24 +122,412 @@ func (api *API) Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) SetupRoutes() {
|
func (api *API) SetupRoutes() {
|
||||||
// Proxy
|
api.Router.GET("/api/healthcheck", api.Handlers.HealthCheck)
|
||||||
api.Router.GET("/api/auth/:proxy", api.Handlers.AuthHandler)
|
api.Router.GET("/api/auth/logout", api.Handlers.Logout)
|
||||||
|
api.Router.GET("/api/auth", api.Handlers.CheckAuth)
|
||||||
|
|
||||||
// Auth
|
api.Router.POST("/api/login", func(c *gin.Context) {
|
||||||
api.Router.POST("/api/login", api.Handlers.LoginHandler)
|
// Create login struct
|
||||||
api.Router.POST("/api/totp", api.Handlers.TotpHandler)
|
var login types.LoginRequest
|
||||||
api.Router.POST("/api/logout", api.Handlers.LogoutHandler)
|
|
||||||
|
|
||||||
// Context
|
// Bind JSON
|
||||||
api.Router.GET("/api/app", api.Handlers.AppHandler)
|
err := c.BindJSON(&login)
|
||||||
api.Router.GET("/api/user", api.Handlers.UserHandler)
|
|
||||||
|
|
||||||
// OAuth
|
// Handle error
|
||||||
api.Router.GET("/api/oauth/url/:provider", api.Handlers.OauthUrlHandler)
|
if err != nil {
|
||||||
api.Router.GET("/api/oauth/callback/:provider", api.Handlers.OauthCallbackHandler)
|
log.Error().Err(err).Msg("Failed to bind JSON")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad Request",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// App
|
log.Debug().Msg("Got login request")
|
||||||
api.Router.GET("/api/healthcheck", api.Handlers.HealthcheckHandler)
|
|
||||||
|
// Get user based on username
|
||||||
|
user := api.Auth.GetUser(login.Username)
|
||||||
|
|
||||||
|
// User does not exist
|
||||||
|
if user == nil {
|
||||||
|
log.Debug().Str("username", login.Username).Msg("User not found")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"status": 401,
|
||||||
|
"message": "Unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got user")
|
||||||
|
|
||||||
|
// Check if password is correct
|
||||||
|
if !api.Auth.CheckPassword(*user, login.Password) {
|
||||||
|
log.Debug().Str("username", login.Username).Msg("Password incorrect")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"status": 401,
|
||||||
|
"message": "Unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Password correct, checking totp")
|
||||||
|
|
||||||
|
// Check if user has totp enabled
|
||||||
|
if user.TotpSecret != "" {
|
||||||
|
log.Debug().Msg("Totp enabled")
|
||||||
|
|
||||||
|
// Set totp pending cookie
|
||||||
|
api.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
|
Username: login.Username,
|
||||||
|
Provider: "username",
|
||||||
|
TotpPending: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return totp required
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"message": "Waiting for totp",
|
||||||
|
"totpPending": true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Stop further processing
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session cookie with username as provider
|
||||||
|
api.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
|
Username: login.Username,
|
||||||
|
Provider: "username",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return logged in
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"message": "Logged in",
|
||||||
|
"totpPending": false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
api.Router.POST("/api/totp", func(c *gin.Context) {
|
||||||
|
// Create totp struct
|
||||||
|
var totpReq types.TotpRequest
|
||||||
|
|
||||||
|
// Bind JSON
|
||||||
|
err := c.BindJSON(&totpReq)
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to bind JSON")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad Request",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Checking totp")
|
||||||
|
|
||||||
|
// Get user context
|
||||||
|
userContext := api.Hooks.UseUserContext(c)
|
||||||
|
|
||||||
|
// Check if we have a user
|
||||||
|
if userContext.Username == "" {
|
||||||
|
log.Debug().Msg("No user context")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"status": 401,
|
||||||
|
"message": "Unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
user := api.Auth.GetUser(userContext.Username)
|
||||||
|
|
||||||
|
// Check if user exists
|
||||||
|
if user == nil {
|
||||||
|
log.Debug().Msg("User not found")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"status": 401,
|
||||||
|
"message": "Unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if totp is correct
|
||||||
|
totpOk := totp.Validate(totpReq.Code, user.TotpSecret)
|
||||||
|
|
||||||
|
// TOTP is incorrect
|
||||||
|
if !totpOk {
|
||||||
|
log.Debug().Msg("Totp incorrect")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"status": 401,
|
||||||
|
"message": "Unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Totp correct")
|
||||||
|
|
||||||
|
// Create session cookie with username as provider
|
||||||
|
api.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
|
Username: user.Username,
|
||||||
|
Provider: "username",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return logged in
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"message": "Logged in",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
api.Router.GET("/api/app", func(c *gin.Context) {
|
||||||
|
log.Debug().Msg("Getting app context")
|
||||||
|
|
||||||
|
// Get configured providers
|
||||||
|
configuredProviders := api.Providers.GetConfiguredProviders()
|
||||||
|
|
||||||
|
// We have username/password configured so add it to our providers
|
||||||
|
if api.Auth.UserAuthConfigured() {
|
||||||
|
configuredProviders = append(configuredProviders, "username")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create app context struct
|
||||||
|
appContext := types.AppContext{
|
||||||
|
Status: 200,
|
||||||
|
Message: "Ok",
|
||||||
|
ConfiguredProviders: configuredProviders,
|
||||||
|
DisableContinue: api.Config.DisableContinue,
|
||||||
|
Title: api.Config.Title,
|
||||||
|
GenericName: api.Config.GenericName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return app context
|
||||||
|
c.JSON(200, appContext)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.Router.GET("/api/user", func(c *gin.Context) {
|
||||||
|
log.Debug().Msg("Getting user context")
|
||||||
|
|
||||||
|
// Get user context
|
||||||
|
userContext := api.Hooks.UseUserContext(c)
|
||||||
|
|
||||||
|
// Create user context response
|
||||||
|
userContextResponse := types.UserContextResponse{
|
||||||
|
Status: 200,
|
||||||
|
IsLoggedIn: userContext.IsLoggedIn,
|
||||||
|
Username: userContext.Username,
|
||||||
|
Provider: userContext.Provider,
|
||||||
|
Oauth: userContext.OAuth,
|
||||||
|
TotpPending: userContext.TotpPending,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are not logged in we set the status to 401 and add the WWW-Authenticate header else we set it to 200
|
||||||
|
if !userContext.IsLoggedIn {
|
||||||
|
log.Debug().Msg("Unauthorized")
|
||||||
|
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
|
||||||
|
userContextResponse.Message = "Unauthorized"
|
||||||
|
} else {
|
||||||
|
log.Debug().Interface("userContext", userContext).Msg("Authenticated")
|
||||||
|
userContextResponse.Message = "Authenticated"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return user context
|
||||||
|
c.JSON(200, userContextResponse)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.Router.GET("/api/oauth/url/:provider", func(c *gin.Context) {
|
||||||
|
// Create struct for OAuth request
|
||||||
|
var request types.OAuthRequest
|
||||||
|
|
||||||
|
// Bind URI
|
||||||
|
bindErr := c.BindUri(&request)
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if bindErr != nil {
|
||||||
|
log.Error().Err(bindErr).Msg("Failed to bind URI")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad Request",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got OAuth request")
|
||||||
|
|
||||||
|
// Check if provider exists
|
||||||
|
provider := api.Providers.GetProvider(request.Provider)
|
||||||
|
|
||||||
|
// Provider does not exist
|
||||||
|
if provider == nil {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"status": 404,
|
||||||
|
"message": "Not Found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("provider", request.Provider).Msg("Got provider")
|
||||||
|
|
||||||
|
// Get auth URL
|
||||||
|
authURL := provider.GetAuthURL()
|
||||||
|
|
||||||
|
log.Debug().Msg("Got auth URL")
|
||||||
|
|
||||||
|
// Get redirect URI
|
||||||
|
redirectURI := c.Query("redirect_uri")
|
||||||
|
|
||||||
|
// Set redirect cookie if redirect URI is provided
|
||||||
|
if redirectURI != "" {
|
||||||
|
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
|
||||||
|
c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", api.Domain, api.Config.CookieSecure, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it
|
||||||
|
if request.Provider == "tailscale" {
|
||||||
|
// Build tailscale query
|
||||||
|
tailscaleQuery, tailscaleQueryErr := query.Values(types.TailscaleQuery{
|
||||||
|
Code: (1000 + rand.IntN(9000)),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if tailscaleQueryErr != nil {
|
||||||
|
log.Error().Err(tailscaleQueryErr).Msg("Failed to build query")
|
||||||
|
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", api.Config.AppURL, tailscaleQuery.Encode()),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return auth URL
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"message": "Ok",
|
||||||
|
"url": authURL,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
api.Router.GET("/api/oauth/callback/:provider", func(c *gin.Context) {
|
||||||
|
// Create struct for OAuth request
|
||||||
|
var providerName types.OAuthRequest
|
||||||
|
|
||||||
|
// Bind URI
|
||||||
|
bindErr := c.BindUri(&providerName)
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if api.handleError(c, "Failed to bind URI", bindErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name")
|
||||||
|
|
||||||
|
// Get code
|
||||||
|
code := c.Query("code")
|
||||||
|
|
||||||
|
// Code empty so redirect to error
|
||||||
|
if code == "" {
|
||||||
|
log.Error().Msg("No code provided")
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, "/error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Got code")
|
||||||
|
|
||||||
|
// Get provider
|
||||||
|
provider := api.Providers.GetProvider(providerName.Provider)
|
||||||
|
|
||||||
|
log.Debug().Str("provider", providerName.Provider).Msg("Got provider")
|
||||||
|
|
||||||
|
// Provider does not exist
|
||||||
|
if provider == nil {
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, "/not-found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange token (authenticates user)
|
||||||
|
_, tokenErr := provider.ExchangeToken(code)
|
||||||
|
|
||||||
|
log.Debug().Msg("Got token")
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if api.handleError(c, "Failed to exchange token", tokenErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get email
|
||||||
|
email, emailErr := api.Providers.GetUser(providerName.Provider)
|
||||||
|
|
||||||
|
log.Debug().Str("email", email).Msg("Got email")
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if api.handleError(c, "Failed to get user", emailErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email is not whitelisted
|
||||||
|
if !api.Auth.EmailWhitelisted(email) {
|
||||||
|
log.Warn().Str("email", email).Msg("Email not whitelisted")
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
unauthorizedQuery, unauthorizedQueryErr := query.Values(types.UnauthorizedQuery{
|
||||||
|
Username: email,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if api.handleError(c, "Failed to build query", unauthorizedQueryErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to unauthorized
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, unauthorizedQuery.Encode()))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Email whitelisted")
|
||||||
|
|
||||||
|
// Create session cookie
|
||||||
|
api.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||||
|
Username: email,
|
||||||
|
Provider: providerName.Provider,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get redirect URI
|
||||||
|
redirectURI, redirectURIErr := 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 redirectURIErr != nil {
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, api.Config.AppURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI")
|
||||||
|
|
||||||
|
// Clean up redirect cookie since we already have the value
|
||||||
|
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
redirectQuery, redirectQueryErr := query.Values(types.LoginQuery{
|
||||||
|
RedirectURI: redirectURI,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Debug().Msg("Got redirect query")
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if api.handleError(c, "Failed to build query", redirectQueryErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to continue with the redirect URI
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", api.Config.AppURL, redirectQuery.Encode()))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) Run() {
|
func (api *API) Run() {
|
||||||
@@ -100,12 +536,23 @@ func (api *API) Run() {
|
|||||||
// Run server
|
// Run server
|
||||||
err := api.Router.Run(fmt.Sprintf("%s:%d", api.Config.Address, api.Config.Port))
|
err := api.Router.Run(fmt.Sprintf("%s:%d", api.Config.Address, api.Config.Port))
|
||||||
|
|
||||||
// Check for errors
|
// Check error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to start server")
|
log.Fatal().Err(err).Msg("Failed to start server")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleError logs the error and redirects to the error page (only meant for stuff the user may access does not apply for login paths)
|
||||||
|
func (api *API) handleError(c *gin.Context, msg string, err error) bool {
|
||||||
|
// If error is not nil log it and redirect to error page also return true so we can stop further processing
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg(msg)
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", api.Config.AppURL))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// zerolog is a middleware for gin that logs requests using zerolog
|
// zerolog is a middleware for gin that logs requests using zerolog
|
||||||
func zerolog() gin.HandlerFunc {
|
func zerolog() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"tinyauth/internal/api"
|
"tinyauth/internal/api"
|
||||||
@@ -21,27 +20,13 @@ import (
|
|||||||
|
|
||||||
// Simple API config for tests
|
// Simple API config for tests
|
||||||
var apiConfig = types.APIConfig{
|
var apiConfig = types.APIConfig{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
}
|
|
||||||
|
|
||||||
// Simple handlers config for tests
|
|
||||||
var handlersConfig = types.HandlersConfig{
|
|
||||||
AppURL: "http://localhost:8080",
|
|
||||||
DisableContinue: false,
|
|
||||||
Title: "Tinyauth",
|
|
||||||
GenericName: "Generic",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple auth config for tests
|
|
||||||
var authConfig = types.AuthConfig{
|
|
||||||
Users: types.Users{},
|
|
||||||
OauthWhitelist: []string{},
|
|
||||||
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
||||||
|
AppURL: "http://tinyauth.localhost",
|
||||||
CookieSecure: false,
|
CookieSecure: false,
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 3600,
|
||||||
LoginTimeout: 0,
|
DisableContinue: false,
|
||||||
LoginMaxRetries: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookie
|
// Cookie
|
||||||
@@ -59,21 +44,20 @@ func getAPI(t *testing.T) *api.API {
|
|||||||
docker := docker.NewDocker()
|
docker := docker.NewDocker()
|
||||||
|
|
||||||
// Initialize docker
|
// Initialize docker
|
||||||
err := docker.Init()
|
dockerErr := docker.Init()
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if dockerErr != nil {
|
||||||
t.Fatalf("Failed to initialize docker: %v", err)
|
t.Fatalf("Failed to initialize docker: %v", dockerErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create auth service
|
// Create auth service
|
||||||
authConfig.Users = types.Users{
|
auth := auth.NewAuth(docker, types.Users{
|
||||||
{
|
{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Password: user.Password,
|
Password: user.Password,
|
||||||
},
|
},
|
||||||
}
|
}, nil, apiConfig.SessionExpiry)
|
||||||
auth := auth.NewAuth(authConfig, docker)
|
|
||||||
|
|
||||||
// Create providers service
|
// Create providers service
|
||||||
providers := providers.NewProviders(types.OAuthConfig{})
|
providers := providers.NewProviders(types.OAuthConfig{})
|
||||||
@@ -84,11 +68,11 @@ func getAPI(t *testing.T) *api.API {
|
|||||||
// Create hooks service
|
// Create hooks service
|
||||||
hooks := hooks.NewHooks(auth, providers)
|
hooks := hooks.NewHooks(auth, providers)
|
||||||
|
|
||||||
// Create handlers service
|
// Create handlers
|
||||||
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
apiHandlers := handlers.NewHandlers(apiConfig)
|
||||||
|
|
||||||
// Create API
|
// Create API
|
||||||
api := api.NewAPI(apiConfig, handlers)
|
api := api.NewAPI(apiConfig, hooks, auth, providers, apiHandlers)
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
api.Init()
|
api.Init()
|
||||||
@@ -143,70 +127,6 @@ func TestLogin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test app context
|
|
||||||
func TestAppContext(t *testing.T) {
|
|
||||||
t.Log("Testing app context")
|
|
||||||
|
|
||||||
// Get API
|
|
||||||
api := getAPI(t)
|
|
||||||
|
|
||||||
// Create recorder
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Create request
|
|
||||||
req, err := http.NewRequest("GET", "/api/app", nil)
|
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error creating request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the cookie
|
|
||||||
req.AddCookie(&http.Cookie{
|
|
||||||
Name: "tinyauth",
|
|
||||||
Value: cookie,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Serve the request
|
|
||||||
api.Router.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
|
||||||
|
|
||||||
// Read the body of the response
|
|
||||||
body, err := io.ReadAll(recorder.Body)
|
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error getting body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
|
||||||
var app types.AppContext
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &app)
|
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error unmarshalling body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create tests values
|
|
||||||
expected := types.AppContext{
|
|
||||||
Status: 200,
|
|
||||||
Message: "OK",
|
|
||||||
ConfiguredProviders: []string{"username"},
|
|
||||||
DisableContinue: false,
|
|
||||||
Title: "Tinyauth",
|
|
||||||
GenericName: "Generic",
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should get the username back
|
|
||||||
if !reflect.DeepEqual(app, expected) {
|
|
||||||
t.Fatalf("Expected %v, got %v", expected, app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test user context
|
// Test user context
|
||||||
func TestUserContext(t *testing.T) {
|
func TestUserContext(t *testing.T) {
|
||||||
t.Log("Testing user context")
|
t.Log("Testing user context")
|
||||||
@@ -238,11 +158,11 @@ func TestUserContext(t *testing.T) {
|
|||||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||||
|
|
||||||
// Read the body of the response
|
// Read the body of the response
|
||||||
body, err := io.ReadAll(recorder.Body)
|
body, bodyErr := io.ReadAll(recorder.Body)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if bodyErr != nil {
|
||||||
t.Fatalf("Error getting body: %v", err)
|
t.Fatalf("Error getting body: %v", bodyErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
// Unmarshal the body into the user struct
|
||||||
@@ -252,11 +172,11 @@ func TestUserContext(t *testing.T) {
|
|||||||
|
|
||||||
var user User
|
var user User
|
||||||
|
|
||||||
err = json.Unmarshal(body, &user)
|
jsonErr := json.Unmarshal(body, &user)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if jsonErr != nil {
|
||||||
t.Fatalf("Error unmarshalling body: %v", err)
|
t.Fatalf("Error unmarshalling body: %v", jsonErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should get the username back
|
// We should get the username back
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v3.2.0
|
v3.1.0
|
||||||
@@ -1,64 +1,38 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
"tinyauth/internal/docker"
|
"tinyauth/internal/docker"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
|
func NewAuth(docker *docker.Docker, userList types.Users, oauthWhitelist []string, sessionExpiry int) *Auth {
|
||||||
return &Auth{
|
return &Auth{
|
||||||
Config: config,
|
Docker: docker,
|
||||||
Docker: docker,
|
Users: userList,
|
||||||
LoginAttempts: make(map[string]*types.LoginAttempt),
|
OAuthWhitelist: oauthWhitelist,
|
||||||
|
SessionExpiry: sessionExpiry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
Config types.AuthConfig
|
Users types.Users
|
||||||
Docker *docker.Docker
|
Docker *docker.Docker
|
||||||
LoginAttempts map[string]*types.LoginAttempt
|
OAuthWhitelist []string
|
||||||
LoginMutex sync.RWMutex
|
SessionExpiry int
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
// Loop through users and return the user if the username matches
|
// Loop through users and return the user if the username matches
|
||||||
for _, user := range auth.Config.Users {
|
for _, user := range auth.Users {
|
||||||
if user.Username == username {
|
if user.Username == username {
|
||||||
return &user
|
return &user
|
||||||
}
|
}
|
||||||
@@ -71,78 +45,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.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.OAuthWhitelist {
|
||||||
if email == emailSrc {
|
if email == emailSrc {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -152,15 +62,11 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
|
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) {
|
||||||
log.Debug().Msg("Creating session cookie")
|
log.Debug().Msg("Creating session cookie")
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
session, err := auth.GetSession(c)
|
sessions := sessions.Default(c)
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to get session")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Setting session cookie")
|
log.Debug().Msg("Setting session cookie")
|
||||||
|
|
||||||
@@ -170,73 +76,54 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
|
|||||||
if data.TotpPending {
|
if data.TotpPending {
|
||||||
sessionExpiry = 3600
|
sessionExpiry = 3600
|
||||||
} else {
|
} else {
|
||||||
sessionExpiry = auth.Config.SessionExpiry
|
sessionExpiry = auth.SessionExpiry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set data
|
// Set data
|
||||||
session.Values["username"] = data.Username
|
sessions.Set("username", data.Username)
|
||||||
session.Values["provider"] = data.Provider
|
sessions.Set("provider", data.Provider)
|
||||||
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
|
sessions.Set("expiry", time.Now().Add(time.Duration(sessionExpiry)*time.Second).Unix())
|
||||||
session.Values["totpPending"] = data.TotpPending
|
sessions.Set("totpPending", data.TotpPending)
|
||||||
session.Values["redirectURI"] = data.RedirectURI
|
|
||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
err = session.Save(c.Request, c.Writer)
|
sessions.Save()
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to save session")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return nil
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
func (auth *Auth) DeleteSessionCookie(c *gin.Context) {
|
||||||
log.Debug().Msg("Deleting session cookie")
|
log.Debug().Msg("Deleting session cookie")
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
session, err := auth.GetSession(c)
|
sessions := sessions.Default(c)
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to get session")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all values in the session
|
// Clear session
|
||||||
for key := range session.Values {
|
sessions.Clear()
|
||||||
delete(session.Values, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
err = session.Save(c.Request, c.Writer)
|
sessions.Save()
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to save session")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return nil
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie {
|
||||||
log.Debug().Msg("Getting session cookie")
|
log.Debug().Msg("Getting session cookie")
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
session, err := auth.GetSession(c)
|
sessions := sessions.Default(c)
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to get session")
|
|
||||||
return types.SessionCookie{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get data from session
|
// Get data
|
||||||
username, usernameOk := session.Values["username"].(string)
|
cookieUsername := sessions.Get("username")
|
||||||
provider, providerOK := session.Values["provider"].(string)
|
cookieProvider := sessions.Get("provider")
|
||||||
redirectURI, redirectOK := session.Values["redirectURI"].(string)
|
cookieExpiry := sessions.Get("expiry")
|
||||||
expiry, expiryOk := session.Values["expiry"].(int64)
|
cookieTotpPending := sessions.Get("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)
|
||||||
return types.SessionCookie{}, nil
|
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{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the cookie has expired
|
// Check if the cookie has expired
|
||||||
@@ -247,7 +134,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
|
|||||||
auth.DeleteSessionCookie(c)
|
auth.DeleteSessionCookie(c)
|
||||||
|
|
||||||
// Return empty cookie
|
// Return empty cookie
|
||||||
return types.SessionCookie{}, nil
|
return types.SessionCookie{}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Msg("Parsed cookie")
|
log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Msg("Parsed cookie")
|
||||||
@@ -257,13 +144,12 @@ 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) UserAuthConfigured() bool {
|
func (auth *Auth) UserAuthConfigured() bool {
|
||||||
// If there are users, return true
|
// If there are users, return true
|
||||||
return len(auth.Config.Users) > 0
|
return len(auth.Users) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) {
|
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) {
|
||||||
@@ -273,37 +159,41 @@ func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bo
|
|||||||
// Get app id
|
// Get app id
|
||||||
appId := strings.Split(host, ".")[0]
|
appId := strings.Split(host, ".")[0]
|
||||||
|
|
||||||
// Get the container labels
|
// Check if resource is allowed
|
||||||
labels, err := auth.Docker.GetLabels(appId)
|
allowed, allowedErr := auth.Docker.ContainerAction(appId, func(labels types.TinyauthLabels) (bool, error) {
|
||||||
|
// If the container has an oauth whitelist, check if the user is in it
|
||||||
|
if context.OAuth {
|
||||||
|
if len(labels.OAuthWhitelist) == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
log.Debug().Msg("Checking OAuth whitelist")
|
||||||
|
if slices.Contains(labels.OAuthWhitelist, context.Username) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the container has users, check if the user is in it
|
||||||
|
if len(labels.Users) != 0 {
|
||||||
|
log.Debug().Msg("Checking users")
|
||||||
|
if slices.Contains(labels.Users, context.Username) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowed
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
|
||||||
// If there is an error, return false
|
// If there is an error, return false
|
||||||
if err != nil {
|
if allowedErr != nil {
|
||||||
return false, err
|
log.Error().Err(allowedErr).Msg("Error checking if resource is allowed")
|
||||||
|
return false, allowedErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if oauth is allowed
|
// Return if the resource is allowed
|
||||||
if context.OAuth {
|
return allowed, nil
|
||||||
if len(labels.OAuthWhitelist) == 0 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
log.Debug().Msg("Checking OAuth whitelist")
|
|
||||||
if slices.Contains(labels.OAuthWhitelist, context.Username) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is allowed
|
|
||||||
if len(labels.Users) != 0 {
|
|
||||||
log.Debug().Msg("Checking users")
|
|
||||||
if slices.Contains(labels.Users, context.Username) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not allowed
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
|
func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
|
||||||
@@ -314,37 +204,40 @@ func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
|
|||||||
// Get app id
|
// Get app id
|
||||||
appId := strings.Split(host, ".")[0]
|
appId := strings.Split(host, ".")[0]
|
||||||
|
|
||||||
// Get the container labels
|
// Check if auth is enabled
|
||||||
labels, err := auth.Docker.GetLabels(appId)
|
enabled, enabledErr := auth.Docker.ContainerAction(appId, func(labels types.TinyauthLabels) (bool, error) {
|
||||||
|
// Check if the allowed label is empty
|
||||||
|
if labels.Allowed == "" {
|
||||||
|
// Auth enabled
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// If there is an error, auth enabled
|
// Compile regex
|
||||||
if err != nil {
|
regex, regexErr := regexp.Compile(labels.Allowed)
|
||||||
return true, err
|
|
||||||
}
|
// If there is an error, invalid regex, auth enabled
|
||||||
|
if regexErr != nil {
|
||||||
|
log.Warn().Err(regexErr).Msg("Invalid regex")
|
||||||
|
return true, regexErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the uri matches the regex
|
||||||
|
if regex.MatchString(uri) {
|
||||||
|
// Auth disabled
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the allowed label is empty
|
|
||||||
if labels.Allowed == "" {
|
|
||||||
// Auth enabled
|
// Auth enabled
|
||||||
return true, nil
|
return true, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// If there is an error, auth enabled
|
||||||
|
if enabledErr != nil {
|
||||||
|
log.Error().Err(enabledErr).Msg("Error checking if auth is enabled")
|
||||||
|
return true, enabledErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile regex
|
return enabled, nil
|
||||||
regex, err := regexp.Compile(labels.Allowed)
|
|
||||||
|
|
||||||
// If there is an error, invalid regex, auth enabled
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("Invalid regex")
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the uri matches the regex
|
|
||||||
if regex.MatchString(uri) {
|
|
||||||
// Auth disabled
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth enabled
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User {
|
func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User {
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,5 +5,4 @@ var TinyauthLabels = []string{
|
|||||||
"tinyauth.oauth.whitelist",
|
"tinyauth.oauth.whitelist",
|
||||||
"tinyauth.users",
|
"tinyauth.users",
|
||||||
"tinyauth.allowed",
|
"tinyauth.allowed",
|
||||||
"tinyauth.headers",
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package docker
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
"tinyauth/internal/types"
|
appTypes "tinyauth/internal/types"
|
||||||
"tinyauth/internal/utils"
|
"tinyauth/internal/utils"
|
||||||
|
|
||||||
apiTypes "github.com/docker/docker/api/types"
|
apiTypes "github.com/docker/docker/api/types"
|
||||||
@@ -23,7 +23,7 @@ type Docker struct {
|
|||||||
|
|
||||||
func (docker *Docker) Init() error {
|
func (docker *Docker) Init() error {
|
||||||
// Create a new docker client
|
// Create a new docker client
|
||||||
client, err := client.NewClientWithOpts(client.FromEnv)
|
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -32,7 +32,7 @@ func (docker *Docker) Init() error {
|
|||||||
|
|
||||||
// Set the context and api client
|
// Set the context and api client
|
||||||
docker.Context = context.Background()
|
docker.Context = context.Background()
|
||||||
docker.Client = client
|
docker.Client = apiClient
|
||||||
|
|
||||||
// Done
|
// Done
|
||||||
return nil
|
return nil
|
||||||
@@ -70,22 +70,22 @@ func (docker *Docker) DockerConnected() bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) {
|
func (docker *Docker) ContainerAction(appId string, runCheck func(labels appTypes.TinyauthLabels) (bool, error)) (bool, error) {
|
||||||
// Check if we have access to the Docker API
|
// Check if we have access to the Docker API
|
||||||
isConnected := docker.DockerConnected()
|
isConnected := docker.DockerConnected()
|
||||||
|
|
||||||
// If we don't have access, return an empty struct
|
// If we don't have access, it is assumed that the check passed
|
||||||
if !isConnected {
|
if !isConnected {
|
||||||
log.Debug().Msg("Docker not connected, returning empty labels")
|
log.Debug().Msg("Docker not connected, passing check")
|
||||||
return types.TinyauthLabels{}, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the containers
|
// Get the containers
|
||||||
containers, err := docker.GetContainers()
|
containers, containersErr := docker.GetContainers()
|
||||||
|
|
||||||
// If there is an error, return false
|
// If there is an error, return false
|
||||||
if err != nil {
|
if containersErr != nil {
|
||||||
return types.TinyauthLabels{}, err
|
return false, containersErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got containers")
|
log.Debug().Msg("Got containers")
|
||||||
@@ -93,15 +93,15 @@ func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) {
|
|||||||
// Loop through the containers
|
// Loop through the containers
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
// Inspect the container
|
// Inspect the container
|
||||||
inspect, err := docker.InspectContainer(container.ID)
|
inspect, inspectErr := docker.InspectContainer(container.ID)
|
||||||
|
|
||||||
// If there is an error, return false
|
// If there is an error, return false
|
||||||
if err != nil {
|
if inspectErr != nil {
|
||||||
return types.TinyauthLabels{}, err
|
return false, inspectErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the container name (for some reason it is /name)
|
// Get the container name (for some reason it is /name)
|
||||||
containerName := strings.TrimPrefix(inspect.Name, "/")
|
containerName := strings.Split(inspect.Name, "/")[1]
|
||||||
|
|
||||||
// There is a container with the same name as the app ID
|
// There is a container with the same name as the app ID
|
||||||
if containerName == appId {
|
if containerName == appId {
|
||||||
@@ -112,14 +112,14 @@ func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) {
|
|||||||
|
|
||||||
log.Debug().Msg("Got labels")
|
log.Debug().Msg("Got labels")
|
||||||
|
|
||||||
// Return labels
|
// Run the check
|
||||||
return labels, nil
|
return runCheck(labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("No matching container found, returning empty labels")
|
log.Debug().Msg("No matching container found, passing check")
|
||||||
|
|
||||||
// If no matching container is found, return empty labels
|
// If no matching container is found, pass check
|
||||||
return types.TinyauthLabels{}, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,47 +2,76 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"tinyauth/internal/auth"
|
"tinyauth/internal/auth"
|
||||||
"tinyauth/internal/docker"
|
|
||||||
"tinyauth/internal/hooks"
|
"tinyauth/internal/hooks"
|
||||||
"tinyauth/internal/providers"
|
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/pquerna/otp/totp"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
|
func NewHandlers(config types.APIConfig, auth *auth.Auth, hooks *hooks.Hooks) *Handlers {
|
||||||
return &Handlers{
|
return &Handlers{
|
||||||
Config: config,
|
Config: config,
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
Hooks: hooks,
|
Hooks: hooks,
|
||||||
Providers: providers,
|
|
||||||
Docker: docker,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
Config types.HandlersConfig
|
Config types.APIConfig
|
||||||
Auth *auth.Auth
|
Auth *auth.Auth
|
||||||
Hooks *hooks.Hooks
|
Hooks *hooks.Hooks
|
||||||
Providers *providers.Providers
|
|
||||||
Docker *docker.Docker
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) AuthHandler(c *gin.Context) {
|
// @Summary Health Check
|
||||||
// Create struct for proxy
|
// @Description Simple health check
|
||||||
|
// @Tags health
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} types.HealthCheckResponse
|
||||||
|
// @Router /healthcheck [get]
|
||||||
|
func (h *Handlers) HealthCheck(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"message": "OK",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Logout
|
||||||
|
// @Description Log the user out by invalidating the session cookie
|
||||||
|
// @Tags auth
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} types.LogoutResponse
|
||||||
|
// @Router /auth/logout [get]
|
||||||
|
func (h *Handlers) Logout(c *gin.Context) {
|
||||||
|
log.Debug().Msg("Logging out")
|
||||||
|
|
||||||
|
h.Auth.DeleteSessionCookie(c)
|
||||||
|
|
||||||
|
log.Debug().Msg("Cleaning up redirect cookie")
|
||||||
|
|
||||||
|
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true)
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"message": "Logged out",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Auth Check (Traefik)
|
||||||
|
// @Description Check the authentication status of the user and redirect to the login page if not authenticated
|
||||||
|
// @Tags authn
|
||||||
|
// @Produce json
|
||||||
|
// @Success 302
|
||||||
|
// @Router /api/auth/traefik [get]
|
||||||
|
func (h *Handlers) CheckAuth(c *gin.Context) {
|
||||||
var proxy types.Proxy
|
var proxy types.Proxy
|
||||||
|
|
||||||
// Bind URI
|
|
||||||
err := c.BindUri(&proxy)
|
err := c.BindUri(&proxy)
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to bind URI")
|
log.Error().Err(err).Msg("Failed to bind URI")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, gin.H{
|
||||||
@@ -52,7 +81,6 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the request is coming from a browser (tools like curl/bruno use */* and they don't include the text/html)
|
|
||||||
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
|
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
|
||||||
|
|
||||||
if isBrowser {
|
if isBrowser {
|
||||||
@@ -63,17 +91,10 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy")
|
log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy")
|
||||||
|
|
||||||
// Get headers
|
|
||||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
|
||||||
proto := c.Request.Header.Get("X-Forwarded-Proto")
|
|
||||||
host := c.Request.Header.Get("X-Forwarded-Host")
|
|
||||||
|
|
||||||
// Check if auth is enabled
|
|
||||||
authEnabled, err := h.Auth.AuthEnabled(c)
|
authEnabled, err := h.Auth.AuthEnabled(c)
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to check if app is allowed")
|
log.Error().Err(err).Msg("Failed to check if auth is enabled")
|
||||||
|
|
||||||
if proxy.Proxy == "nginx" || !isBrowser {
|
if proxy.Proxy == "nginx" || !isBrowser {
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
@@ -87,34 +108,7 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the app id
|
|
||||||
appId := strings.Split(host, ".")[0]
|
|
||||||
|
|
||||||
// Get the container labels
|
|
||||||
labels, err := h.Docker.GetLabels(appId)
|
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to check if app is allowed")
|
|
||||||
|
|
||||||
if proxy.Proxy == "nginx" || !isBrowser {
|
|
||||||
c.JSON(500, gin.H{
|
|
||||||
"status": 500,
|
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If auth is not enabled, return 200
|
|
||||||
if !authEnabled {
|
if !authEnabled {
|
||||||
for key, value := range labels.Headers {
|
|
||||||
log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
|
|
||||||
c.Header(key, value)
|
|
||||||
}
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Authenticated",
|
"message": "Authenticated",
|
||||||
@@ -122,17 +116,17 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user context
|
|
||||||
userContext := h.Hooks.UseUserContext(c)
|
userContext := h.Hooks.UseUserContext(c)
|
||||||
|
|
||||||
// Check if user is logged in
|
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||||
|
proto := c.Request.Header.Get("X-Forwarded-Proto")
|
||||||
|
host := c.Request.Header.Get("X-Forwarded-Host")
|
||||||
|
|
||||||
if userContext.IsLoggedIn {
|
if userContext.IsLoggedIn {
|
||||||
log.Debug().Msg("Authenticated")
|
log.Debug().Msg("Authenticated")
|
||||||
|
|
||||||
// Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx
|
|
||||||
appAllowed, err := h.Auth.ResourceAllowed(c, userContext)
|
appAllowed, err := h.Auth.ResourceAllowed(c, userContext)
|
||||||
|
|
||||||
// Check if there was an error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to check if app is allowed")
|
log.Error().Err(err).Msg("Failed to check if app is allowed")
|
||||||
|
|
||||||
@@ -150,11 +144,9 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed")
|
log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed")
|
||||||
|
|
||||||
// The user is not allowed to access the app
|
|
||||||
if !appAllowed {
|
if !appAllowed {
|
||||||
log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User not allowed")
|
log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User not allowed")
|
||||||
|
|
||||||
// Set WWW-Authenticate header
|
|
||||||
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
|
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
|
||||||
|
|
||||||
if proxy.Proxy == "nginx" || !isBrowser {
|
if proxy.Proxy == "nginx" || !isBrowser {
|
||||||
@@ -165,34 +157,23 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build query
|
|
||||||
queries, err := query.Values(types.UnauthorizedQuery{
|
queries, err := query.Values(types.UnauthorizedQuery{
|
||||||
Username: userContext.Username,
|
Username: userContext.Username,
|
||||||
Resource: strings.Split(host, ".")[0],
|
Resource: strings.Split(host, ".")[0],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to build queries")
|
log.Error().Err(err).Msg("Failed to build query")
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are using caddy/traefik so redirect
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode()))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the user header
|
|
||||||
c.Header("Remote-User", userContext.Username)
|
c.Header("Remote-User", userContext.Username)
|
||||||
|
|
||||||
// Set the rest of the headers
|
|
||||||
for key, value := range labels.Headers {
|
|
||||||
log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
|
|
||||||
c.Header(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The user is allowed to access the app
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Authenticated",
|
"message": "Authenticated",
|
||||||
@@ -200,10 +181,8 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user is not logged in
|
|
||||||
log.Debug().Msg("Unauthorized")
|
log.Debug().Msg("Unauthorized")
|
||||||
|
|
||||||
// Set www-authenticate header
|
|
||||||
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
|
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
|
||||||
|
|
||||||
if proxy.Proxy == "nginx" || !isBrowser {
|
if proxy.Proxy == "nginx" || !isBrowser {
|
||||||
@@ -219,474 +198,12 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to build queries")
|
log.Error().Err(err).Msg("Failed to build query")
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Interface("redirect_uri", fmt.Sprintf("%s://%s%s", proto, host, uri)).Msg("Redirecting to login")
|
log.Debug().Interface("redirect_uri", fmt.Sprintf("%s://%s%s", proto, host, uri)).Msg("Redirecting to login")
|
||||||
|
|
||||||
// Redirect to login
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/?%s", h.Config.AppURL, queries.Encode()))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/?%s", h.Config.AppURL, queries.Encode()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) LoginHandler(c *gin.Context) {
|
|
||||||
// Create login struct
|
|
||||||
var login types.LoginRequest
|
|
||||||
|
|
||||||
// Bind JSON
|
|
||||||
err := c.BindJSON(&login)
|
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to bind JSON")
|
|
||||||
c.JSON(400, gin.H{
|
|
||||||
"status": 400,
|
|
||||||
"message": "Bad Request",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Got login request")
|
|
||||||
|
|
||||||
// Get client IP for rate limiting
|
|
||||||
clientIP := c.ClientIP()
|
|
||||||
|
|
||||||
// Create an identifier for rate limiting (username or IP if username doesn't exist yet)
|
|
||||||
rateIdentifier := login.Username
|
|
||||||
if rateIdentifier == "" {
|
|
||||||
rateIdentifier = clientIP
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the account is locked due to too many failed attempts
|
|
||||||
locked, remainingTime := h.Auth.IsAccountLocked(rateIdentifier)
|
|
||||||
if locked {
|
|
||||||
log.Warn().Str("identifier", rateIdentifier).Int("remaining_seconds", remainingTime).Msg("Account is locked due to too many failed login attempts")
|
|
||||||
c.JSON(429, gin.H{
|
|
||||||
"status": 429,
|
|
||||||
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user based on username
|
|
||||||
user := h.Auth.GetUser(login.Username)
|
|
||||||
|
|
||||||
// User does not exist
|
|
||||||
if user == nil {
|
|
||||||
log.Debug().Str("username", login.Username).Msg("User not found")
|
|
||||||
// Record failed login attempt
|
|
||||||
h.Auth.RecordLoginAttempt(rateIdentifier, false)
|
|
||||||
c.JSON(401, gin.H{
|
|
||||||
"status": 401,
|
|
||||||
"message": "Unauthorized",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Got user")
|
|
||||||
|
|
||||||
// Check if password is correct
|
|
||||||
if !h.Auth.CheckPassword(*user, login.Password) {
|
|
||||||
log.Debug().Str("username", login.Username).Msg("Password incorrect")
|
|
||||||
// Record failed login attempt
|
|
||||||
h.Auth.RecordLoginAttempt(rateIdentifier, false)
|
|
||||||
c.JSON(401, gin.H{
|
|
||||||
"status": 401,
|
|
||||||
"message": "Unauthorized",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Password correct, checking totp")
|
|
||||||
|
|
||||||
// Record successful login attempt (will reset failed attempt counter)
|
|
||||||
h.Auth.RecordLoginAttempt(rateIdentifier, true)
|
|
||||||
|
|
||||||
// Check if user has totp enabled
|
|
||||||
if user.TotpSecret != "" {
|
|
||||||
log.Debug().Msg("Totp enabled")
|
|
||||||
|
|
||||||
// Set totp pending cookie
|
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
|
||||||
Username: login.Username,
|
|
||||||
Provider: "username",
|
|
||||||
TotpPending: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return totp required
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": 200,
|
|
||||||
"message": "Waiting for totp",
|
|
||||||
"totpPending": true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Stop further processing
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create session cookie with username as provider
|
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
|
||||||
Username: login.Username,
|
|
||||||
Provider: "username",
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return logged in
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": 200,
|
|
||||||
"message": "Logged in",
|
|
||||||
"totpPending": false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) TotpHandler(c *gin.Context) {
|
|
||||||
// Create totp struct
|
|
||||||
var totpReq types.TotpRequest
|
|
||||||
|
|
||||||
// Bind JSON
|
|
||||||
err := c.BindJSON(&totpReq)
|
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to bind JSON")
|
|
||||||
c.JSON(400, gin.H{
|
|
||||||
"status": 400,
|
|
||||||
"message": "Bad Request",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Checking totp")
|
|
||||||
|
|
||||||
// Get user context
|
|
||||||
userContext := h.Hooks.UseUserContext(c)
|
|
||||||
|
|
||||||
// Check if we have a user
|
|
||||||
if userContext.Username == "" {
|
|
||||||
log.Debug().Msg("No user context")
|
|
||||||
c.JSON(401, gin.H{
|
|
||||||
"status": 401,
|
|
||||||
"message": "Unauthorized",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user
|
|
||||||
user := h.Auth.GetUser(userContext.Username)
|
|
||||||
|
|
||||||
// Check if user exists
|
|
||||||
if user == nil {
|
|
||||||
log.Debug().Msg("User not found")
|
|
||||||
c.JSON(401, gin.H{
|
|
||||||
"status": 401,
|
|
||||||
"message": "Unauthorized",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if totp is correct
|
|
||||||
ok := totp.Validate(totpReq.Code, user.TotpSecret)
|
|
||||||
|
|
||||||
// TOTP is incorrect
|
|
||||||
if !ok {
|
|
||||||
log.Debug().Msg("Totp incorrect")
|
|
||||||
c.JSON(401, gin.H{
|
|
||||||
"status": 401,
|
|
||||||
"message": "Unauthorized",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Totp correct")
|
|
||||||
|
|
||||||
// Create session cookie with username as provider
|
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
|
||||||
Username: user.Username,
|
|
||||||
Provider: "username",
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return logged in
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": 200,
|
|
||||||
"message": "Logged in",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) LogoutHandler(c *gin.Context) {
|
|
||||||
log.Debug().Msg("Logging out")
|
|
||||||
|
|
||||||
// Delete session cookie
|
|
||||||
h.Auth.DeleteSessionCookie(c)
|
|
||||||
|
|
||||||
log.Debug().Msg("Cleaning up redirect cookie")
|
|
||||||
|
|
||||||
// Return logged out
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": 200,
|
|
||||||
"message": "Logged out",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) AppHandler(c *gin.Context) {
|
|
||||||
log.Debug().Msg("Getting app context")
|
|
||||||
|
|
||||||
// Get configured providers
|
|
||||||
configuredProviders := h.Providers.GetConfiguredProviders()
|
|
||||||
|
|
||||||
// We have username/password configured so add it to our providers
|
|
||||||
if h.Auth.UserAuthConfigured() {
|
|
||||||
configuredProviders = append(configuredProviders, "username")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create app context struct
|
|
||||||
appContext := types.AppContext{
|
|
||||||
Status: 200,
|
|
||||||
Message: "OK",
|
|
||||||
ConfiguredProviders: configuredProviders,
|
|
||||||
DisableContinue: h.Config.DisableContinue,
|
|
||||||
Title: h.Config.Title,
|
|
||||||
GenericName: h.Config.GenericName,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return app context
|
|
||||||
c.JSON(200, appContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) UserHandler(c *gin.Context) {
|
|
||||||
log.Debug().Msg("Getting user context")
|
|
||||||
|
|
||||||
// Get user context
|
|
||||||
userContext := h.Hooks.UseUserContext(c)
|
|
||||||
|
|
||||||
// Create user context response
|
|
||||||
userContextResponse := types.UserContextResponse{
|
|
||||||
Status: 200,
|
|
||||||
IsLoggedIn: userContext.IsLoggedIn,
|
|
||||||
Username: userContext.Username,
|
|
||||||
Provider: userContext.Provider,
|
|
||||||
Oauth: userContext.OAuth,
|
|
||||||
TotpPending: userContext.TotpPending,
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are not logged in we set the status to 401 and add the WWW-Authenticate header else we set it to 200
|
|
||||||
if !userContext.IsLoggedIn {
|
|
||||||
log.Debug().Msg("Unauthorized")
|
|
||||||
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
|
|
||||||
userContextResponse.Message = "Unauthorized"
|
|
||||||
} else {
|
|
||||||
log.Debug().Interface("userContext", userContext).Msg("Authenticated")
|
|
||||||
userContextResponse.Message = "Authenticated"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return user context
|
|
||||||
c.JSON(200, userContextResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) OauthUrlHandler(c *gin.Context) {
|
|
||||||
// Create struct for OAuth request
|
|
||||||
var request types.OAuthRequest
|
|
||||||
|
|
||||||
// Bind URI
|
|
||||||
err := c.BindUri(&request)
|
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to bind URI")
|
|
||||||
c.JSON(400, gin.H{
|
|
||||||
"status": 400,
|
|
||||||
"message": "Bad Request",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Got OAuth request")
|
|
||||||
|
|
||||||
// Check if provider exists
|
|
||||||
provider := h.Providers.GetProvider(request.Provider)
|
|
||||||
|
|
||||||
// Provider does not exist
|
|
||||||
if provider == nil {
|
|
||||||
c.JSON(404, gin.H{
|
|
||||||
"status": 404,
|
|
||||||
"message": "Not Found",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("provider", request.Provider).Msg("Got provider")
|
|
||||||
|
|
||||||
// Get auth URL
|
|
||||||
authURL := provider.GetAuthURL()
|
|
||||||
|
|
||||||
log.Debug().Msg("Got auth URL")
|
|
||||||
|
|
||||||
// Get redirect URI
|
|
||||||
redirectURI := c.Query("redirect_uri")
|
|
||||||
|
|
||||||
// Set redirect cookie if redirect URI is provided
|
|
||||||
if redirectURI != "" {
|
|
||||||
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
|
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
|
||||||
RedirectURI: redirectURI,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it
|
|
||||||
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
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": 200,
|
|
||||||
"message": "OK",
|
|
||||||
"url": authURL,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
|
|
||||||
// Create struct for OAuth request
|
|
||||||
var providerName types.OAuthRequest
|
|
||||||
|
|
||||||
// Bind URI
|
|
||||||
err := c.BindUri(&providerName)
|
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to bind URI")
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name")
|
|
||||||
|
|
||||||
// Get code
|
|
||||||
code := c.Query("code")
|
|
||||||
|
|
||||||
// Code empty so redirect to error
|
|
||||||
if code == "" {
|
|
||||||
log.Error().Msg("No code provided")
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Got code")
|
|
||||||
|
|
||||||
// Get provider
|
|
||||||
provider := h.Providers.GetProvider(providerName.Provider)
|
|
||||||
|
|
||||||
log.Debug().Str("provider", providerName.Provider).Msg("Got provider")
|
|
||||||
|
|
||||||
// Provider does not exist
|
|
||||||
if provider == nil {
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, "/not-found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exchange token (authenticates user)
|
|
||||||
_, err = provider.ExchangeToken(code)
|
|
||||||
|
|
||||||
log.Debug().Msg("Got token")
|
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msg("Failed to exchange token")
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get email
|
|
||||||
email, err := h.Providers.GetUser(providerName.Provider)
|
|
||||||
|
|
||||||
log.Debug().Str("email", email).Msg("Got email")
|
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msg("Failed to get email")
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email is not whitelisted
|
|
||||||
if !h.Auth.EmailWhitelisted(email) {
|
|
||||||
log.Warn().Str("email", email).Msg("Email not whitelisted")
|
|
||||||
|
|
||||||
// Build query
|
|
||||||
queries, err := query.Values(types.UnauthorizedQuery{
|
|
||||||
Username: email,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msg("Failed to build queries")
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to unauthorized
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode()))
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Email whitelisted")
|
|
||||||
|
|
||||||
// Get redirect URI
|
|
||||||
cookie, err := h.Auth.GetSessionCookie(c)
|
|
||||||
|
|
||||||
// Create session cookie (also cleans up redirect cookie)
|
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
|
||||||
Username: email,
|
|
||||||
Provider: providerName.Provider,
|
|
||||||
})
|
|
||||||
|
|
||||||
// If it is empty it means that no redirect_uri was provided to the login screen so we just log in
|
|
||||||
if err != nil {
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("redirectURI", cookie.RedirectURI).Msg("Got redirect URI")
|
|
||||||
|
|
||||||
// Build query
|
|
||||||
queries, err := query.Values(types.LoginQuery{
|
|
||||||
RedirectURI: cookie.RedirectURI,
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Debug().Msg("Got redirect query")
|
|
||||||
|
|
||||||
// Handle error
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msg("Failed to build queries")
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to continue with the redirect URI
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", h.Config.AppURL, queries.Encode()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) HealthcheckHandler(c *gin.Context) {
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": 200,
|
|
||||||
"message": "OK",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ 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 := hooks.Auth.GetSessionCookie(c)
|
||||||
basic := hooks.Auth.GetBasicAuth(c)
|
basic := hooks.Auth.GetBasicAuth(c)
|
||||||
|
|
||||||
// Check if basic auth is set
|
// Check if basic auth is set
|
||||||
@@ -46,19 +46,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")
|
||||||
|
|||||||
@@ -15,21 +15,21 @@ type GenericUserInfoResponse struct {
|
|||||||
|
|
||||||
func GetGenericEmail(client *http.Client, url string) (string, error) {
|
func GetGenericEmail(client *http.Client, url string) (string, error) {
|
||||||
// Using the oauth client get the user info url
|
// Using the oauth client get the user info url
|
||||||
res, err := client.Get(url)
|
res, resErr := client.Get(url)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if resErr != nil {
|
||||||
return "", err
|
return "", resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got response from generic provider")
|
log.Debug().Msg("Got response from generic provider")
|
||||||
|
|
||||||
// Read the body of the response
|
// Read the body of the response
|
||||||
body, err := io.ReadAll(res.Body)
|
body, bodyErr := io.ReadAll(res.Body)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if bodyErr != nil {
|
||||||
return "", err
|
return "", bodyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Read body from generic provider")
|
log.Debug().Msg("Read body from generic provider")
|
||||||
@@ -38,11 +38,11 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
|
|||||||
var user GenericUserInfoResponse
|
var user GenericUserInfoResponse
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
// Unmarshal the body into the user struct
|
||||||
err = json.Unmarshal(body, &user)
|
jsonErr := json.Unmarshal(body, &user)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if jsonErr != nil {
|
||||||
return "", err
|
return "", jsonErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Parsed user from generic provider")
|
log.Debug().Msg("Parsed user from generic provider")
|
||||||
|
|||||||
@@ -22,21 +22,21 @@ func GithubScopes() []string {
|
|||||||
|
|
||||||
func GetGithubEmail(client *http.Client) (string, error) {
|
func GetGithubEmail(client *http.Client) (string, error) {
|
||||||
// Get the user emails from github using the oauth http client
|
// Get the user emails from github using the oauth http client
|
||||||
res, err := client.Get("https://api.github.com/user/emails")
|
res, resErr := client.Get("https://api.github.com/user/emails")
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if resErr != nil {
|
||||||
return "", err
|
return "", resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got response from github")
|
log.Debug().Msg("Got response from github")
|
||||||
|
|
||||||
// Read the body of the response
|
// Read the body of the response
|
||||||
body, err := io.ReadAll(res.Body)
|
body, bodyErr := io.ReadAll(res.Body)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if bodyErr != nil {
|
||||||
return "", err
|
return "", bodyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Read body from github")
|
log.Debug().Msg("Read body from github")
|
||||||
@@ -45,11 +45,11 @@ func GetGithubEmail(client *http.Client) (string, error) {
|
|||||||
var emails GithubUserInfoResponse
|
var emails GithubUserInfoResponse
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
// Unmarshal the body into the user struct
|
||||||
err = json.Unmarshal(body, &emails)
|
jsonErr := json.Unmarshal(body, &emails)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if jsonErr != nil {
|
||||||
return "", err
|
return "", jsonErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Parsed emails from github")
|
log.Debug().Msg("Parsed emails from github")
|
||||||
|
|||||||
@@ -20,21 +20,21 @@ func GoogleScopes() []string {
|
|||||||
|
|
||||||
func GetGoogleEmail(client *http.Client) (string, error) {
|
func GetGoogleEmail(client *http.Client) (string, error) {
|
||||||
// Get the user info from google using the oauth http client
|
// Get the user info from google using the oauth http client
|
||||||
res, err := client.Get("https://www.googleapis.com/userinfo/v2/me")
|
res, resErr := client.Get("https://www.googleapis.com/userinfo/v2/me")
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if resErr != nil {
|
||||||
return "", err
|
return "", resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got response from google")
|
log.Debug().Msg("Got response from google")
|
||||||
|
|
||||||
// Read the body of the response
|
// Read the body of the response
|
||||||
body, err := io.ReadAll(res.Body)
|
body, bodyErr := io.ReadAll(res.Body)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if bodyErr != nil {
|
||||||
return "", err
|
return "", bodyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Read body from google")
|
log.Debug().Msg("Read body from google")
|
||||||
@@ -43,11 +43,11 @@ func GetGoogleEmail(client *http.Client) (string, error) {
|
|||||||
var user GoogleUserInfoResponse
|
var user GoogleUserInfoResponse
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
// Unmarshal the body into the user struct
|
||||||
err = json.Unmarshal(body, &user)
|
jsonErr := json.Unmarshal(body, &user)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if jsonErr != nil {
|
||||||
return "", err
|
return "", jsonErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Parsed user from google")
|
log.Debug().Msg("Parsed user from google")
|
||||||
|
|||||||
@@ -128,11 +128,11 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
|||||||
log.Debug().Msg("Got client from github")
|
log.Debug().Msg("Got client from github")
|
||||||
|
|
||||||
// Get the email from the github provider
|
// Get the email from the github provider
|
||||||
email, err := GetGithubEmail(client)
|
email, emailErr := GetGithubEmail(client)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if emailErr != nil {
|
||||||
return "", err
|
return "", emailErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got email from github")
|
log.Debug().Msg("Got email from github")
|
||||||
@@ -152,11 +152,11 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
|||||||
log.Debug().Msg("Got client from google")
|
log.Debug().Msg("Got client from google")
|
||||||
|
|
||||||
// Get the email from the google provider
|
// Get the email from the google provider
|
||||||
email, err := GetGoogleEmail(client)
|
email, emailErr := GetGoogleEmail(client)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if emailErr != nil {
|
||||||
return "", err
|
return "", emailErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got email from google")
|
log.Debug().Msg("Got email from google")
|
||||||
@@ -176,11 +176,11 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
|||||||
log.Debug().Msg("Got client from tailscale")
|
log.Debug().Msg("Got client from tailscale")
|
||||||
|
|
||||||
// Get the email from the tailscale provider
|
// Get the email from the tailscale provider
|
||||||
email, err := GetTailscaleEmail(client)
|
email, emailErr := GetTailscaleEmail(client)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if emailErr != nil {
|
||||||
return "", err
|
return "", emailErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got email from tailscale")
|
log.Debug().Msg("Got email from tailscale")
|
||||||
@@ -200,11 +200,11 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
|||||||
log.Debug().Msg("Got client from generic")
|
log.Debug().Msg("Got client from generic")
|
||||||
|
|
||||||
// Get the email from the generic provider
|
// Get the email from the generic provider
|
||||||
email, err := GetGenericEmail(client, providers.Config.GenericUserURL)
|
email, emailErr := GetGenericEmail(client, providers.Config.GenericUserURL)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if emailErr != nil {
|
||||||
return "", err
|
return "", emailErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got email from generic")
|
log.Debug().Msg("Got email from generic")
|
||||||
|
|||||||
@@ -31,21 +31,21 @@ var TailscaleEndpoint = oauth2.Endpoint{
|
|||||||
|
|
||||||
func GetTailscaleEmail(client *http.Client) (string, error) {
|
func GetTailscaleEmail(client *http.Client) (string, error) {
|
||||||
// Get the user info from tailscale using the oauth http client
|
// Get the user info from tailscale using the oauth http client
|
||||||
res, err := client.Get("https://api.tailscale.com/api/v2/tailnet/-/users")
|
res, resErr := client.Get("https://api.tailscale.com/api/v2/tailnet/-/users")
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if resErr != nil {
|
||||||
return "", err
|
return "", resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Got response from tailscale")
|
log.Debug().Msg("Got response from tailscale")
|
||||||
|
|
||||||
// Read the body of the response
|
// Read the body of the response
|
||||||
body, err := io.ReadAll(res.Body)
|
body, bodyErr := io.ReadAll(res.Body)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if bodyErr != nil {
|
||||||
return "", err
|
return "", bodyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Read body from tailscale")
|
log.Debug().Msg("Read body from tailscale")
|
||||||
@@ -54,11 +54,11 @@ func GetTailscaleEmail(client *http.Client) (string, error) {
|
|||||||
var users TailscaleUserInfoResponse
|
var users TailscaleUserInfoResponse
|
||||||
|
|
||||||
// Unmarshal the body into the user struct
|
// Unmarshal the body into the user struct
|
||||||
err = json.Unmarshal(body, &users)
|
jsonErr := json.Unmarshal(body, &users)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if jsonErr != nil {
|
||||||
return "", err
|
return "", jsonErr
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Parsed users from tailscale")
|
log.Debug().Msg("Parsed users from tailscale")
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// LoginQuery is the query parameters for the login endpoint
|
|
||||||
type LoginQuery struct {
|
|
||||||
RedirectURI string `url:"redirect_uri"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginRequest is the request body for the login endpoint
|
|
||||||
type LoginRequest struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuthRequest is the request for the OAuth endpoint
|
|
||||||
type OAuthRequest struct {
|
|
||||||
Provider string `uri:"provider" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnauthorizedQuery is the query parameters for the unauthorized endpoint
|
|
||||||
type UnauthorizedQuery struct {
|
|
||||||
Username string `url:"username"`
|
|
||||||
Resource string `url:"resource"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TailscaleQuery is the query parameters for the tailscale endpoint
|
|
||||||
type TailscaleQuery struct {
|
|
||||||
Code int `url:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proxy is the uri parameters for the proxy endpoint
|
|
||||||
type Proxy struct {
|
|
||||||
Proxy string `uri:"proxy" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// User Context response is the response for the user context endpoint
|
|
||||||
type UserContextResponse struct {
|
|
||||||
Status int `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
IsLoggedIn bool `json:"isLoggedIn"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
Oauth bool `json:"oauth"`
|
|
||||||
TotpPending bool `json:"totpPending"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// App Context is the response for the app context endpoint
|
|
||||||
type AppContext struct {
|
|
||||||
Status int `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
ConfiguredProviders []string `json:"configuredProviders"`
|
|
||||||
DisableContinue bool `json:"disableContinue"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
GenericName string `json:"genericName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Totp request is the request for the totp endpoint
|
|
||||||
type TotpRequest struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
}
|
|
||||||
@@ -1,81 +1,15 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
// Config is the configuration for the tinyauth server
|
// API config is the configuration for the API
|
||||||
type Config struct {
|
type APIConfig struct {
|
||||||
Port int `mapstructure:"port" validate:"required"`
|
Port int
|
||||||
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
Address string
|
||||||
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
Secret string
|
||||||
SecretFile string `mapstructure:"secret-file"`
|
|
||||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
|
||||||
Users string `mapstructure:"users"`
|
|
||||||
UsersFile string `mapstructure:"users-file"`
|
|
||||||
CookieSecure bool `mapstructure:"cookie-secure"`
|
|
||||||
GithubClientId string `mapstructure:"github-client-id"`
|
|
||||||
GithubClientSecret string `mapstructure:"github-client-secret"`
|
|
||||||
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
|
|
||||||
GoogleClientId string `mapstructure:"google-client-id"`
|
|
||||||
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
|
||||||
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
|
|
||||||
TailscaleClientId string `mapstructure:"tailscale-client-id"`
|
|
||||||
TailscaleClientSecret string `mapstructure:"tailscale-client-secret"`
|
|
||||||
TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"`
|
|
||||||
GenericClientId string `mapstructure:"generic-client-id"`
|
|
||||||
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
|
||||||
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
|
|
||||||
GenericScopes string `mapstructure:"generic-scopes"`
|
|
||||||
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
|
||||||
GenericTokenURL string `mapstructure:"generic-token-url"`
|
|
||||||
GenericUserURL string `mapstructure:"generic-user-url"`
|
|
||||||
GenericName string `mapstructure:"generic-name"`
|
|
||||||
DisableContinue bool `mapstructure:"disable-continue"`
|
|
||||||
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
|
||||||
SessionExpiry int `mapstructure:"session-expiry"`
|
|
||||||
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
|
|
||||||
Title string `mapstructure:"app-title"`
|
|
||||||
EnvFile string `mapstructure:"env-file"`
|
|
||||||
LoginTimeout int `mapstructure:"login-timeout"`
|
|
||||||
LoginMaxRetries int `mapstructure:"login-max-retries"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server configuration
|
|
||||||
type HandlersConfig struct {
|
|
||||||
AppURL string
|
AppURL string
|
||||||
|
CookieSecure bool
|
||||||
|
SessionExpiry int
|
||||||
DisableContinue bool
|
DisableContinue bool
|
||||||
GenericName string
|
GenericName string
|
||||||
Title string
|
Title string
|
||||||
}
|
|
||||||
|
|
||||||
// OAuthConfig is the configuration for the providers
|
|
||||||
type OAuthConfig struct {
|
|
||||||
GithubClientId string
|
|
||||||
GithubClientSecret string
|
|
||||||
GoogleClientId string
|
|
||||||
GoogleClientSecret string
|
|
||||||
TailscaleClientId string
|
|
||||||
TailscaleClientSecret string
|
|
||||||
GenericClientId string
|
|
||||||
GenericClientSecret string
|
|
||||||
GenericScopes []string
|
|
||||||
GenericAuthURL string
|
|
||||||
GenericTokenURL string
|
|
||||||
GenericUserURL string
|
|
||||||
AppURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIConfig is the configuration for the API
|
|
||||||
type APIConfig struct {
|
|
||||||
Port int
|
|
||||||
Address string
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthConfig is the configuration for the auth service
|
|
||||||
type AuthConfig struct {
|
|
||||||
Users Users
|
|
||||||
OauthWhitelist []string
|
|
||||||
SessionExpiry int
|
|
||||||
Secret string
|
|
||||||
CookieSecure bool
|
|
||||||
Domain string
|
Domain string
|
||||||
LoginTimeout int
|
|
||||||
LoginMaxRetries int
|
|
||||||
}
|
}
|
||||||
|
|||||||
13
internal/types/handlers.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// HealthCheckResponse is the response for the health check endpoint
|
||||||
|
type HealthCheckResponse struct {
|
||||||
|
Status int `json:"status" example:"200"`
|
||||||
|
Message string `json:"message" example:"Ok"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogoutResponse is the response for the health check endpoint
|
||||||
|
type LogoutResponse struct {
|
||||||
|
Status int `json:"status" example:"200"`
|
||||||
|
Message string `json:"message" example:"Logged out"`
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import "tinyauth/internal/oauth"
|
||||||
"time"
|
|
||||||
"tinyauth/internal/oauth"
|
// LoginQuery is the query parameters for the login endpoint
|
||||||
)
|
type LoginQuery struct {
|
||||||
|
RedirectURI string `url:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRequest is the request body for the login endpoint
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
// User is the struct for a user
|
// User is the struct for a user
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -15,27 +23,39 @@ type User struct {
|
|||||||
// Users is a list of users
|
// Users is a list of users
|
||||||
type Users []User
|
type Users []User
|
||||||
|
|
||||||
// OAuthProviders is the struct for the OAuth providers
|
// Config is the configuration for the tinyauth server
|
||||||
type OAuthProviders struct {
|
type Config struct {
|
||||||
Github *oauth.OAuth
|
Port int `mapstructure:"port" validate:"required"`
|
||||||
Google *oauth.OAuth
|
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
||||||
Microsoft *oauth.OAuth
|
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
||||||
}
|
SecretFile string `mapstructure:"secret-file"`
|
||||||
|
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||||
// SessionCookie is the cookie for the session (exculding the expiry)
|
Users string `mapstructure:"users"`
|
||||||
type SessionCookie struct {
|
UsersFile string `mapstructure:"users-file"`
|
||||||
Username string
|
CookieSecure bool `mapstructure:"cookie-secure"`
|
||||||
Provider string
|
GithubClientId string `mapstructure:"github-client-id"`
|
||||||
TotpPending bool
|
GithubClientSecret string `mapstructure:"github-client-secret"`
|
||||||
RedirectURI string
|
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
|
||||||
}
|
GoogleClientId string `mapstructure:"google-client-id"`
|
||||||
|
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
||||||
// TinyauthLabels is the labels for the tinyauth container
|
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
|
||||||
type TinyauthLabels struct {
|
TailscaleClientId string `mapstructure:"tailscale-client-id"`
|
||||||
OAuthWhitelist []string
|
TailscaleClientSecret string `mapstructure:"tailscale-client-secret"`
|
||||||
Users []string
|
TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"`
|
||||||
Allowed string
|
GenericClientId string `mapstructure:"generic-client-id"`
|
||||||
Headers map[string]string
|
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
||||||
|
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
|
||||||
|
GenericScopes string `mapstructure:"generic-scopes"`
|
||||||
|
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
||||||
|
GenericTokenURL string `mapstructure:"generic-token-url"`
|
||||||
|
GenericUserURL string `mapstructure:"generic-user-url"`
|
||||||
|
GenericName string `mapstructure:"generic-name"`
|
||||||
|
DisableContinue bool `mapstructure:"disable-continue"`
|
||||||
|
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
||||||
|
SessionExpiry int `mapstructure:"session-expiry"`
|
||||||
|
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
|
||||||
|
Title string `mapstructure:"app-title"`
|
||||||
|
EnvFile string `mapstructure:"env-file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserContext is the context for the user
|
// UserContext is the context for the user
|
||||||
@@ -47,9 +67,87 @@ type UserContext struct {
|
|||||||
TotpPending bool
|
TotpPending bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginAttempt tracks information about login attempts for rate limiting
|
// OAuthConfig is the configuration for the providers
|
||||||
type LoginAttempt struct {
|
type OAuthConfig struct {
|
||||||
FailedAttempts int
|
GithubClientId string
|
||||||
LastAttempt time.Time
|
GithubClientSecret string
|
||||||
LockedUntil time.Time
|
GoogleClientId string
|
||||||
|
GoogleClientSecret string
|
||||||
|
TailscaleClientId string
|
||||||
|
TailscaleClientSecret string
|
||||||
|
GenericClientId string
|
||||||
|
GenericClientSecret string
|
||||||
|
GenericScopes []string
|
||||||
|
GenericAuthURL string
|
||||||
|
GenericTokenURL string
|
||||||
|
GenericUserURL string
|
||||||
|
AppURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthRequest is the request for the OAuth endpoint
|
||||||
|
type OAuthRequest struct {
|
||||||
|
Provider string `uri:"provider" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthProviders is the struct for the OAuth providers
|
||||||
|
type OAuthProviders struct {
|
||||||
|
Github *oauth.OAuth
|
||||||
|
Google *oauth.OAuth
|
||||||
|
Microsoft *oauth.OAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnauthorizedQuery is the query parameters for the unauthorized endpoint
|
||||||
|
type UnauthorizedQuery struct {
|
||||||
|
Username string `url:"username"`
|
||||||
|
Resource string `url:"resource"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionCookie is the cookie for the session (exculding the expiry)
|
||||||
|
type SessionCookie struct {
|
||||||
|
Username string
|
||||||
|
Provider string
|
||||||
|
TotpPending bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TinyauthLabels is the labels for the tinyauth container
|
||||||
|
type TinyauthLabels struct {
|
||||||
|
OAuthWhitelist []string
|
||||||
|
Users []string
|
||||||
|
Allowed string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TailscaleQuery is the query parameters for the tailscale endpoint
|
||||||
|
type TailscaleQuery struct {
|
||||||
|
Code int `url:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy is the uri parameters for the proxy endpoint
|
||||||
|
type Proxy struct {
|
||||||
|
Proxy string `uri:"proxy" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Context response is the response for the user context endpoint
|
||||||
|
type UserContextResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
IsLoggedIn bool `json:"isLoggedIn"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Oauth bool `json:"oauth"`
|
||||||
|
TotpPending bool `json:"totpPending"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Context is the response for the app context endpoint
|
||||||
|
type AppContext struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
ConfiguredProviders []string `json:"configuredProviders"`
|
||||||
|
DisableContinue bool `json:"disableContinue"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
GenericName string `json:"genericName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Totp request is the request for the totp endpoint
|
||||||
|
type TotpRequest struct {
|
||||||
|
Code string `json:"code"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ func ParseUsers(users string) (types.Users, error) {
|
|||||||
|
|
||||||
// Loop through the users and split them by colon
|
// Loop through the users and split them by colon
|
||||||
for _, user := range userList {
|
for _, user := range userList {
|
||||||
parsed, err := ParseUser(user)
|
parsed, parseErr := ParseUser(user)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if parseErr != nil {
|
||||||
return types.Users{}, err
|
return types.Users{}, parseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the user to the users struct
|
// Append the user to the users struct
|
||||||
@@ -46,14 +46,14 @@ func ParseUsers(users string) (types.Users, error) {
|
|||||||
return usersParsed, nil
|
return usersParsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get upper domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com)
|
// Root url parses parses a hostname and returns the root domain (e.g. sub1.sub2.domain.com -> sub2.domain.com)
|
||||||
func GetUpperDomain(urlSrc string) (string, error) {
|
func GetRootURL(urlSrc string) (string, error) {
|
||||||
// Make sure the url is valid
|
// Make sure the url is valid
|
||||||
urlParsed, err := url.Parse(urlSrc)
|
urlParsed, parseErr := url.Parse(urlSrc)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if parseErr != nil {
|
||||||
return "", err
|
return "", parseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the hostname by period
|
// Split the hostname by period
|
||||||
@@ -69,19 +69,19 @@ func GetUpperDomain(urlSrc string) (string, error) {
|
|||||||
// Reads a file and returns the contents
|
// Reads a file and returns the contents
|
||||||
func ReadFile(file string) (string, error) {
|
func ReadFile(file string) (string, error) {
|
||||||
// Check if the file exists
|
// Check if the file exists
|
||||||
_, err := os.Stat(file)
|
_, statErr := os.Stat(file)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if statErr != nil {
|
||||||
return "", err
|
return "", statErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the file
|
// Read the file
|
||||||
data, err := os.ReadFile(file)
|
data, readErr := os.ReadFile(file)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if readErr != nil {
|
||||||
return "", err
|
return "", readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the file contents
|
// Return the file contents
|
||||||
@@ -152,10 +152,10 @@ func GetUsers(conf string, file string) (types.Users, error) {
|
|||||||
// If the file is set, read the file and append the users to the users string
|
// If the file is set, read the file and append the users to the users string
|
||||||
if file != "" {
|
if file != "" {
|
||||||
// Read the file
|
// Read the file
|
||||||
contents, err := ReadFile(file)
|
fileContents, fileErr := ReadFile(file)
|
||||||
|
|
||||||
// If there isn't an error we can append the users to the users string
|
// If there isn't an error we can append the users to the users string
|
||||||
if err == nil {
|
if fileErr == nil {
|
||||||
log.Debug().Msg("Using users from file")
|
log.Debug().Msg("Using users from file")
|
||||||
|
|
||||||
// Append the users to the users string
|
// Append the users to the users string
|
||||||
@@ -164,7 +164,7 @@ func GetUsers(conf string, file string) (types.Users, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse the file contents into a comma separated list of users
|
// Parse the file contents into a comma separated list of users
|
||||||
users += ParseFileToLine(contents)
|
users += ParseFileToLine(fileContents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,16 +193,6 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
|
|||||||
tinyauthLabels.Users = strings.Split(value, ",")
|
tinyauthLabels.Users = strings.Split(value, ",")
|
||||||
case "tinyauth.allowed":
|
case "tinyauth.allowed":
|
||||||
tinyauthLabels.Allowed = value
|
tinyauthLabels.Allowed = value
|
||||||
case "tinyauth.headers":
|
|
||||||
tinyauthLabels.Headers = make(map[string]string)
|
|
||||||
headers := strings.Split(value, ",")
|
|
||||||
for _, header := range headers {
|
|
||||||
headerSplit := strings.Split(header, "=")
|
|
||||||
if len(headerSplit) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,15 +38,15 @@ func TestParseUsers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the get upper domain function
|
// Test the get root url function
|
||||||
func TestGetUpperDomain(t *testing.T) {
|
func TestGetRootURL(t *testing.T) {
|
||||||
t.Log("Testing get upper domain with a valid url")
|
t.Log("Testing get root url with a valid url")
|
||||||
|
|
||||||
// Test the get upper domain function with a valid url
|
// Test the get root url function with a valid url
|
||||||
url := "https://sub1.sub2.domain.com:8080"
|
url := "https://sub1.sub2.domain.com:8080"
|
||||||
expected := "sub2.domain.com"
|
expected := "sub2.domain.com"
|
||||||
|
|
||||||
result, err := utils.GetUpperDomain(url)
|
result, err := utils.GetRootURL(url)
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -102,7 +102,7 @@ func TestParseFileToLine(t *testing.T) {
|
|||||||
t.Log("Testing parse file to line with a valid string")
|
t.Log("Testing parse file to line with a valid string")
|
||||||
|
|
||||||
// Test the parse file to line function with a valid string
|
// Test the parse file to line function with a valid string
|
||||||
content := "\nuser1:pass1\nuser2:pass2\n"
|
content := "user1:pass1\nuser2:pass2"
|
||||||
expected := "user1:pass1,user2:pass2"
|
expected := "user1:pass1,user2:pass2"
|
||||||
|
|
||||||
result := utils.ParseFileToLine(content)
|
result := utils.ParseFileToLine(content)
|
||||||
|
|||||||
0
frontend/.gitignore → site/.gitignore
vendored
23
site/Dockerfile.dev
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM oven/bun:1.1.45-alpine
|
||||||
|
|
||||||
|
WORKDIR /site
|
||||||
|
|
||||||
|
COPY ./site/package.json ./
|
||||||
|
COPY ./site/bun.lockb ./
|
||||||
|
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
COPY ./site/public ./public
|
||||||
|
COPY ./site/src ./src
|
||||||
|
|
||||||
|
COPY ./site/eslint.config.js ./
|
||||||
|
COPY ./site/index.html ./
|
||||||
|
COPY ./site/tsconfig.json ./
|
||||||
|
COPY ./site/tsconfig.app.json ./
|
||||||
|
COPY ./site/tsconfig.node.json ./
|
||||||
|
COPY ./site/vite.config.ts ./
|
||||||
|
COPY ./site/postcss.config.cjs ./
|
||||||
|
|
||||||
|
EXPOSE 5173
|
||||||
|
|
||||||
|
ENTRYPOINT ["bun", "run", "dev"]
|
||||||
BIN
site/bun.lockb
Executable file
@@ -6,7 +6,7 @@
|
|||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
<link rel="manifest" href="/frontend.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<title>Tinyauth</title>
|
<title>Tinyauth</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "site",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "site",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^7.16.0",
|
"@mantine/core": "^7.16.0",
|
||||||
@@ -2246,4 +2246,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "site",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -16,14 +16,8 @@
|
|||||||
"@mantine/notifications": "^7.16.0",
|
"@mantine/notifications": "^7.16.0",
|
||||||
"@tanstack/react-query": "4",
|
"@tanstack/react-query": "4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"i18next": "^24.2.3",
|
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
|
||||||
"i18next-chained-backend": "^4.6.2",
|
|
||||||
"i18next-http-backend": "^3.0.2",
|
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^15.4.1",
|
|
||||||
"react-router": "^7.1.3",
|
"react-router": "^7.1.3",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
@@ -44,4 +38,4 @@
|
|||||||
"typescript-eslint": "^8.18.2",
|
"typescript-eslint": "^8.18.2",
|
||||||
"vite": "^6.0.5"
|
"vite": "^6.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 602 B After Width: | Height: | Size: 602 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |