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)
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.7 MiB |
@@ -1,17 +1 @@
|
||||
<svg id="a"
|
||||
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>
|
||||
<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>
|
||||
|
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",
|
||||
"jwks_uri": appUrl + "/.well-known/jwks.json",
|
||||
"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"},
|
||||
"subject_types_supported": []string{"public"},
|
||||
"id_token_signing_alg_values_supported": []string{"RS256"},
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package dto
|
||||
|
||||
type PublicOidcClientDto struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
HasLogo bool `json:"hasLogo"`
|
||||
}
|
||||
|
||||
type OidcClientDto struct {
|
||||
PublicOidcClientDto
|
||||
HasLogo bool `json:"hasLogo"`
|
||||
CallbackURLs []string `json:"callbackURLs"`
|
||||
CreatedBy UserDto `json:"createdBy"`
|
||||
}
|
||||
|
||||
@@ -23,8 +23,3 @@ type OneTimeAccessTokenCreateDto struct {
|
||||
UserID string `json:"userId" 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 {
|
||||
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())
|
||||
return matched
|
||||
}
|
||||
|
||||
@@ -303,6 +303,7 @@ func (s *OidcService) GetUserClaimsForClient(userID string, clientID string) (ma
|
||||
profileClaims := map[string]interface{}{
|
||||
"given_name": user.FirstName,
|
||||
"family_name": user.LastName,
|
||||
"name": user.FirstName + " " + user.LastName,
|
||||
"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) {
|
||||
user := &model.User{
|
||||
user := model.User{
|
||||
FirstName: input.FirstName,
|
||||
LastName: input.LastName,
|
||||
Email: input.Email,
|
||||
Username: input.Username,
|
||||
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) {
|
||||
return model.User{}, s.checkDuplicatedFields(*user)
|
||||
return model.User{}, s.checkDuplicatedFields(user)
|
||||
}
|
||||
return model.User{}, err
|
||||
}
|
||||
return *user, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
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":
|
||||
errorMessage = fmt.Sprintf("%s must be a valid email address", fieldName)
|
||||
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":
|
||||
errorMessage = fmt.Sprintf("%s must be a valid URL", fieldName)
|
||||
case "min":
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<Avatar.Fallback>{initials}</Avatar.Fallback>
|
||||
</Avatar.Root></DropdownMenu.Trigger
|
||||
>
|
||||
<DropdownMenu.Content class="w-40" align="start">
|
||||
<DropdownMenu.Content class="min-w-40" align="start">
|
||||
<DropdownMenu.Label class="font-normal">
|
||||
<div class="flex flex-col space-y-1">
|
||||
<p class="text-sm font-medium leading-none">
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
import HeaderAvatar from './header-avatar.svelte';
|
||||
|
||||
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>
|
||||
|
||||
<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">
|
||||
{#if !isAuthPage}
|
||||
<Logo class="mr-3 h-10 w-10" />
|
||||
|
||||
@@ -83,16 +83,17 @@
|
||||
<SignInWrapper>
|
||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||
<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">
|
||||
{#if errorMessage}
|
||||
{errorMessage}. Please try again.
|
||||
{:else}
|
||||
Do you want to sign in to <b>{client.name}</b> with your
|
||||
<b>{$applicationConfigurationStore.appName}</b> account?
|
||||
{/if}
|
||||
{errorMessage}. Please try again.
|
||||
</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 }}>
|
||||
<Card.Root class="mb-10 mt-6">
|
||||
<Card.Header class="pb-5">
|
||||
|
||||
@@ -22,24 +22,28 @@
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="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">
|
||||
<div class="mx-auto grid w-full max-w-[1440px] gap-2">
|
||||
<h1 class="text-3xl font-semibold">Settings</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx-auto grid w-full max-w-[1440px] items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]"
|
||||
>
|
||||
<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 class="bg-muted/40 h-screen w-full">
|
||||
<main
|
||||
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"
|
||||
>
|
||||
<div>
|
||||
<div class="mx-auto grid w-full gap-2">
|
||||
<h1 class="mb-5 text-3xl font-semibold">Settings</h1>
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
@@ -10,13 +11,24 @@
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideChevronLeft, LucideRefreshCcw } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { slide } from 'svelte/transition';
|
||||
import OidcForm from '../oidc-client-form.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let client = $state(data);
|
||||
let showAllDetails = $state(false);
|
||||
|
||||
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) {
|
||||
let success = true;
|
||||
const dataPromise = oidcService.updateClient(client.id, updatedClient);
|
||||
@@ -74,23 +86,43 @@
|
||||
<Card.Title>{client.name}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="flex">
|
||||
<Label class="mb-0 w-44">Client ID</Label>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center">
|
||||
<Label class="mb-0 w-44">Client secret</Label>
|
||||
<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
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-2 flex">
|
||||
<Label class="mb-0 w-44">Client ID</Label>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
||||
</div>
|
||||
<div class="mb-2 mt-1 flex items-center">
|
||||
<Label class="w-44">Client secret</Label>
|
||||
<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}
|
||||
</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}
|
||||
</div>
|
||||
</Card.Content>
|
||||
|
||||
@@ -32,7 +32,10 @@
|
||||
.string()
|
||||
.min(2)
|
||||
.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(),
|
||||
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 |