mirror of https://github.com/wg-easy/wg-easy
committed by
GitHub
203 changed files with 9372 additions and 4201 deletions
@ -1,3 +1,6 @@ |
|||
.DS_Store |
|||
*.swp |
|||
node_modules |
|||
node_modules |
|||
|
|||
docs/site |
|||
docs/.cache |
|||
@ -0,0 +1,182 @@ |
|||
--- |
|||
title: External Authentication |
|||
--- |
|||
|
|||
## OAuth |
|||
|
|||
### Setup |
|||
|
|||
To enable OAuth set the env var `OAUTH_PROVIDERS` to any of the following providers: |
|||
|
|||
| Provider | Value | |
|||
| ----------------------------- | -------- | |
|||
| [Google](#google) | `google` | |
|||
| [GitHub](#github) | `github` | |
|||
| [Generic OIDC](#generic-oidc) | `oidc` | |
|||
|
|||
You can enable multiple providers by separating them with a comma: |
|||
|
|||
e.g. `google,github` |
|||
|
|||
### Auto Register |
|||
|
|||
To automatically register users that log in with an OAuth provider, set the following environment variable to `true`: |
|||
|
|||
| Env | Required | Default | Description | |
|||
| --------------------- | -------- | ------- | ------------------------ | |
|||
| `OAUTH_AUTO_REGISTER` | :x: | `false` | Enable auto-registration | |
|||
|
|||
When enabled: |
|||
|
|||
- If a user logs in with an email address that is not yet registered, a new account will be created for them. |
|||
|
|||
- If a user logs in with an email address that is already registered, their account will be linked to the OAuth provider (if not already linked), regardless of the value of `OAUTH_AUTO_REGISTER`. |
|||
|
|||
/// warning | Security |
|||
|
|||
Users will be created with Admin Permissions, as the permissions system is not yet implemented. Only enable this if you trust all users that can log in with the OAuth provider. |
|||
|
|||
Use [Allowed Domains](#allowed-domains) to restrict which users can log in. |
|||
|
|||
/// |
|||
|
|||
### Allowed Domains |
|||
|
|||
To only allow users with an email address from a specific domain to log in, set the following environment variable to the allowed domain. |
|||
|
|||
| Env | Required | Default | Description | |
|||
| ----------------------- | -------- | ------- | --------------------- | |
|||
| `OAUTH_ALLOWED_DOMAINS` | :x: | - | Allowed email domains | |
|||
|
|||
You can allow multiple domains by separating them with a comma: |
|||
|
|||
e.g. `example.com,example.org` |
|||
|
|||
### Auto Launch |
|||
|
|||
To automatically launch the OAuth login flow when visiting the login page, set the following environment variable to the provider you want to launch: |
|||
|
|||
| Env | Required | Default | Description | |
|||
| ------------------- | -------- | ------- | ----------------------------- | |
|||
| `OAUTH_AUTO_LAUNCH` | :x: | - | Auto launch an OAuth provider | |
|||
|
|||
When enabled: |
|||
|
|||
- Visiting the login page will automatically redirect to the selected provider's login page |
|||
- The user can still access the normal login page by visiting `/login?auto_launch=false` |
|||
- You can auto launch any provider by visiting `/login?auto_launch=<provider>` |
|||
|
|||
### Redirect URIs |
|||
|
|||
You have to configure the following redirect URIs in your OAuth provider: |
|||
|
|||
- `https://<your-domain>/api/auth/<provider>/callback` |
|||
Used to log in to with the provider |
|||
- `https://<your-domain>/api/auth/<provider>/link` |
|||
Used to link an existing account to the provider |
|||
|
|||
If your provider does not support multiple redirect URIs (e.g. GitHub) but allows multiple URIs under the same base, then configure: |
|||
|
|||
- `https://<your-domain>/api/auth/<provider>/` |
|||
|
|||
### Provider Configuration |
|||
|
|||
#### Google |
|||
|
|||
| Env | Required | Description | |
|||
| ---------------------------- | ------------------ | -------------------- | |
|||
| `OAUTH_GOOGLE_CLIENT_ID` | :white_check_mark: | Google Client ID | |
|||
| `OAUTH_GOOGLE_CLIENT_SECRET` | :white_check_mark: | Google Client Secret | |
|||
|
|||
<h5>Setup</h5> |
|||
|
|||
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials) |
|||
2. Create an OAuth 2.0 Client ID (Web application) |
|||
3. Add Authorized redirect URI: See [Redirect URIs](#redirect-uris) |
|||
4. Copy the Client ID and Client Secret to the environment variables |
|||
|
|||
#### GitHub |
|||
|
|||
| Env | Required | Description | |
|||
| ---------------------------- | ------------------ | -------------------- | |
|||
| `OAUTH_GITHUB_CLIENT_ID` | :white_check_mark: | GitHub Client ID | |
|||
| `OAUTH_GITHUB_CLIENT_SECRET` | :white_check_mark: | GitHub Client Secret | |
|||
|
|||
<h5>Setup</h5> |
|||
|
|||
1. Go to [GitHub Developer Settings](https://github.com/settings/developers) |
|||
2. Create a new OAuth App |
|||
3. Add Authorization callback URL: See [Redirect URIs](#redirect-uris) |
|||
4. Create a new client secret |
|||
5. Copy the Client ID and Client Secret to the environment variables |
|||
|
|||
#### Generic OIDC |
|||
|
|||
This supports generic OIDC providers like Authelia, Authentik, etc. |
|||
|
|||
The provider needs to support: |
|||
|
|||
- PKCE |
|||
- default scopes: `openid email profile` |
|||
- Client Secret Authentication `client_secret_post` |
|||
|
|||
The provider needs to be available with HTTPS and have a valid certificate. |
|||
|
|||
| Env | Required | Default | Example | Description | |
|||
| -------------------------- | ------------------ | ------- | -------------------------- | ------------------ | |
|||
| `OAUTH_OIDC_SERVER` | :white_check_mark: | - | `https://auth.example.com` | OIDC Server | |
|||
| `OAUTH_OIDC_CLIENT_ID` | :white_check_mark: | - | - | OIDC Client ID | |
|||
| `OAUTH_OIDC_CLIENT_SECRET` | :white_check_mark: | - | - | OIDC Client Secret | |
|||
| `OAUTH_OIDC_NAME` | :x: | OIDC | `Authelia` | Provider Name | |
|||
|
|||
##### Authelia Setup |
|||
|
|||
Generate Client ID and Secret: |
|||
|
|||
```shell |
|||
# Client ID |
|||
docker run --rm authelia/authelia:latest authelia crypto rand --length 72 --charset rfc3986 |
|||
# Client Secret |
|||
docker run --rm authelia/authelia:latest authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986 |
|||
``` |
|||
|
|||
```yaml |
|||
- client_id: '...' |
|||
client_name: wg-easy |
|||
client_secret: '$pbkdf2-...' |
|||
redirect_uris: |
|||
- https://<your-domain>/api/auth/oidc/callback |
|||
- https://<your-domain>/api/auth/oidc/link |
|||
scopes: |
|||
- openid |
|||
- profile |
|||
- email |
|||
authorization_policy: one_factor |
|||
pre_configured_consent_duration: 1 week |
|||
require_pkce: true |
|||
token_endpoint_auth_method: client_secret_post |
|||
``` |
|||
|
|||
#### Generic OAuth |
|||
|
|||
Not currently supported |
|||
|
|||
### Disable Password Authentication |
|||
|
|||
To disable password-based authentication and only allow login via OAuth providers, set the following environment variable to `true`: |
|||
|
|||
| Env | Required | Default | Description | |
|||
| ----------------------- | -------- | ------- | ------------------------------- | |
|||
| `DISABLE_PASSWORD_AUTH` | :x: | `false` | Disable password authentication | |
|||
|
|||
When enabled: |
|||
|
|||
- Users will not be able to log in with a password |
|||
|
|||
/// warning | Access Recovery |
|||
|
|||
Before disabling password authentication, ensure that at least one OAuth provider is configured and that you have successfully linked an administrator account. |
|||
|
|||
If no login method is available, you will not be able to log in to the application and will need to reset the configuration to regain access. |
|||
|
|||
/// |
|||
@ -1,87 +0,0 @@ |
|||
site_name: 'wg-easy' |
|||
site_description: 'The easiest way to run WireGuard VPN + Web-based Admin UI.' |
|||
site_author: 'WireGuard Easy' |
|||
copyright: > |
|||
<p> |
|||
© <a href="https://github.com/wg-easy"><em>Wireguard Easy</em></a><br/> |
|||
<span>This project is licensed under AGPL-3.0-only.</span><br/> |
|||
<span>This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Jason A. Donenfeld, ZX2C4 or Edge Security</span><br/> |
|||
<span>"WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld</span> |
|||
</p> |
|||
|
|||
repo_url: https://github.com/wg-easy/wg-easy |
|||
repo_name: wg-easy |
|||
|
|||
edit_uri: 'edit/master/docs/content' |
|||
|
|||
docs_dir: 'content/' |
|||
|
|||
site_url: https://wg-easy.github.io/wg-easy |
|||
|
|||
theme: |
|||
name: material |
|||
favicon: assets/logo/favicon.png |
|||
logo: assets/logo/logo.png |
|||
icon: |
|||
repo: fontawesome/brands/github |
|||
features: |
|||
- navigation.tabs |
|||
- navigation.top |
|||
- navigation.expand |
|||
- navigation.instant |
|||
- content.action.edit |
|||
- content.action.view |
|||
- content.code.annotate |
|||
palette: |
|||
# Light mode |
|||
- media: '(prefers-color-scheme: light)' |
|||
scheme: default |
|||
primary: grey |
|||
accent: red |
|||
toggle: |
|||
icon: material/weather-night |
|||
name: Switch to dark mode |
|||
# Dark mode |
|||
- media: '(prefers-color-scheme: dark)' |
|||
scheme: slate |
|||
primary: grey |
|||
accent: red |
|||
toggle: |
|||
icon: material/weather-sunny |
|||
name: Switch to light mode |
|||
|
|||
extra: |
|||
version: |
|||
provider: mike |
|||
|
|||
markdown_extensions: |
|||
- toc: |
|||
anchorlink: true |
|||
- abbr |
|||
- attr_list |
|||
- pymdownx.blocks.admonition: |
|||
types: |
|||
- danger |
|||
- note |
|||
- info |
|||
- question |
|||
- warning |
|||
- pymdownx.details |
|||
- pymdownx.superfences: |
|||
custom_fences: |
|||
- name: mermaid |
|||
class: mermaid |
|||
format: !!python/name:pymdownx.superfences.fence_code_format |
|||
- pymdownx.tabbed: |
|||
alternate_style: true |
|||
slugify: !!python/object/apply:pymdownx.slugs.slugify |
|||
kwds: |
|||
case: lower |
|||
- pymdownx.tasklist: |
|||
custom_checkbox: true |
|||
- pymdownx.magiclink |
|||
- pymdownx.inlinehilite |
|||
- pymdownx.tilde |
|||
- pymdownx.emoji: |
|||
emoji_index: !!python/name:material.extensions.emoji.twemoji |
|||
emoji_generator: !!python/name:material.extensions.emoji.to_svg |
|||
@ -1,4 +1,2 @@ |
|||
mkdocs-material |
|||
pillow |
|||
cairosvg |
|||
mike |
|||
zensical==0.0.45 |
|||
git+https://github.com/squidfunk/mike.git |
|||
|
|||
@ -0,0 +1,97 @@ |
|||
[project] |
|||
site_name = "wg-easy" |
|||
site_description = "The easiest way to run WireGuard VPN + Web-based Admin UI." |
|||
site_author = "WireGuard Easy" |
|||
copyright = """ |
|||
<p> |
|||
© <a href="https://github.com/wg-easy"><em>Wireguard Easy</em></a><br/> |
|||
<span>This project is licensed under AGPL-3.0-only.</span><br/> |
|||
<span>This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Jason A. Donenfeld, ZX2C4 or Edge Security</span><br/> |
|||
<span>"WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld</span> |
|||
</p> |
|||
""" |
|||
|
|||
repo_url = "https://github.com/wg-easy/wg-easy" |
|||
repo_name = "wg-easy" |
|||
|
|||
edit_uri = "edit/master/docs/content" |
|||
|
|||
docs_dir = "content/" |
|||
|
|||
site_url = "https://wg-easy.github.io/wg-easy" |
|||
|
|||
[project.theme] |
|||
variant = "modern" |
|||
favicon = "assets/logo/favicon.png" |
|||
logo = "assets/logo/logo.png" |
|||
features = [ |
|||
"navigation.instant", |
|||
"navigation.tabs", |
|||
"navigation.expand", |
|||
"navigation.path", |
|||
"navigation.top", |
|||
"content.action.edit", |
|||
"content.action.view", |
|||
"content.code.copy", |
|||
"content.code.annotate", |
|||
] |
|||
|
|||
[project.theme.icon] |
|||
repo = "fontawesome/brands/github" |
|||
|
|||
# Palette toggle for automatic mode |
|||
[[project.theme.palette]] |
|||
media = "(prefers-color-scheme)" |
|||
toggle.icon = "lucide/sun-moon" |
|||
toggle.name = "Switch to light mode" |
|||
|
|||
# Palette toggle for light mode |
|||
[[project.theme.palette]] |
|||
media = "(prefers-color-scheme: light)" |
|||
scheme = "default" |
|||
primary = "grey" |
|||
accent = "red" |
|||
toggle.icon = "lucide/sun" |
|||
toggle.name = "Switch to dark mode" |
|||
|
|||
# Palette toggle for dark mode |
|||
[[project.theme.palette]] |
|||
media = "(prefers-color-scheme: dark)" |
|||
scheme = "slate" |
|||
primary = "grey" |
|||
accent = "red" |
|||
toggle.icon = "lucide/moon" |
|||
toggle.name = "Switch to system preference" |
|||
|
|||
# [project.plugins.privacy] |
|||
|
|||
[project.extra.version] |
|||
provider = "mike" |
|||
|
|||
[project.markdown_extensions.pymdownx.blocks.admonition] |
|||
types = ["danger", "note", "info", "question", "warning"] |
|||
|
|||
[project.markdown_extensions.md_in_html] |
|||
|
|||
[project.markdown_extensions.toc] |
|||
permalink = true |
|||
|
|||
[project.markdown_extensions.pymdownx.details] |
|||
|
|||
[project.markdown_extensions.pymdownx.emoji] |
|||
emoji_generator = "zensical.extensions.emoji.to_svg" |
|||
emoji_index = "zensical.extensions.emoji.twemoji" |
|||
|
|||
[project.markdown_extensions.pymdownx.highlight] |
|||
anchor_linenums = true |
|||
line_spans = "__span" |
|||
pygments_lang_class = true |
|||
|
|||
[project.markdown_extensions.pymdownx.inlinehilite] |
|||
|
|||
[project.markdown_extensions.pymdownx.smartsymbols] |
|||
|
|||
[project.markdown_extensions.pymdownx.superfences] |
|||
custom_fences = [ |
|||
{ name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" }, |
|||
] |
|||
@ -9,16 +9,16 @@ importers: |
|||
.: |
|||
devDependencies: |
|||
prettier: |
|||
specifier: ^3.8.1 |
|||
version: 3.8.1 |
|||
specifier: ^3.8.4 |
|||
version: 3.8.4 |
|||
|
|||
packages: |
|||
|
|||
[email protected].1: |
|||
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} |
|||
[email protected].4: |
|||
resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} |
|||
engines: {node: '>=14'} |
|||
hasBin: true |
|||
|
|||
snapshots: |
|||
|
|||
[email protected].1: {} |
|||
[email protected].4: {} |
|||
|
|||
@ -1 +1 @@ |
|||
setups.@nuxt/test-utils="4.0.0" |
|||
setups.@nuxt/test-utils="4.0.3" |
|||
@ -0,0 +1,26 @@ |
|||
<template> |
|||
<component |
|||
:is="elementType" |
|||
role="button" |
|||
class="rounded-lg border-2 border-gray-100 text-gray-500 hover:border-red-800 hover:bg-red-800 hover:text-white focus:border-red-800 focus:outline-0 focus:ring-0 disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-100 disabled:text-gray-400 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400 dark:disabled:border-neutral-800 dark:disabled:bg-neutral-700 dark:disabled:text-neutral-400" |
|||
v-bind="attrs" |
|||
> |
|||
<slot /> |
|||
</component> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const props = defineProps({ |
|||
as: { |
|||
type: String, |
|||
default: 'button', |
|||
}, |
|||
}); |
|||
|
|||
const elementType = computed(() => props.as); |
|||
|
|||
const attrs = computed(() => { |
|||
const { as, ...rest } = props; |
|||
return rest; |
|||
}); |
|||
</script> |
|||
@ -1,8 +1,8 @@ |
|||
<template> |
|||
<ClientsCreateDialog> |
|||
<BaseSecondaryButton as="span"> |
|||
<IconsPlus class="w-4 md:mr-2" /> |
|||
<span class="text-sm max-md:hidden">{{ $t('client.newShort') }}</span> |
|||
<IconsPlus class="mr-2 w-4" /> |
|||
<span class="text-sm">{{ $t('client.newShort') }}</span> |
|||
</BaseSecondaryButton> |
|||
</ClientsCreateDialog> |
|||
</template> |
|||
|
|||
@ -1,16 +1,32 @@ |
|||
<template> |
|||
<input |
|||
:value="label" |
|||
:type="type ?? 'button'" |
|||
class="col-span-2 rounded-lg border-2 border-gray-100 py-2 text-gray-500 hover:border-red-800 hover:bg-red-800 hover:text-white focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
|||
/> |
|||
<BaseFormSecondaryButton |
|||
:as="as" |
|||
:value="isInput ? label : undefined" |
|||
:type="isInput ? type : undefined" |
|||
class="col-span-2 px-4 py-2" |
|||
> |
|||
<template v-if="!isInput"> |
|||
{{ label }} |
|||
</template> |
|||
</BaseFormSecondaryButton> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import type { InputTypeHTMLAttribute } from 'vue'; |
|||
|
|||
defineProps<{ |
|||
label: string; |
|||
type?: InputTypeHTMLAttribute; |
|||
}>(); |
|||
const props = withDefaults( |
|||
defineProps<{ |
|||
label: string; |
|||
type?: InputTypeHTMLAttribute; |
|||
as?: string; |
|||
}>(), |
|||
{ |
|||
type: 'button', |
|||
as: 'input', |
|||
} |
|||
); |
|||
|
|||
const isInput = computed(() => { |
|||
return props.as === 'input'; |
|||
}); |
|||
</script> |
|||
|
|||
@ -0,0 +1,23 @@ |
|||
<template> |
|||
<!-- https://brand.github.com/foundations/logo --> |
|||
<svg |
|||
width="98" |
|||
height="96" |
|||
viewBox="0 0 98 96" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
> |
|||
<g clip-path="url(#clip0_730_27136)"> |
|||
<path |
|||
d="M41.4395 69.3848C28.8066 67.8535 19.9062 58.7617 19.9062 46.9902C19.9062 42.2051 21.6289 37.0371 24.5 33.5918C23.2559 30.4336 23.4473 23.7344 24.8828 20.959C28.7109 20.4805 33.8789 22.4902 36.9414 25.2656C40.5781 24.1172 44.4062 23.543 49.0957 23.543C53.7852 23.543 57.6133 24.1172 61.0586 25.1699C64.0254 22.4902 69.2891 20.4805 73.1172 20.959C74.457 23.543 74.6484 30.2422 73.4043 33.4961C76.4668 37.1328 78.0937 42.0137 78.0937 46.9902C78.0937 58.7617 69.1934 67.6621 56.3691 69.2891C59.623 71.3945 61.8242 75.9883 61.8242 81.252L61.8242 91.2051C61.8242 94.0762 64.2168 95.7031 67.0879 94.5547C84.4102 87.9512 98 70.6289 98 49.1914C98 22.1074 75.9883 6.69539e-07 48.9043 4.309e-07C21.8203 1.92261e-07 -1.9479e-07 22.1074 -4.3343e-07 49.1914C-6.20631e-07 70.4375 13.4941 88.0469 31.6777 94.6504C34.2617 95.6074 36.75 93.8848 36.75 91.3008L36.75 83.6445C35.4102 84.2188 33.6875 84.6016 32.1562 84.6016C25.8398 84.6016 22.1074 81.1563 19.4277 74.7441C18.375 72.1602 17.2266 70.6289 15.0254 70.3418C13.877 70.2461 13.4941 69.7676 13.4941 69.1934C13.4941 68.0449 15.4082 67.1836 17.3223 67.1836C20.0977 67.1836 22.4902 68.9063 24.9785 72.4473C26.8926 75.2227 28.9023 76.4668 31.2949 76.4668C33.6875 76.4668 35.2187 75.6055 37.4199 73.4043C39.0469 71.7773 40.291 70.3418 41.4395 69.3848Z" |
|||
fill="white" |
|||
class="fill-black dark:fill-white" |
|||
/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id="clip0_730_27136"> |
|||
<rect width="98" height="96" fill="white" /> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
|||
</template> |
|||
@ -0,0 +1,247 @@ |
|||
<template> |
|||
<!-- By Original: Google Vector: Designism, Liquinoid, and YeBoy371 - Own work based on: Google updating its ‘G’ icon for the first time in 10 years, Public Domain, https://commons.wikimedia.org/w/index.php?curid=165142698 --> |
|||
<svg |
|||
version="1.1" |
|||
viewBox="0 0 268.1522 273.8827" |
|||
overflow="hidden" |
|||
xml:space="preserve" |
|||
xmlns:xlink="http://www.w3.org/1999/xlink" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
xmlns:svg="http://www.w3.org/2000/svg" |
|||
> |
|||
<defs> |
|||
<linearGradient id="a"> |
|||
<stop offset="0" stop-color="#0fbc5c" /> |
|||
<stop offset="1" stop-color="#0cba65" /> |
|||
</linearGradient> |
|||
<linearGradient id="g"> |
|||
<stop offset=".2312727" stop-color="#0fbc5f" /> |
|||
<stop offset=".3115468" stop-color="#0fbc5f" /> |
|||
<stop offset=".3660131" stop-color="#0fbc5e" /> |
|||
<stop offset=".4575163" stop-color="#0fbc5d" /> |
|||
<stop offset=".540305" stop-color="#12bc58" /> |
|||
<stop offset=".6993464" stop-color="#28bf3c" /> |
|||
<stop offset=".7712418" stop-color="#38c02b" /> |
|||
<stop offset=".8605665" stop-color="#52c218" /> |
|||
<stop offset=".9150327" stop-color="#67c30f" /> |
|||
<stop offset="1" stop-color="#86c504" /> |
|||
</linearGradient> |
|||
<linearGradient id="h"> |
|||
<stop offset=".1416122" stop-color="#1abd4d" /> |
|||
<stop offset=".2475151" stop-color="#6ec30d" /> |
|||
<stop offset=".3115468" stop-color="#8ac502" /> |
|||
<stop offset=".3660131" stop-color="#a2c600" /> |
|||
<stop offset=".4456735" stop-color="#c8c903" /> |
|||
<stop offset=".540305" stop-color="#ebcb03" /> |
|||
<stop offset=".6156363" stop-color="#f7cd07" /> |
|||
<stop offset=".6993454" stop-color="#fdcd04" /> |
|||
<stop offset=".7712418" stop-color="#fdce05" /> |
|||
<stop offset=".8605661" stop-color="#ffce0a" /> |
|||
</linearGradient> |
|||
<linearGradient id="f"> |
|||
<stop offset=".3159041" stop-color="#ff4c3c" /> |
|||
<stop offset=".6038179" stop-color="#ff692c" /> |
|||
<stop offset=".7268366" stop-color="#ff7825" /> |
|||
<stop offset=".884534" stop-color="#ff8d1b" /> |
|||
<stop offset="1" stop-color="#ff9f13" /> |
|||
</linearGradient> |
|||
<linearGradient id="b"> |
|||
<stop offset=".2312727" stop-color="#ff4541" /> |
|||
<stop offset=".3115468" stop-color="#ff4540" /> |
|||
<stop offset=".4575163" stop-color="#ff4640" /> |
|||
<stop offset=".540305" stop-color="#ff473f" /> |
|||
<stop offset=".6993464" stop-color="#ff5138" /> |
|||
<stop offset=".7712418" stop-color="#ff5b33" /> |
|||
<stop offset=".8605665" stop-color="#ff6c29" /> |
|||
<stop offset="1" stop-color="#ff8c18" /> |
|||
</linearGradient> |
|||
<linearGradient id="d"> |
|||
<stop offset=".4084578" stop-color="#fb4e5a" /> |
|||
<stop offset="1" stop-color="#ff4540" /> |
|||
</linearGradient> |
|||
<linearGradient id="c"> |
|||
<stop offset=".1315461" stop-color="#0cba65" /> |
|||
<stop offset=".2097843" stop-color="#0bb86d" /> |
|||
<stop offset=".2972969" stop-color="#09b479" /> |
|||
<stop offset=".3962575" stop-color="#08ad93" /> |
|||
<stop offset=".4771242" stop-color="#0aa6a9" /> |
|||
<stop offset=".5684245" stop-color="#0d9cc6" /> |
|||
<stop offset=".667385" stop-color="#1893dd" /> |
|||
<stop offset=".7687273" stop-color="#258bf1" /> |
|||
<stop offset=".8585063" stop-color="#3086ff" /> |
|||
</linearGradient> |
|||
<linearGradient id="e"> |
|||
<stop offset=".3660131" stop-color="#ff4e3a" /> |
|||
<stop offset=".4575163" stop-color="#ff8a1b" /> |
|||
<stop offset=".540305" stop-color="#ffa312" /> |
|||
<stop offset=".6156363" stop-color="#ffb60c" /> |
|||
<stop offset=".7712418" stop-color="#ffcd0a" /> |
|||
<stop offset=".8605665" stop-color="#fecf0a" /> |
|||
<stop offset=".9150327" stop-color="#fecf08" /> |
|||
<stop offset="1" stop-color="#fdcd01" /> |
|||
</linearGradient> |
|||
<linearGradient |
|||
id="s" |
|||
xlink:href="#a" |
|||
x1="219.6997" |
|||
y1="329.5351" |
|||
x2="254.4673" |
|||
y2="329.5351" |
|||
gradientUnits="userSpaceOnUse" |
|||
/> |
|||
<radialGradient |
|||
id="m" |
|||
xlink:href="#b" |
|||
gradientUnits="userSpaceOnUse" |
|||
gradientTransform="matrix(-1.936885,1.043001,1.455731,2.555422,290.5254,-400.6338)" |
|||
cx="109.6267" |
|||
cy="135.8619" |
|||
fx="109.6267" |
|||
fy="135.8619" |
|||
r="71.46001" |
|||
/> |
|||
<radialGradient |
|||
id="n" |
|||
xlink:href="#c" |
|||
gradientUnits="userSpaceOnUse" |
|||
gradientTransform="matrix(-3.512595,-4.45809,-1.692547,1.260616,870.8006,191.554)" |
|||
cx="45.25866" |
|||
cy="279.2738" |
|||
fx="45.25866" |
|||
fy="279.2738" |
|||
r="71.46001" |
|||
/> |
|||
<radialGradient |
|||
id="l" |
|||
xlink:href="#d" |
|||
cx="304.0166" |
|||
cy="118.0089" |
|||
fx="304.0166" |
|||
fy="118.0089" |
|||
r="47.85445" |
|||
gradientTransform="matrix(2.064353,-4.926832e-6,-2.901531e-6,2.592041,-297.6788,-151.7469)" |
|||
gradientUnits="userSpaceOnUse" |
|||
/> |
|||
<radialGradient |
|||
id="o" |
|||
xlink:href="#e" |
|||
gradientUnits="userSpaceOnUse" |
|||
gradientTransform="matrix(-0.2485783,2.083138,2.962486,0.3341668,-255.1463,-331.1636)" |
|||
cx="181.001" |
|||
cy="177.2013" |
|||
fx="181.001" |
|||
fy="177.2013" |
|||
r="71.46001" |
|||
/> |
|||
<radialGradient |
|||
id="p" |
|||
xlink:href="#f" |
|||
cx="207.6733" |
|||
cy="108.0972" |
|||
fx="207.6733" |
|||
fy="108.0972" |
|||
r="41.1025" |
|||
gradientTransform="matrix(-1.249206,1.343263,-3.896837,-3.425693,880.5011,194.9051)" |
|||
gradientUnits="userSpaceOnUse" |
|||
/> |
|||
<radialGradient |
|||
id="r" |
|||
xlink:href="#g" |
|||
gradientUnits="userSpaceOnUse" |
|||
gradientTransform="matrix(-1.936885,-1.043001,1.455731,-2.555422,290.5254,838.6834)" |
|||
cx="109.6267" |
|||
cy="135.8619" |
|||
fx="109.6267" |
|||
fy="135.8619" |
|||
r="71.46001" |
|||
/> |
|||
<radialGradient |
|||
id="j" |
|||
xlink:href="#h" |
|||
gradientUnits="userSpaceOnUse" |
|||
gradientTransform="matrix(-0.081402,-1.93722,2.926737,-0.1162508,-215.1345,632.8606)" |
|||
cx="154.8697" |
|||
cy="145.9691" |
|||
fx="154.8697" |
|||
fy="145.9691" |
|||
r="71.46001" |
|||
/> |
|||
<filter |
|||
id="q" |
|||
x="-.04842873" |
|||
y="-.0582241" |
|||
width="1.096857" |
|||
height="1.116448" |
|||
color-interpolation-filters="sRGB" |
|||
> |
|||
<feGaussianBlur stdDeviation="1.700914" /> |
|||
</filter> |
|||
<filter |
|||
id="k" |
|||
x="-.01670084" |
|||
y="-.01009856" |
|||
width="1.033402" |
|||
height="1.020197" |
|||
color-interpolation-filters="sRGB" |
|||
> |
|||
<feGaussianBlur stdDeviation=".2419367" /> |
|||
</filter> |
|||
<clipPath id="i" clipPathUnits="userSpaceOnUse"> |
|||
<path |
|||
d="M371.3784 193.2406H237.0825v53.4375h77.167c-1.2405 7.5627-4.0259 15.0024-8.1049 21.7862-4.6734 7.7723-10.4511 13.6895-16.373 18.1957-17.7389 13.4983-38.42 16.2584-52.7828 16.2584-36.2824 0-67.2833-23.2865-79.2844-54.9287-.4843-1.1482-.8059-2.3344-1.1975-3.5068-2.652-8.0533-4.101-16.5825-4.101-25.4474 0-9.226 1.5691-18.0575 4.4301-26.3985 11.2851-32.8967 42.9849-57.4674 80.1789-57.4674 7.4811 0 14.6854.8843 21.5173 2.6481 15.6135 4.0309 26.6578 11.9698 33.4252 18.2494l40.834-39.7111c-24.839-22.616-57.2194-36.3201-95.8444-36.3201-30.8782-.00066-59.3863 9.55308-82.7477 25.6992-18.9454 13.0941-34.4833 30.6254-44.9695 50.9861-9.75366 18.8785-15.09441 39.7994-15.09441 62.2934 0 22.495 5.34891 43.6334 15.10261 62.3374v.126c10.3023 19.8567 25.3678 36.9537 43.6783 49.9878 15.9962 11.3866 44.6789 26.5516 84.0307 26.5516 22.6301 0 42.6867-4.0517 60.3748-11.6447 12.76-5.4775 24.0655-12.6217 34.3012-21.8036 13.5247-12.1323 24.1168-27.1388 31.3465-44.4041 7.2297-17.2654 11.097-36.7895 11.097-57.957 0-9.858-.9971-19.8694-2.6881-28.9684Z" |
|||
fill="#000" |
|||
/> |
|||
</clipPath> |
|||
</defs> |
|||
<g transform="matrix(0.957922,0,0,0.985255,-90.17436,-78.85577)"> |
|||
<g clip-path="url(#i)"> |
|||
<path |
|||
d="M92.07563 219.9585c.14844 22.14 6.5014 44.983 16.11767 63.4234v.1269c6.9482 13.3919 16.4444 23.9704 27.2604 34.4518l65.326-23.67c-12.3593-6.2344-14.2452-10.0546-23.1048-17.0253-9.0537-9.0658-15.8015-19.4735-20.0038-31.677h-.1693l.1693-.1269c-2.7646-8.0587-3.0373-16.6129-3.1393-25.5029Z" |
|||
fill="url(#j)" |
|||
filter="url(#k)" |
|||
/> |
|||
<path |
|||
d="M237.0835 79.02491c-6.4568 22.52569-3.988 44.42139 0 57.16129 7.4561.0055 14.6388.8881 21.4494 2.6464 15.6135 4.0309 26.6566 11.97 33.424 18.2496l41.8794-40.7256c-24.8094-22.58904-54.6663-37.2961-96.7528-37.33169Z" |
|||
fill="url(#l)" |
|||
filter="url(#k)" |
|||
/> |
|||
<path |
|||
d="M236.9434 78.84678c-31.6709-.00068-60.9107 9.79833-84.8718 26.35902-8.8968 6.149-17.0612 13.2521-24.3311 21.1509-1.9045 17.7429 14.2569 39.5507 46.2615 39.3702 15.5284-17.9373 38.4946-29.5427 64.0561-29.5427.0233 0 .046.0019.0693.002l-1.0439-57.33536c-.0472-.00003-.0929-.00406-.1401-.00406Z" |
|||
fill="url(#m)" |
|||
filter="url(#k)" |
|||
/> |
|||
<path |
|||
d="m341.4751 226.3788-28.2685 19.2848c-1.2405 7.5627-4.0278 15.0023-8.1068 21.7861-4.6734 7.7723-10.4506 13.6898-16.3725 18.196-17.7022 13.4704-38.3286 16.2439-52.6877 16.2553-14.8415 25.1018-17.4435 37.6749 1.0439 57.9342 22.8762-.0167 43.157-4.1174 61.0458-11.7965 12.9312-5.551 24.3879-12.7913 34.7609-22.0964 13.7061-12.295 24.4421-27.5034 31.7688-45.0003 7.3267-17.497 11.2446-37.2822 11.2446-58.7336Z" |
|||
fill="url(#n)" |
|||
filter="url(#k)" |
|||
/> |
|||
<path |
|||
d="M234.9956 191.2104v57.4981h136.0062c1.1962-7.8745 5.1523-18.0644 5.1523-26.5001 0-9.858-.9963-21.899-2.6873-30.998Z" |
|||
fill="#3086ff" |
|||
filter="url(#k)" |
|||
/> |
|||
<path |
|||
d="M128.3894 124.3268c-8.393 9.1191-15.5632 19.326-21.2483 30.3646-9.75351 18.8785-15.09402 41.8295-15.09402 64.3235 0 .317.02642.6271.02855.9436 4.31953 8.2244 59.66647 6.6495 62.45617 0-.0035-.3103-.0387-.6128-.0387-.9238 0-9.226 1.5696-16.0262 4.4306-24.3672 3.5294-10.2885 9.0557-19.7628 16.1223-27.9257 1.6019-2.0309 5.8748-6.3969 7.1214-9.0157.4749-.9975-.8621-1.5574-.9369-1.9085-.0836-.3927-1.8762-.0769-2.2778-.3694-1.2751-.9288-3.8001-1.4138-5.3334-1.8449-3.2772-.9215-8.7085-2.9536-11.7252-5.0601-9.5357-6.6586-24.417-14.6122-33.5047-24.2164Z" |
|||
fill="url(#o)" |
|||
filter="url(#k)" |
|||
/> |
|||
<path |
|||
d="M162.0989 155.8569c22.1123 13.3013 28.4714-6.7139 43.173-12.9771L179.698 90.21568c-9.4075 3.92642-18.2957 8.80465-26.5426 14.50442-12.316 8.5122-23.192 18.8995-32.1763 30.7204Z" |
|||
fill="url(#p)" |
|||
filter="url(#q)" |
|||
/> |
|||
<path |
|||
d="M171.0987 290.222c-29.6829 10.6413-34.3299 11.023-37.0622 29.2903 5.2213 5.0597 10.8312 9.74 16.7926 13.9835 15.9962 11.3867 46.766 26.5517 86.1178 26.5517.0462 0 .0904-.004.1366-.004v-59.1574c-.0298.0001-.064.002-.0938.002-14.7359 0-26.5113-3.8435-38.5848-10.5273-2.9768-1.6479-8.3775 2.7772-11.1229.799-3.7865-2.7284-12.8991 2.3508-16.1833-.9378Z" |
|||
fill="url(#r)" |
|||
filter="url(#k)" |
|||
/> |
|||
<path |
|||
d="M219.6997 299.0227v59.9959c5.506.6402 11.2361 1.0289 17.2472 1.0289 6.0259 0 11.8556-.3073 17.5204-.8723v-59.7481c-6.3482 1.0777-12.3272 1.461-17.4776 1.461-5.9318 0-11.7005-.6858-17.29-1.8654Z" |
|||
opacity=".5" |
|||
fill="url(#s)" |
|||
filter="url(#k)" |
|||
/> |
|||
</g> |
|||
</g> |
|||
</svg> |
|||
</template> |
|||
@ -0,0 +1,10 @@ |
|||
<template> |
|||
<IconsBrandsGitHub v-if="provider === 'github'" /> |
|||
<IconsBrandsGoogle v-else-if="provider === 'google'" /> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
provider: OAUTH_PROVIDER; |
|||
}>(); |
|||
</script> |
|||
@ -1,7 +1,9 @@ |
|||
<template> |
|||
<div |
|||
class="container mx-auto max-w-3xl overflow-hidden rounded-lg bg-white px-3 text-gray-700 shadow-md md:px-0 dark:bg-neutral-700 dark:text-neutral-200" |
|||
> |
|||
<slot /> |
|||
<div class="container mx-auto max-w-3xl"> |
|||
<div |
|||
class="mx-3 overflow-hidden rounded-lg bg-white text-gray-700 shadow-md dark:bg-neutral-700 dark:text-neutral-200" |
|||
> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
<template> |
|||
<div class="flex flex-shrink-0 items-center space-x-2"> |
|||
<div class="flex flex-shrink-0 flex-col items-center gap-2 sm:flex-row"> |
|||
<slot /> |
|||
</div> |
|||
</template> |
|||
|
|||
@ -0,0 +1,21 @@ |
|||
<template> |
|||
<a |
|||
:href="`/api/auth/${provider}`" |
|||
class="flex items-center justify-center gap-2 rounded border border-gray-300 bg-white py-2 text-sm text-gray-700 shadow-sm transition hover:bg-gray-50 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700" |
|||
> |
|||
<IconsBrandsProvider :provider="provider" class="h-4 w-4" /> |
|||
<span> |
|||
{{ $t('login.signInWith', [info.friendlyName]) }} |
|||
</span> |
|||
</a> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
info: { |
|||
enabled: boolean; |
|||
friendlyName: string; |
|||
}; |
|||
provider: OAUTH_PROVIDER; |
|||
}>(); |
|||
</script> |
|||
@ -0,0 +1,108 @@ |
|||
<template> |
|||
<div> |
|||
<form class="flex flex-col gap-5" @submit.prevent="submit"> |
|||
<BaseInput |
|||
v-model="totp" |
|||
type="text" |
|||
name="totp" |
|||
:placeholder="$t('general.2faCode')" |
|||
autocomplete="one-time-code" |
|||
inputmode="numeric" |
|||
maxlength="6" |
|||
pattern="\d{6}" |
|||
autofocus |
|||
/> |
|||
|
|||
<button |
|||
class="rounded bg-red-800 py-2 text-sm text-white shadow transition hover:bg-red-700 disabled:cursor-not-allowed disabled:bg-gray-200 dark:bg-red-800 dark:text-white dark:hover:bg-red-700 disabled:dark:bg-neutral-800" |
|||
:disabled="!totp || authenticating" |
|||
> |
|||
<IconsLoading v-if="authenticating" class="mx-auto w-5 animate-spin" /> |
|||
<span v-else>{{ $t('general.continue') }}</span> |
|||
</button> |
|||
|
|||
<button |
|||
type="button" |
|||
class="rounded border-2 border-gray-100 py-2 text-sm text-gray-700 transition hover:border-red-800 hover:bg-red-800 hover:text-white dark:border-neutral-600 dark:text-neutral-200" |
|||
@click="cancel" |
|||
> |
|||
{{ $t('dialog.cancel') }} |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const toast = useToast(); |
|||
const { t } = useI18n(); |
|||
|
|||
const authenticating = ref(false); |
|||
const totp = ref<string>(''); |
|||
|
|||
const { error } = await useFetch('/api/auth/pending'); |
|||
if (error.value) { |
|||
await navigateTo('/login'); |
|||
} |
|||
|
|||
const _submit = useSubmit( |
|||
(data) => |
|||
$fetch('/api/auth/verify-2fa', { |
|||
method: 'post', |
|||
body: data, |
|||
}), |
|||
{ |
|||
revert: async (success, data) => { |
|||
if (success) { |
|||
if (data?.status === 'success') { |
|||
await navigateTo('/'); |
|||
return; |
|||
} else if (data?.status === 'INVALID_TOTP_CODE') { |
|||
authenticating.value = false; |
|||
totp.value = ''; |
|||
toast.showToast({ |
|||
title: t('general.2fa'), |
|||
message: t('login.2faWrong'), |
|||
type: 'error', |
|||
}); |
|||
return; |
|||
} else if (data?.status === 'PENDING_LOGIN_EXPIRED') { |
|||
toast.showToast({ |
|||
title: t('general.2fa'), |
|||
message: t('login.loginExpired'), |
|||
type: 'error', |
|||
}); |
|||
await navigateTo('/login'); |
|||
return; |
|||
} |
|||
} |
|||
authenticating.value = false; |
|||
}, |
|||
noSuccessToast: true, |
|||
} |
|||
); |
|||
|
|||
async function submit() { |
|||
if (!totp.value || authenticating.value) return; |
|||
|
|||
authenticating.value = true; |
|||
return _submit({ totpCode: totp.value }); |
|||
} |
|||
|
|||
const _cancel = useSubmit( |
|||
(data) => |
|||
$fetch('/api/auth/cancel', { |
|||
method: 'post', |
|||
body: data, |
|||
}), |
|||
{ |
|||
revert: async () => { |
|||
await navigateTo('/login'); |
|||
}, |
|||
noSuccessToast: true, |
|||
} |
|||
); |
|||
|
|||
async function cancel() { |
|||
return _cancel({}); |
|||
} |
|||
</script> |
|||
@ -0,0 +1,147 @@ |
|||
<template> |
|||
<div class="flex flex-col gap-5"> |
|||
<div |
|||
v-if="authMethods && authMethods.oauthEnabled" |
|||
class="flex flex-col gap-5" |
|||
> |
|||
<UiLoginOauthButton |
|||
v-for="(info, provider) in authMethods.providers" |
|||
:key="provider" |
|||
:provider="provider" |
|||
:info="info" |
|||
/> |
|||
|
|||
<!-- Divider --> |
|||
<div |
|||
v-if="authMethods.oauthEnabled && !authMethods.passwordDisabled" |
|||
class="flex items-center gap-2" |
|||
> |
|||
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div> |
|||
<span class="text-xs text-gray-500 dark:text-neutral-400"> |
|||
{{ $t('login.or') }} |
|||
</span> |
|||
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Classic Login Form --> |
|||
<form |
|||
v-if="!authMethods?.passwordDisabled" |
|||
class="flex flex-col gap-5" |
|||
@submit.prevent="submit" |
|||
> |
|||
<BaseInput |
|||
v-model="username" |
|||
type="text" |
|||
:placeholder="$t('general.username')" |
|||
autocomplete="username" |
|||
autofocus |
|||
name="username" |
|||
/> |
|||
|
|||
<BaseInput |
|||
v-model="password" |
|||
type="password" |
|||
name="password" |
|||
:placeholder="$t('general.password')" |
|||
autocomplete="current-password" |
|||
/> |
|||
|
|||
<label |
|||
class="flex gap-2 whitespace-nowrap" |
|||
:title="$t('login.rememberMeDesc')" |
|||
> |
|||
<BaseSwitch v-model="remember" /> |
|||
<span class="text-sm">{{ $t('login.rememberMe') }}</span> |
|||
</label> |
|||
|
|||
<button |
|||
class="rounded bg-red-800 py-2 text-sm text-white shadow transition hover:bg-red-700 disabled:cursor-not-allowed disabled:bg-gray-200 dark:bg-red-800 dark:text-white dark:hover:bg-red-700 disabled:dark:bg-neutral-800" |
|||
:disabled="!password || !username" |
|||
> |
|||
<IconsLoading v-if="authenticating" class="mx-auto w-5 animate-spin" /> |
|||
<span v-else>{{ $t('login.signIn') }}</span> |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const toast = useToast(); |
|||
const { t } = useI18n(); |
|||
const route = useRoute(); |
|||
|
|||
const authenticating = ref(false); |
|||
const remember = ref(false); |
|||
const username = ref<string>(''); |
|||
const password = ref<string>(''); |
|||
|
|||
const { data: authMethods } = await useFetch('/api/auth/methods'); |
|||
|
|||
watchEffect(() => { |
|||
const autoLauchQuery = |
|||
typeof route.query.auto_launch === 'string' && !!route.query.auto_launch |
|||
? route.query.auto_launch |
|||
: undefined; |
|||
|
|||
if (authMethods.value?.autoLaunchProvider && !autoLauchQuery) { |
|||
navigateTo(`/api/auth/${authMethods.value.autoLaunchProvider}`, { |
|||
external: true, |
|||
}); |
|||
} |
|||
|
|||
if ( |
|||
autoLauchQuery && |
|||
autoLauchQuery !== 'false' && |
|||
authMethods.value?.providers?.[autoLauchQuery as OAUTH_PROVIDER] |
|||
) { |
|||
navigateTo(`/api/auth/${autoLauchQuery}`, { |
|||
external: true, |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
const _submit = useSubmit( |
|||
(data) => |
|||
$fetch('/api/auth/password', { |
|||
method: 'post', |
|||
body: data, |
|||
}), |
|||
{ |
|||
revert: async (success, data) => { |
|||
if (success) { |
|||
if (data?.status === 'success') { |
|||
await navigateTo('/'); |
|||
} else if (data?.status === 'TOTP_REQUIRED') { |
|||
authenticating.value = false; |
|||
await navigateTo('/login/2fa'); |
|||
return; |
|||
} else if (data?.status === 'INVALID_TOTP_CODE') { |
|||
authenticating.value = false; |
|||
toast.showToast({ |
|||
title: t('general.2fa'), |
|||
message: t('login.2faWrong'), |
|||
type: 'error', |
|||
}); |
|||
return; |
|||
} |
|||
} |
|||
authenticating.value = false; |
|||
password.value = ''; |
|||
}, |
|||
noSuccessToast: true, |
|||
} |
|||
); |
|||
|
|||
async function submit() { |
|||
if (!username.value || !password.value || authenticating.value) return; |
|||
|
|||
authenticating.value = true; |
|||
|
|||
return _submit({ |
|||
username: username.value, |
|||
password: password.value, |
|||
remember: remember.value, |
|||
}); |
|||
} |
|||
</script> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue