mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-28 02:06:37 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee885fbff5 | ||
|
|
333a1a18d5 | ||
|
|
1ff20caa3c | ||
|
|
f6f2736bba | ||
|
|
993330d932 | ||
|
|
204313aacf | ||
|
|
0729ce9e1a | ||
|
|
2d0bd8dcbf | ||
|
|
ff75322e7d | ||
|
|
daced661c4 | ||
|
|
0716c38fb8 | ||
|
|
789d9394a5 |
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,3 +1,30 @@
|
|||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.21.0...v) (2025-01-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add warning if passkeys missing ([2d0bd8d](https://github.com/stonith404/pocket-id/commit/2d0bd8dcbfb73650b7829cb66f40decb284bd73b))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow first and last name of user to be between 1 and 50 characters ([1ff20ca](https://github.com/stonith404/pocket-id/commit/1ff20caa3ccd651f9fb30f958ffb807dfbbcbd8a))
|
||||||
|
* hash in callback url is incorrectly appended ([f6f2736](https://github.com/stonith404/pocket-id/commit/f6f2736bba65eee017f2d8cdaa70621574092869))
|
||||||
|
* make user validation consistent between pages ([333a1a1](https://github.com/stonith404/pocket-id/commit/333a1a18d59f675111f4ed106fa5614ef563c6f4))
|
||||||
|
* passkey can't be added if `PUBLIC_APP_URL` includes a port ([0729ce9](https://github.com/stonith404/pocket-id/commit/0729ce9e1a8dab9912900a01dcd0fbd892718a1a))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.20.1...v) (2024-12-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* improve error state design for login page ([0716c38](https://github.com/stonith404/pocket-id/commit/0716c38fb8ce7fa719c7fe0df750bdb213786c21))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* OIDC client logo gets removed if other properties get updated ([789d939](https://github.com/stonith404/pocket-id/commit/789d9394a533831e7e2fb8dc3f6b338787336ad8))
|
||||||
|
|
||||||
## [](https://github.com/stonith404/pocket-id/compare/v0.20.0...v) (2024-12-13)
|
## [](https://github.com/stonith404/pocket-id/compare/v0.20.0...v) (2024-12-13)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -17,7 +17,7 @@ Additionally, what makes Pocket ID special is that it only supports [passkey](ht
|
|||||||
|
|
||||||
### Before you start
|
### Before you start
|
||||||
|
|
||||||
Pocket ID requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), meaning it must be served over HTTPS. This is necessary because Pocket ID uses the [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) which requires a secure context.
|
Pocket ID requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), meaning it must be served over HTTPS. This is necessary because Pocket ID uses the [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API).
|
||||||
|
|
||||||
### Installation with Docker (recommended)
|
### Installation with Docker (recommended)
|
||||||
|
|
||||||
@@ -78,14 +78,14 @@ Required tools:
|
|||||||
|
|
||||||
# Optional: Start Caddy (You can use any other reverse proxy)
|
# Optional: Start Caddy (You can use any other reverse proxy)
|
||||||
cd ..
|
cd ..
|
||||||
pm2 start --name pocket-id-caddy caddy -- run --config Caddyfile
|
pm2 start --name pocket-id-caddy caddy -- run --config reverse-proxy/Caddyfile
|
||||||
```
|
```
|
||||||
|
|
||||||
You can now sign in with the admin account on `http://localhost/login/setup`.
|
You can now sign in with the admin account on `http://localhost/login/setup`.
|
||||||
|
|
||||||
### Nginx Reverse Proxy
|
### Nginx Reverse Proxy
|
||||||
|
|
||||||
To use Nginx in front of Pocket ID, add the following configuration to increase the header buffer size because, as SvelteKit generates larger headers.
|
To use Nginx as a reverse proxy for Pocket ID, update the configuration to increase the header buffer size. This adjustment is necessary because SvelteKit generates larger headers, which may exceed the default buffer limits.
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
proxy_busy_buffers_size 512k;
|
proxy_busy_buffers_size 512k;
|
||||||
@@ -95,7 +95,7 @@ proxy_buffer_size 256k;
|
|||||||
|
|
||||||
## Proxy Services with Pocket ID
|
## Proxy Services with Pocket ID
|
||||||
|
|
||||||
As the goal of Pocket ID is to stay simple, we don't have a built-in proxy provider. However, you can use [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy) to add authentication to your services that don't support OIDC.
|
As the goal of Pocket ID is to stay simple, it doesn't have a built-in proxy provider. However, you can use [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy) to add authentication to your services that don't support OIDC.
|
||||||
|
|
||||||
See the [guide](docs/proxy-services.md) for more information.
|
See the [guide](docs/proxy-services.md) for more information.
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ docker compose up -d
|
|||||||
|
|
||||||
# Optional: Start Caddy (You can use any other reverse proxy)
|
# Optional: Start Caddy (You can use any other reverse proxy)
|
||||||
cd ..
|
cd ..
|
||||||
pm2 start caddy --name pocket-id-caddy -- run --config Caddyfile
|
pm2 start caddy --name pocket-id-caddy -- run --config reverse-proxy/Caddyfile
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ type UserDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserCreateDto struct {
|
type UserCreateDto struct {
|
||||||
Username string `json:"username" binding:"required,username,min=3,max=20"`
|
Username string `json:"username" binding:"required,username,min=2,max=50"`
|
||||||
Email string `json:"email" binding:"required,email"`
|
Email string `json:"email" binding:"required,email"`
|
||||||
FirstName string `json:"firstName" binding:"required,min=3,max=30"`
|
FirstName string `json:"firstName" binding:"required,min=1,max=50"`
|
||||||
LastName string `json:"lastName" binding:"required,min=3,max=30"`
|
LastName string `json:"lastName" binding:"required,min=1,max=50"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
@@ -96,7 +95,7 @@ func (s *JwtService) GenerateAccessToken(user model.User) (string, error) {
|
|||||||
Subject: user.ID,
|
Subject: user.ID,
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(sessionDurationInMinutes) * time.Minute)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(sessionDurationInMinutes) * time.Minute)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
Audience: jwt.ClaimStrings{utils.GetHostFromURL(common.EnvConfig.AppURL)},
|
Audience: jwt.ClaimStrings{common.EnvConfig.AppURL},
|
||||||
},
|
},
|
||||||
IsAdmin: user.IsAdmin,
|
IsAdmin: user.IsAdmin,
|
||||||
}
|
}
|
||||||
@@ -125,7 +124,7 @@ func (s *JwtService) VerifyAccessToken(tokenString string) (*AccessTokenJWTClaim
|
|||||||
return nil, errors.New("can't parse claims")
|
return nil, errors.New("can't parse claims")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Contains(claims.Audience, utils.GetHostFromURL(common.EnvConfig.AppURL)) {
|
if !slices.Contains(claims.Audience, common.EnvConfig.AppURL) {
|
||||||
return nil, errors.New("audience doesn't match")
|
return nil, errors.New("audience doesn't match")
|
||||||
}
|
}
|
||||||
return claims, nil
|
return claims, nil
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type WebAuthnService struct {
|
|||||||
func NewWebAuthnService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, appConfigService *AppConfigService) *WebAuthnService {
|
func NewWebAuthnService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, appConfigService *AppConfigService) *WebAuthnService {
|
||||||
webauthnConfig := &webauthn.Config{
|
webauthnConfig := &webauthn.Config{
|
||||||
RPDisplayName: appConfigService.DbConfig.AppName.Value,
|
RPDisplayName: appConfigService.DbConfig.AppName.Value,
|
||||||
RPID: utils.GetHostFromURL(common.EnvConfig.AppURL),
|
RPID: utils.GetHostnameFromURL(common.EnvConfig.AppURL),
|
||||||
RPOrigins: []string{common.EnvConfig.AppURL},
|
RPOrigins: []string{common.EnvConfig.AppURL},
|
||||||
Timeouts: webauthn.TimeoutsConfig{
|
Timeouts: webauthn.TimeoutsConfig{
|
||||||
Login: webauthn.TimeoutConfig{
|
Login: webauthn.TimeoutConfig{
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ func GenerateRandomAlphanumericString(length int) (string, error) {
|
|||||||
return string(result), nil
|
return string(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHostFromURL(rawURL string) string {
|
func GetHostnameFromURL(rawURL string) string {
|
||||||
parsedURL, err := url.Parse(rawURL)
|
parsedURL, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return parsedURL.Host
|
return parsedURL.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringPointer creates a string pointer from a string value
|
// StringPointer creates a string pointer from a string value
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ upstreams="http://<service-to-be-proxied>:<port>"
|
|||||||
|
|
||||||
# Additional Configuration
|
# Additional Configuration
|
||||||
provider="oidc"
|
provider="oidc"
|
||||||
scope = "openid email profile"
|
scope = "openid email profile groups"
|
||||||
|
|
||||||
# If you are using a reverse proxy in front of OAuth2 Proxy
|
# If you are using a reverse proxy in front of OAuth2 Proxy
|
||||||
reverse_proxy = true
|
reverse_proxy = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.20.1",
|
"version": "0.22.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --port 3000",
|
"dev": "vite dev --port 3000",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn("text-sm [&_p]:leading-relaxed", className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
21
frontend/src/lib/components/ui/alert/alert-title.svelte
Normal file
21
frontend/src/lib/components/ui/alert/alert-title.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { HeadingLevel } from "./index.js";
|
||||||
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||||
|
level?: HeadingLevel;
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let level: $$Props["level"] = "h5";
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={level}
|
||||||
|
class={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</svelte:element>
|
||||||
17
frontend/src/lib/components/ui/alert/alert.svelte
Normal file
17
frontend/src/lib/components/ui/alert/alert.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { type Variant, alertVariants } from "./index.js";
|
||||||
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
variant?: Variant;
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let variant: $$Props["variant"] = "default";
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
35
frontend/src/lib/components/ui/alert/index.ts
Normal file
35
frontend/src/lib/components/ui/alert/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
import Description from './alert-description.svelte';
|
||||||
|
import Title from './alert-title.svelte';
|
||||||
|
import Root from './alert.svelte';
|
||||||
|
|
||||||
|
export const alertVariants = tv({
|
||||||
|
base: '[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-background text-foreground',
|
||||||
|
destructive:
|
||||||
|
'border-destructive/50 text-destructive text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||||
|
warning:
|
||||||
|
'bg-amber-100 text-amber-900 dark:bg-amber-900 dark:text-amber-100 [&>svg]:text-amber-900 dark:[&>svg]:text-amber-100'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Variant = VariantProps<typeof alertVariants>['variant'];
|
||||||
|
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||||
|
|
||||||
|
export {
|
||||||
|
//
|
||||||
|
Root as Alert,
|
||||||
|
Description as AlertDescription,
|
||||||
|
Title as AlertTitle,
|
||||||
|
Description,
|
||||||
|
Root,
|
||||||
|
Title
|
||||||
|
};
|
||||||
@@ -10,7 +10,7 @@ export type OidcClient = {
|
|||||||
export type OidcClientCreate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
|
export type OidcClientCreate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
|
||||||
|
|
||||||
export type OidcClientCreateWithLogo = OidcClientCreate & {
|
export type OidcClientCreateWithLogo = OidcClientCreate & {
|
||||||
logo: File | null;
|
logo: File | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthorizeResponse = {
|
export type AuthorizeResponse = {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
let authorizationRequired = false;
|
let authorizationRequired = false;
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
let { scope, nonce, client, state, callbackURL, codeChallenge, codeChallengeMethod } = data;
|
let { scope, nonce, client, state, callbackURL, codeChallenge, codeChallengeMethod } = data;
|
||||||
|
|
||||||
async function authorize() {
|
async function authorize() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@@ -55,7 +55,14 @@
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
try {
|
try {
|
||||||
await oidService
|
await oidService
|
||||||
.authorizeNewClient(client!.id, scope, callbackURL, nonce, codeChallenge, codeChallengeMethod)
|
.authorizeNewClient(
|
||||||
|
client!.id,
|
||||||
|
scope,
|
||||||
|
callbackURL,
|
||||||
|
nonce,
|
||||||
|
codeChallenge,
|
||||||
|
codeChallengeMethod
|
||||||
|
)
|
||||||
.then(async ({ code, callbackURL }) => {
|
.then(async ({ code, callbackURL }) => {
|
||||||
onSuccess(code, callbackURL);
|
onSuccess(code, callbackURL);
|
||||||
});
|
});
|
||||||
@@ -68,7 +75,11 @@
|
|||||||
function onSuccess(code: string, callbackURL: string) {
|
function onSuccess(code: string, callbackURL: string) {
|
||||||
success = true;
|
success = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `${callbackURL}?code=${code}&state=${state}`;
|
const redirectURL = new URL(callbackURL);
|
||||||
|
redirectURL.searchParams.append('code', code);
|
||||||
|
redirectURL.searchParams.append('state', state);
|
||||||
|
|
||||||
|
window.location.href = redirectURL.toString();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||||
import Logo from '$lib/components/logo.svelte';
|
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import WebAuthnService from '$lib/services/webauthn-service';
|
import WebAuthnService from '$lib/services/webauthn-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import userStore from '$lib/stores/user-store';
|
import userStore from '$lib/stores/user-store';
|
||||||
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||||
import { startAuthentication } from '@simplewebauthn/browser';
|
import { startAuthentication } from '@simplewebauthn/browser';
|
||||||
import { toast } from 'svelte-sonner';
|
import { fade } from 'svelte/transition';
|
||||||
|
import LoginLogoErrorIndicator from './components/login-logo-error-indicator.svelte';
|
||||||
const webauthnService = new WebAuthnService();
|
const webauthnService = new WebAuthnService();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
let error: string | undefined = $state(undefined);
|
||||||
|
|
||||||
async function authenticate() {
|
async function authenticate() {
|
||||||
|
error = undefined;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
try {
|
try {
|
||||||
const loginOptions = await webauthnService.getLoginOptions();
|
const loginOptions = await webauthnService.getLoginOptions();
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
userStore.setUser(user);
|
userStore.setUser(user);
|
||||||
goto('/settings');
|
goto('/settings');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(getWebauthnErrorMessage(e));
|
error = getWebauthnErrorMessage(e);
|
||||||
}
|
}
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -35,15 +37,21 @@
|
|||||||
|
|
||||||
<SignInWrapper>
|
<SignInWrapper>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="bg-muted rounded-2xl p-3">
|
<LoginLogoErrorIndicator error={!!error} />
|
||||||
<Logo class="h-10 w-10" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||||
Sign in to {$appConfigStore.appName}
|
Sign in to {$appConfigStore.appName}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-muted-foreground mt-2">
|
{#if error}
|
||||||
Authenticate yourself with your passkey to access the admin panel
|
<p class="text-muted-foreground mt-2" in:fade>
|
||||||
</p>
|
{error}. Please try to sign in again.
|
||||||
<Button class="mt-5" {isLoading} on:click={authenticate}>Authenticate</Button>
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-muted-foreground mt-2" in:fade>
|
||||||
|
Authenticate yourself with your passkey to access the admin panel.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<Button class="mt-10" {isLoading} on:click={authenticate}
|
||||||
|
>{error ? 'Try again' : 'Authenticate'}</Button
|
||||||
|
>
|
||||||
</SignInWrapper>
|
</SignInWrapper>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Logo from '$lib/components/logo.svelte';
|
||||||
|
import CrossAnimated from '$lib/icons/cross-animated.svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
const {
|
||||||
|
error
|
||||||
|
}: {
|
||||||
|
error: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-2xl p-3 transition-[background-color] duration-300
|
||||||
|
{error ? 'bg-red-200' : 'bg-muted'}"
|
||||||
|
>
|
||||||
|
{#if error}
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center">
|
||||||
|
<CrossAnimated class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div in:fade={{ duration: 300 }}>
|
||||||
|
<Logo class="h-10 w-10" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as Alert from '$lib/components/ui/alert';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
import type { UserCreate } from '$lib/types/user.type';
|
import type { UserCreate } from '$lib/types/user.type';
|
||||||
import { axiosErrorToast, getWebauthnErrorMessage } from '$lib/utils/error-util';
|
import { axiosErrorToast, getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||||
import { startRegistration } from '@simplewebauthn/browser';
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
|
import { LucideAlertTriangle } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import AccountForm from './account-form.svelte';
|
import AccountForm from './account-form.svelte';
|
||||||
import PasskeyList from './passkey-list.svelte';
|
import PasskeyList from './passkey-list.svelte';
|
||||||
@@ -52,6 +54,16 @@
|
|||||||
<title>Account Settings</title>
|
<title>Account Settings</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if passkeys.length == 0}
|
||||||
|
<Alert.Root variant="warning">
|
||||||
|
<LucideAlertTriangle class="size-4" />
|
||||||
|
<Alert.Title>Passkey missing</Alert.Title>
|
||||||
|
<Alert.Description
|
||||||
|
>Please add a passkey to prevent losing access to your account.</Alert.Description
|
||||||
|
>
|
||||||
|
</Alert.Root>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if $appConfigStore.allowOwnAccountEdit}
|
{#if $appConfigStore.allowOwnAccountEdit}
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
@@ -77,7 +89,7 @@
|
|||||||
</Card.Header>
|
</Card.Header>
|
||||||
{#if passkeys.length != 0}
|
{#if passkeys.length != 0}
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<PasskeyList {passkeys} />
|
<PasskeyList bind:passkeys />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@@ -16,9 +16,16 @@
|
|||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(2).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: z.string().min(2).max(50),
|
lastName: z.string().min(1).max(50),
|
||||||
username: z.string().min(2).max(50),
|
username: z
|
||||||
|
.string()
|
||||||
|
.min(2)
|
||||||
|
.max(30)
|
||||||
|
.regex(
|
||||||
|
/^[a-z0-9_@.-]+$/,
|
||||||
|
"Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols"
|
||||||
|
),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
isAdmin: z.boolean()
|
isAdmin: z.boolean()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,15 +9,10 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
||||||
|
|
||||||
let { passkeys: initialsPasskeys }: { passkeys: Passkey[] } = $props();
|
let { passkeys = $bindable() }: { passkeys: Passkey[] } = $props();
|
||||||
let passkeys = $state<Passkey[]>(initialsPasskeys);
|
|
||||||
|
|
||||||
const webauthnService = new WebauthnService();
|
const webauthnService = new WebauthnService();
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
passkeys = initialsPasskeys;
|
|
||||||
});
|
|
||||||
|
|
||||||
let passkeyToRename: Passkey | null = $state(null);
|
let passkeyToRename: Passkey | null = $state(null);
|
||||||
|
|
||||||
async function deletePasskey(passkey: Passkey) {
|
async function deletePasskey(passkey: Passkey) {
|
||||||
|
|||||||
@@ -33,7 +33,10 @@
|
|||||||
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
|
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
|
||||||
let success = true;
|
let success = true;
|
||||||
const dataPromise = oidcService.updateClient(client.id, updatedClient);
|
const dataPromise = oidcService.updateClient(client.id, updatedClient);
|
||||||
const imagePromise = oidcService.updateClientLogo(client, updatedClient.logo);
|
const imagePromise =
|
||||||
|
updatedClient.logo !== undefined
|
||||||
|
? oidcService.updateClientLogo(client, updatedClient.logo)
|
||||||
|
: Promise.resolve();
|
||||||
|
|
||||||
client.isPublic = updatedClient.isPublic;
|
client.isPublic = updatedClient.isPublic;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let logo = $state<File | null>(null);
|
let logo = $state<File | null | undefined>();
|
||||||
let logoDataURL: string | null = $state(
|
let logoDataURL: string | null = $state(
|
||||||
existingClient?.hasLogo ? `/api/oidc/clients/${existingClient!.id}/logo` : null
|
existingClient?.hasLogo ? `/api/oidc/clients/${existingClient!.id}/logo` : null
|
||||||
);
|
);
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
onchange={onLogoChange}
|
onchange={onLogoChange}
|
||||||
>
|
>
|
||||||
<Button variant="secondary">
|
<Button variant="secondary">
|
||||||
{existingClient?.hasLogo ? 'Change Logo' : 'Upload Logo'}
|
{logoDataURL ? 'Change Logo' : 'Upload Logo'}
|
||||||
</Button>
|
</Button>
|
||||||
</FileInput>
|
</FileInput>
|
||||||
{#if logoDataURL}
|
{#if logoDataURL}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(2).max(30),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: z.string().min(2).max(30),
|
lastName: z.string().min(1).max(50),
|
||||||
username: z
|
username: z
|
||||||
.string()
|
.string()
|
||||||
.min(2)
|
.min(2)
|
||||||
|
|||||||
Reference in New Issue
Block a user