Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4010ee27d6 | ||
|
|
4e7574a297 | ||
|
|
8038a111dd | ||
|
|
c6f83a581a | ||
|
|
8ad632e6c1 | ||
|
|
903b0b3918 | ||
|
|
fd21ce5aac | ||
|
|
e7861df95a | ||
|
|
8e27320649 | ||
|
|
2b9413c757 | ||
|
|
fd5a881cfb | ||
|
|
28ed064668 | ||
|
|
5446b46b65 | ||
|
|
0ce6045657 |
28
CHANGELOG.md
@@ -1,3 +1,31 @@
|
|||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.4.0...v) (2024-09-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add name claim to userinfo endpoint and id token ([4e7574a](https://github.com/stonith404/pocket-id/commit/4e7574a297307395603267c7a3285d538d4111d8))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* limit width of content on large screens ([c6f83a5](https://github.com/stonith404/pocket-id/commit/c6f83a581ad385391d77fec7eeb385060742f097))
|
||||||
|
* show error message if error occurs while authorizing new client ([8038a11](https://github.com/stonith404/pocket-id/commit/8038a111dd7fa8f5d421b29c3bc0c11d865dc71b))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.3.1...v) (2024-09-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add setup details to oidc client details ([fd21ce5](https://github.com/stonith404/pocket-id/commit/fd21ce5aac1daeba04e4e7399a0720338ea710c2))
|
||||||
|
* add support for more username formats ([903b0b3](https://github.com/stonith404/pocket-id/commit/903b0b39181c208e9411ee61849d2671e7c56dc5))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* non pointer passed to create user ([e7861df](https://github.com/stonith404/pocket-id/commit/e7861df95a6beecab359d1c56f4383373f74bb73))
|
||||||
|
* oidc client logo not displayed on authorize page ([28ed064](https://github.com/stonith404/pocket-id/commit/28ed064668afeec8f80adda59ba94f1fc2fbce17))
|
||||||
|
* typo in hasLogo property of oidc dto ([2b9413c](https://github.com/stonith404/pocket-id/commit/2b9413c7575e1322f8547490a9b02a1836bad549))
|
||||||
|
|
||||||
## [](https://github.com/stonith404/pocket-id/compare/v0.3.0...v) (2024-08-24)
|
## [](https://github.com/stonith404/pocket-id/compare/v0.3.0...v) (2024-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.7 MiB |
@@ -1,17 +1 @@
|
|||||||
<svg id="a"
|
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015"><path d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/><style>@media (prefers-color-scheme:dark){#a path{fill:#fff}}@media (prefers-color-scheme:light){#a path{fill:#000}}</style></svg>
|
||||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1015 1015">
|
|
||||||
<path d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z" />
|
|
||||||
<style>
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
#a path {
|
|
||||||
fill: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
#a path {
|
|
||||||
fill: #000000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 539 B |
@@ -37,7 +37,7 @@ func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) {
|
|||||||
"userinfo_endpoint": appUrl + "/api/oidc/userinfo",
|
"userinfo_endpoint": appUrl + "/api/oidc/userinfo",
|
||||||
"jwks_uri": appUrl + "/.well-known/jwks.json",
|
"jwks_uri": appUrl + "/.well-known/jwks.json",
|
||||||
"scopes_supported": []string{"openid", "profile", "email"},
|
"scopes_supported": []string{"openid", "profile", "email"},
|
||||||
"claims_supported": []string{"sub", "given_name", "family_name", "email", "preferred_username"},
|
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "preferred_username"},
|
||||||
"response_types_supported": []string{"code", "id_token"},
|
"response_types_supported": []string{"code", "id_token"},
|
||||||
"subject_types_supported": []string{"public"},
|
"subject_types_supported": []string{"public"},
|
||||||
"id_token_signing_alg_values_supported": []string{"RS256"},
|
"id_token_signing_alg_values_supported": []string{"RS256"},
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
type PublicOidcClientDto struct {
|
type PublicOidcClientDto struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
HasLogo bool `json:"hasLogo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcClientDto struct {
|
type OidcClientDto struct {
|
||||||
PublicOidcClientDto
|
PublicOidcClientDto
|
||||||
HasLogo bool `json:"hasLogo"`
|
|
||||||
CallbackURLs []string `json:"callbackURLs"`
|
CallbackURLs []string `json:"callbackURLs"`
|
||||||
CreatedBy UserDto `json:"createdBy"`
|
CreatedBy UserDto `json:"createdBy"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,3 @@ type OneTimeAccessTokenCreateDto struct {
|
|||||||
UserID string `json:"userId" binding:"required"`
|
UserID string `json:"userId" binding:"required"`
|
||||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginUserDto struct {
|
|
||||||
Username string `json:"username" binding:"required"`
|
|
||||||
Password string `json:"password" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ var validateUrlList validator.Func = func(fl validator.FieldLevel) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
|
var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
|
||||||
regex := "^[a-z0-9_]*$"
|
// [a-zA-Z0-9] : The username must start with an alphanumeric character
|
||||||
|
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
|
||||||
|
// [a-zA-Z0-9]$ : The username must end with an alphanumeric character
|
||||||
|
regex := "^[a-zA-Z0-9][a-zA-Z0-9_.@-]*[a-zA-Z0-9]$"
|
||||||
matched, _ := regexp.MatchString(regex, fl.Field().String())
|
matched, _ := regexp.MatchString(regex, fl.Field().String())
|
||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ func (s *OidcService) GetUserClaimsForClient(userID string, clientID string) (ma
|
|||||||
profileClaims := map[string]interface{}{
|
profileClaims := map[string]interface{}{
|
||||||
"given_name": user.FirstName,
|
"given_name": user.FirstName,
|
||||||
"family_name": user.LastName,
|
"family_name": user.LastName,
|
||||||
|
"name": user.FirstName + " " + user.LastName,
|
||||||
"preferred_username": user.Username,
|
"preferred_username": user.Username,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,20 +48,20 @@ func (s *UserService) DeleteUser(userID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) {
|
func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) {
|
||||||
user := &model.User{
|
user := model.User{
|
||||||
FirstName: input.FirstName,
|
FirstName: input.FirstName,
|
||||||
LastName: input.LastName,
|
LastName: input.LastName,
|
||||||
Email: input.Email,
|
Email: input.Email,
|
||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
IsAdmin: input.IsAdmin,
|
IsAdmin: input.IsAdmin,
|
||||||
}
|
}
|
||||||
if err := s.db.Create(user).Error; err != nil {
|
if err := s.db.Create(&user).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
return model.User{}, s.checkDuplicatedFields(*user)
|
return model.User{}, s.checkDuplicatedFields(user)
|
||||||
}
|
}
|
||||||
return model.User{}, err
|
return model.User{}, err
|
||||||
}
|
}
|
||||||
return *user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, updateOwnUser bool) (model.User, error) {
|
func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, updateOwnUser bool) (model.User, error) {
|
||||||
@@ -46,7 +46,7 @@ func handleValidationError(validationErrors validator.ValidationErrors) string {
|
|||||||
case "email":
|
case "email":
|
||||||
errorMessage = fmt.Sprintf("%s must be a valid email address", fieldName)
|
errorMessage = fmt.Sprintf("%s must be a valid email address", fieldName)
|
||||||
case "username":
|
case "username":
|
||||||
errorMessage = fmt.Sprintf("%s must contain only lowercase letters, numbers, and underscores", fieldName)
|
errorMessage = fmt.Sprintf("%s must only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols and not start or end with a special character", fieldName)
|
||||||
case "url":
|
case "url":
|
||||||
errorMessage = fmt.Sprintf("%s must be a valid URL", fieldName)
|
errorMessage = fmt.Sprintf("%s must be a valid URL", fieldName)
|
||||||
case "min":
|
case "min":
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<Avatar.Fallback>{initials}</Avatar.Fallback>
|
<Avatar.Fallback>{initials}</Avatar.Fallback>
|
||||||
</Avatar.Root></DropdownMenu.Trigger
|
</Avatar.Root></DropdownMenu.Trigger
|
||||||
>
|
>
|
||||||
<DropdownMenu.Content class="w-40" align="start">
|
<DropdownMenu.Content class="min-w-40" align="start">
|
||||||
<DropdownMenu.Label class="font-normal">
|
<DropdownMenu.Label class="font-normal">
|
||||||
<div class="flex flex-col space-y-1">
|
<div class="flex flex-col space-y-1">
|
||||||
<p class="text-sm font-medium leading-none">
|
<p class="text-sm font-medium leading-none">
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
import HeaderAvatar from './header-avatar.svelte';
|
import HeaderAvatar from './header-avatar.svelte';
|
||||||
|
|
||||||
let isAuthPage = $derived(
|
let isAuthPage = $derived(
|
||||||
!$page.error && ($page.url.pathname.startsWith('/authorize') || $page.url.pathname.startsWith('/login'))
|
!$page.error &&
|
||||||
|
($page.url.pathname.startsWith('/authorize') || $page.url.pathname.startsWith('/login'))
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" w-full {isAuthPage ? 'absolute top-0 z-10 mt-4' : 'border-b'}">
|
<div class=" w-full {isAuthPage ? 'absolute top-0 z-10 mt-4' : 'border-b'}">
|
||||||
<div class="mx-auto flex w-full max-w-[1520px] items-center justify-between px-4 md:px-10">
|
<div class="mx-auto flex w-full max-w-[1640px] items-center justify-between px-4 md:px-10">
|
||||||
<div class="flex h-16 items-center">
|
<div class="flex h-16 items-center">
|
||||||
{#if !isAuthPage}
|
{#if !isAuthPage}
|
||||||
<Logo class="mr-3 h-10 w-10" />
|
<Logo class="mr-3 h-10 w-10" />
|
||||||
|
|||||||
@@ -83,16 +83,17 @@
|
|||||||
<SignInWrapper>
|
<SignInWrapper>
|
||||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Sign in to {client.name}</h1>
|
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Sign in to {client.name}</h1>
|
||||||
{#if !authorizationRequired}
|
{#if errorMessage}
|
||||||
<p class="text-muted-foreground mb-10 mt-2">
|
<p class="text-muted-foreground mb-10 mt-2">
|
||||||
{#if errorMessage}
|
{errorMessage}. Please try again.
|
||||||
{errorMessage}. Please try again.
|
|
||||||
{:else}
|
|
||||||
Do you want to sign in to <b>{client.name}</b> with your
|
|
||||||
<b>{$applicationConfigurationStore.appName}</b> account?
|
|
||||||
{/if}
|
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{/if}
|
||||||
|
{#if !authorizationRequired && !errorMessage}
|
||||||
|
<p class="text-muted-foreground mb-10 mt-2">
|
||||||
|
Do you want to sign in to <b>{client.name}</b> with your
|
||||||
|
<b>{$applicationConfigurationStore.appName}</b> account?
|
||||||
|
</p>
|
||||||
|
{:else if authorizationRequired}
|
||||||
<div transition:slide={{ duration: 300 }}>
|
<div transition:slide={{ duration: 300 }}>
|
||||||
<Card.Root class="mb-10 mt-6">
|
<Card.Root class="mb-10 mt-6">
|
||||||
<Card.Header class="pb-5">
|
<Card.Header class="pb-5">
|
||||||
|
|||||||
@@ -22,24 +22,28 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="h-screen w-full">
|
<div class="bg-muted/40 h-screen w-full">
|
||||||
<main class="flex min-h-screen flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10">
|
<main
|
||||||
<div class="mx-auto grid w-full max-w-[1440px] gap-2">
|
class="mx-auto flex min-h-screen max-w-[1640px] flex-col gap-x-4 gap-y-10 p-4 md:p-10 lg:flex-row"
|
||||||
<h1 class="text-3xl font-semibold">Settings</h1>
|
>
|
||||||
</div>
|
<div>
|
||||||
<div
|
<div class="mx-auto grid w-full gap-2">
|
||||||
class="mx-auto grid w-full max-w-[1440px] items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]"
|
<h1 class="mb-5 text-3xl font-semibold">Settings</h1>
|
||||||
>
|
|
||||||
<nav class="grid gap-4 text-sm text-muted-foreground">
|
|
||||||
{#each links as { href, label }}
|
|
||||||
<a {href} class={$page.url.pathname.startsWith(href) ? 'font-bold text-primary' : ''}>
|
|
||||||
{label}
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
</nav>
|
|
||||||
<div class="flex flex-col gap-5">
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx-auto grid items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]"
|
||||||
|
>
|
||||||
|
<nav class="text-muted-foreground grid gap-4 text-sm">
|
||||||
|
{#each links as { href, label }}
|
||||||
|
<a {href} class={$page.url.pathname.startsWith(href) ? 'text-primary font-bold' : ''}>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-col gap-5">
|
||||||
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { beforeNavigate } from '$app/navigation';
|
import { beforeNavigate } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores';
|
||||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||||
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';
|
||||||
@@ -10,13 +11,24 @@
|
|||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideChevronLeft, LucideRefreshCcw } from 'lucide-svelte';
|
import { LucideChevronLeft, LucideRefreshCcw } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
import OidcForm from '../oidc-client-form.svelte';
|
import OidcForm from '../oidc-client-form.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let client = $state(data);
|
let client = $state(data);
|
||||||
|
let showAllDetails = $state(false);
|
||||||
|
|
||||||
const oidcService = new OidcService();
|
const oidcService = new OidcService();
|
||||||
|
|
||||||
|
const setupDetails = {
|
||||||
|
'Authorization URL': `https://${$page.url.hostname}/authorize`,
|
||||||
|
'OIDC Discovery URL': `https://${$page.url.hostname}/.well-known/openid-configuration`,
|
||||||
|
'Token URL': `https://${$page.url.hostname}/api/oidc/token`,
|
||||||
|
'Userinfo URL': `https://${$page.url.hostname}/api/oidc/userinfo`,
|
||||||
|
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`,
|
||||||
|
PKCE: 'Disabled'
|
||||||
|
};
|
||||||
|
|
||||||
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);
|
||||||
@@ -74,23 +86,43 @@
|
|||||||
<Card.Title>{client.name}</Card.Title>
|
<Card.Title>{client.name}</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<div class="flex">
|
<div class="flex flex-col">
|
||||||
<Label class="mb-0 w-44">Client ID</Label>
|
<div class="mb-2 flex">
|
||||||
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
<Label class="mb-0 w-44">Client ID</Label>
|
||||||
</div>
|
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
||||||
<div class="mt-3 flex items-center">
|
</div>
|
||||||
<Label class="mb-0 w-44">Client secret</Label>
|
<div class="mb-2 mt-1 flex items-center">
|
||||||
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
<Label class="w-44">Client secret</Label>
|
||||||
>{$clientSecretStore ?? '••••••••••••••••••••••••••••••••'}</span
|
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
||||||
>
|
>{$clientSecretStore ?? '••••••••••••••••••••••••••••••••'}</span
|
||||||
{#if !$clientSecretStore}
|
|
||||||
<Button
|
|
||||||
class="ml-2"
|
|
||||||
onclick={createClientSecret}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
aria-label="Create new client secret"><LucideRefreshCcw class="h-3 w-3" /></Button
|
|
||||||
>
|
>
|
||||||
|
{#if !$clientSecretStore}
|
||||||
|
<Button
|
||||||
|
class="ml-2"
|
||||||
|
onclick={createClientSecret}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="Create new client secret"><LucideRefreshCcw class="h-3 w-3" /></Button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if showAllDetails}
|
||||||
|
<div transition:slide>
|
||||||
|
{#each Object.entries(setupDetails) as [key, value]}
|
||||||
|
<div class="mb-5 flex">
|
||||||
|
<Label class="mb-0 w-44">{key}</Label>
|
||||||
|
<span class="text-muted-foreground text-sm">{value}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !showAllDetails}
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<Button on:click={() => (showAllDetails = true)} size="sm" variant="ghost"
|
||||||
|
>Show more details</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|||||||
@@ -32,7 +32,10 @@
|
|||||||
.string()
|
.string()
|
||||||
.min(2)
|
.min(2)
|
||||||
.max(30)
|
.max(30)
|
||||||
.regex(/^[a-z0-9_]+$/, 'Only lowercase letters, numbers, and underscores are allowed'),
|
.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()
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 578 KiB After Width: | Height: | Size: 528 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 58 KiB |