mirror of
https://github.com/nikdoof/pocket-id.git
synced 2025-12-14 07:12:19 +00:00
feat: add support for multiple callback urls
This commit is contained in:
@@ -6,7 +6,6 @@ var (
|
||||
ErrUsernameTaken = errors.New("username is already taken")
|
||||
ErrEmailTaken = errors.New("email is already taken")
|
||||
ErrSetupAlreadyCompleted = errors.New("setup already completed")
|
||||
ErrInvalidBody = errors.New("invalid request body")
|
||||
ErrTokenInvalidOrExpired = errors.New("token is invalid or expired")
|
||||
ErrOidcMissingAuthorization = errors.New("missing authorization")
|
||||
ErrOidcGrantTypeNotSupported = errors.New("grant type not supported")
|
||||
|
||||
@@ -40,39 +40,55 @@ type OidcController struct {
|
||||
}
|
||||
|
||||
func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
||||
var input dto.AuthorizeOidcClientDto
|
||||
var input dto.AuthorizeOidcClientRequestDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
utils.ControllerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
code, err := oc.oidcService.Authorize(input, c.GetString("userID"))
|
||||
code, callbackURL, err := oc.oidcService.Authorize(input, c.GetString("userID"))
|
||||
if err != nil {
|
||||
if errors.Is(err, common.ErrOidcMissingAuthorization) {
|
||||
utils.CustomControllerError(c, http.StatusForbidden, err.Error())
|
||||
} else if errors.Is(err, common.ErrOidcInvalidCallbackURL) {
|
||||
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
utils.ControllerError(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": code})
|
||||
response := dto.AuthorizeOidcClientResponseDto{
|
||||
Code: code,
|
||||
CallbackURL: callbackURL,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (oc *OidcController) authorizeNewClientHandler(c *gin.Context) {
|
||||
var input dto.AuthorizeOidcClientDto
|
||||
var input dto.AuthorizeOidcClientRequestDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
utils.ControllerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
code, err := oc.oidcService.AuthorizeNewClient(input, c.GetString("userID"))
|
||||
code, callbackURL, err := oc.oidcService.AuthorizeNewClient(input, c.GetString("userID"))
|
||||
if err != nil {
|
||||
if errors.Is(err, common.ErrOidcInvalidCallbackURL) {
|
||||
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
utils.ControllerError(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": code})
|
||||
response := dto.AuthorizeOidcClientResponseDto{
|
||||
Code: code,
|
||||
CallbackURL: callbackURL,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (oc *OidcController) createIDTokenHandler(c *gin.Context) {
|
||||
|
||||
@@ -17,12 +17,18 @@ type OidcClientCreateDto struct {
|
||||
CallbackURLs []string `json:"callbackURLs" binding:"required,urlList"`
|
||||
}
|
||||
|
||||
type AuthorizeOidcClientDto struct {
|
||||
type AuthorizeOidcClientRequestDto struct {
|
||||
ClientID string `json:"clientID" binding:"required"`
|
||||
Scope string `json:"scope" binding:"required"`
|
||||
CallbackURL string `json:"callbackURL"`
|
||||
Nonce string `json:"nonce"`
|
||||
}
|
||||
|
||||
type AuthorizeOidcClientResponseDto struct {
|
||||
Code string `json:"code"`
|
||||
CallbackURL string `json:"callbackURL"`
|
||||
}
|
||||
|
||||
type OidcIdTokenDto struct {
|
||||
GrantType string `form:"grant_type" binding:"required"`
|
||||
Code string `form:"code" binding:"required"`
|
||||
|
||||
@@ -52,17 +52,14 @@ func (c *OidcClient) AfterFind(_ *gorm.DB) (err error) {
|
||||
|
||||
type CallbackURLs []string
|
||||
|
||||
func (s *CallbackURLs) Scan(value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, s)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), s)
|
||||
default:
|
||||
return errors.New("type assertion to []byte or string failed")
|
||||
func (cu *CallbackURLs) Scan(value interface{}) error {
|
||||
if v, ok := value.([]byte); ok {
|
||||
return json.Unmarshal(v, cu)
|
||||
} else {
|
||||
return errors.New("type assertion to []byte failed")
|
||||
}
|
||||
}
|
||||
|
||||
func (atl CallbackURLs) Value() (driver.Value, error) {
|
||||
return json.Marshal(atl)
|
||||
func (cu CallbackURLs) Value() (driver.Value, error) {
|
||||
return json.Marshal(cu)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -27,33 +28,50 @@ func NewOidcService(db *gorm.DB, jwtService *JwtService) *OidcService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OidcService) Authorize(req dto.AuthorizeOidcClientDto, userID string) (string, error) {
|
||||
func (s *OidcService) Authorize(input dto.AuthorizeOidcClientRequestDto, userID string) (string, string, error) {
|
||||
var userAuthorizedOIDCClient model.UserAuthorizedOidcClient
|
||||
s.db.First(&userAuthorizedOIDCClient, "client_id = ? AND user_id = ?", req.ClientID, userID)
|
||||
s.db.Preload("Client").First(&userAuthorizedOIDCClient, "client_id = ? AND user_id = ?", input.ClientID, userID)
|
||||
|
||||
if userAuthorizedOIDCClient.Scope != req.Scope {
|
||||
return "", common.ErrOidcMissingAuthorization
|
||||
if userAuthorizedOIDCClient.Scope != input.Scope {
|
||||
return "", "", common.ErrOidcMissingAuthorization
|
||||
}
|
||||
|
||||
return s.createAuthorizationCode(req.ClientID, userID, req.Scope, req.Nonce)
|
||||
callbackURL, err := getCallbackURL(userAuthorizedOIDCClient.Client, input.CallbackURL)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
code, err := s.createAuthorizationCode(input.ClientID, userID, input.Scope, input.Nonce)
|
||||
return code, callbackURL, err
|
||||
}
|
||||
|
||||
func (s *OidcService) AuthorizeNewClient(input dto.AuthorizeOidcClientRequestDto, userID string) (string, string, error) {
|
||||
var client model.OidcClient
|
||||
if err := s.db.First(&client, "id = ?", input.ClientID).Error; err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
callbackURL, err := getCallbackURL(client, input.CallbackURL)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
func (s *OidcService) AuthorizeNewClient(req dto.AuthorizeOidcClientDto, userID string) (string, error) {
|
||||
userAuthorizedClient := model.UserAuthorizedOidcClient{
|
||||
UserID: userID,
|
||||
ClientID: req.ClientID,
|
||||
Scope: req.Scope,
|
||||
ClientID: input.ClientID,
|
||||
Scope: input.Scope,
|
||||
}
|
||||
|
||||
if err := s.db.Create(&userAuthorizedClient).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
err = s.db.Model(&userAuthorizedClient).Update("scope", req.Scope).Error
|
||||
err = s.db.Model(&userAuthorizedClient).Update("scope", input.Scope).Error
|
||||
} else {
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
return s.createAuthorizationCode(req.ClientID, userID, req.Scope, req.Nonce)
|
||||
code, err := s.createAuthorizationCode(input.ClientID, userID, input.Scope, input.Nonce)
|
||||
return code, callbackURL, err
|
||||
}
|
||||
|
||||
func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret string) (string, string, error) {
|
||||
@@ -321,3 +339,14 @@ func (s *OidcService) createAuthorizationCode(clientID string, userID string, sc
|
||||
|
||||
return randomString, nil
|
||||
}
|
||||
|
||||
func getCallbackURL(client model.OidcClient, inputCallbackURL string) (callbackURL string, err error) {
|
||||
if inputCallbackURL == "" {
|
||||
return client.CallbackURLs[0], nil
|
||||
}
|
||||
if slices.Contains(client.CallbackURLs, inputCallbackURL) {
|
||||
return inputCallbackURL, nil
|
||||
}
|
||||
|
||||
return "", common.ErrOidcInvalidCallbackURL
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ CREATE TABLE webauthn_credentials
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
public_key BLOB NOT NULL,
|
||||
attestation_type TEXT NOT NULL,
|
||||
transport TEXT NOT NULL,
|
||||
transport BLOB NOT NULL,
|
||||
user_id TEXT REFERENCES users
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
create table oidc_clients
|
||||
(
|
||||
id TEXT not null
|
||||
primary key,
|
||||
created_at DATETIME,
|
||||
name TEXT,
|
||||
secret TEXT,
|
||||
callback_url TEXT,
|
||||
image_type TEXT,
|
||||
created_by_id TEXT
|
||||
references users
|
||||
);
|
||||
|
||||
insert into oidc_clients(id, created_at, name, secret, callback_url, image_type, created_by_id)
|
||||
select
|
||||
id,
|
||||
created_at,
|
||||
name,
|
||||
secret,
|
||||
json_extract(callback_urls, '$[0]'),
|
||||
image_type,
|
||||
created_by_id
|
||||
from oidc_clients_dg_tmp;
|
||||
|
||||
drop table oidc_clients_dg_tmp;
|
||||
@@ -0,0 +1,27 @@
|
||||
create table oidc_clients_dg_tmp
|
||||
(
|
||||
id TEXT not null
|
||||
primary key,
|
||||
created_at DATETIME,
|
||||
name TEXT,
|
||||
secret TEXT,
|
||||
callback_urls BLOB,
|
||||
image_type TEXT,
|
||||
created_by_id TEXT
|
||||
references users
|
||||
);
|
||||
|
||||
insert into oidc_clients_dg_tmp(id, created_at, name, secret, callback_urls, image_type, created_by_id)
|
||||
select id,
|
||||
created_at,
|
||||
name,
|
||||
secret,
|
||||
CAST(json_group_array(json_quote(callback_url)) AS BLOB),
|
||||
image_type,
|
||||
created_by_id
|
||||
from oidc_clients;
|
||||
|
||||
drop table oidc_clients;
|
||||
|
||||
alter table oidc_clients_dg_tmp
|
||||
rename to oidc_clients;
|
||||
@@ -2,15 +2,17 @@
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import type { FormInput } from '$lib/utils/form-util';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { Input } from './ui/input';
|
||||
|
||||
let {
|
||||
input = $bindable(),
|
||||
label,
|
||||
description,
|
||||
children
|
||||
}: {
|
||||
input: FormInput<string | boolean | number>;
|
||||
children,
|
||||
...restProps
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
input?: FormInput<string | boolean | number>;
|
||||
label: string;
|
||||
description?: string;
|
||||
children?: Snippet;
|
||||
@@ -19,19 +21,19 @@
|
||||
const id = label.toLowerCase().replace(/ /g, '-');
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div {...restProps}>
|
||||
<Label class="mb-0" for={id}>{label}</Label>
|
||||
{#if description}
|
||||
<p class="text-muted-foreground text-xs mt-1">{description}</p>
|
||||
<p class="text-muted-foreground mt-1 text-xs">{description}</p>
|
||||
{/if}
|
||||
<div class="mt-2">
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
{:else if input}
|
||||
<Input {id} bind:value={input.value} />
|
||||
{/if}
|
||||
{#if input.error}
|
||||
<p class="text-sm text-red-500">{input.error}</p>
|
||||
{#if input?.error}
|
||||
<p class="mt-1 text-sm text-red-500">{input.error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import type { OidcClient, OidcClientCreate } from '$lib/types/oidc.type';
|
||||
import type { AuthorizeResponse, OidcClient, OidcClientCreate } from '$lib/types/oidc.type';
|
||||
import type { Paginated, PaginationRequest } from '$lib/types/pagination.type';
|
||||
import APIService from './api-service';
|
||||
|
||||
class OidcService extends APIService {
|
||||
async authorize(clientId: string, scope: string, nonce?: string) {
|
||||
async authorize(clientId: string, scope: string, callbackURL : string, nonce?: string) {
|
||||
const res = await this.api.post('/oidc/authorize', {
|
||||
scope,
|
||||
nonce,
|
||||
callbackURL,
|
||||
clientId
|
||||
});
|
||||
|
||||
return res.data.code as string;
|
||||
return res.data as AuthorizeResponse;
|
||||
}
|
||||
|
||||
async authorizeNewClient(clientId: string, scope: string, nonce?: string) {
|
||||
async authorizeNewClient(clientId: string, scope: string, callbackURL: string, nonce?: string) {
|
||||
const res = await this.api.post('/oidc/authorize/new-client', {
|
||||
scope,
|
||||
nonce,
|
||||
callbackURL,
|
||||
clientId
|
||||
});
|
||||
|
||||
return res.data.code as string;
|
||||
return res.data as AuthorizeResponse;
|
||||
}
|
||||
|
||||
async listClients(search?: string, pagination?: PaginationRequest) {
|
||||
|
||||
@@ -2,7 +2,7 @@ export type OidcClient = {
|
||||
id: string;
|
||||
name: string;
|
||||
logoURL: string;
|
||||
callbackURL: string;
|
||||
callbackURLs: [string, ...string[]];
|
||||
hasLogo: boolean;
|
||||
};
|
||||
|
||||
@@ -11,3 +11,8 @@ export type OidcClientCreate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
|
||||
export type OidcClientCreateWithLogo = OidcClientCreate & {
|
||||
logo: File | null;
|
||||
};
|
||||
|
||||
export type AuthorizeResponse = {
|
||||
code: string;
|
||||
callbackURL: string;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||
scope: url.searchParams.get('scope')!,
|
||||
nonce: url.searchParams.get('nonce') || undefined,
|
||||
state: url.searchParams.get('state')!,
|
||||
callbackURL: url.searchParams.get('redirect_uri')!,
|
||||
client
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
let authorizationRequired = false;
|
||||
|
||||
export let data: PageData;
|
||||
let { scope, nonce, client, state } = data;
|
||||
let { scope, nonce, client, state, callbackURL } = data;
|
||||
|
||||
async function authorize() {
|
||||
isLoading = true;
|
||||
@@ -36,8 +36,10 @@
|
||||
await webauthnService.finishLogin(authResponse);
|
||||
}
|
||||
|
||||
await oidService.authorize(client!.id, scope, nonce).then(async (code) => {
|
||||
onSuccess(code);
|
||||
await oidService
|
||||
.authorize(client!.id, scope, callbackURL, nonce)
|
||||
.then(async ({ code, callbackURL }) => {
|
||||
onSuccess(code, callbackURL);
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError && e.response?.status === 403) {
|
||||
@@ -52,8 +54,10 @@
|
||||
async function authorizeNewClient() {
|
||||
isLoading = true;
|
||||
try {
|
||||
await oidService.authorizeNewClient(client!.id, scope, nonce).then(async (code) => {
|
||||
onSuccess(code);
|
||||
await oidService
|
||||
.authorizeNewClient(client!.id, scope, callbackURL, nonce)
|
||||
.then(async ({ code, callbackURL }) => {
|
||||
onSuccess(code, callbackURL);
|
||||
});
|
||||
} catch (e) {
|
||||
errorMessage = getWebauthnErrorMessage(e);
|
||||
@@ -61,10 +65,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function onSuccess(code: string) {
|
||||
function onSuccess(code: string, callbackURL: string) {
|
||||
success = true;
|
||||
setTimeout(() => {
|
||||
window.location.href = `${client!.callbackURL}?code=${code}&state=${state}`;
|
||||
window.location.href = `${callbackURL}?code=${code}&state=${state}`;
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="flex flex-col gap-5">
|
||||
<FormInput label="Application Name" bind:input={$inputs.appName} />
|
||||
|
||||
<FormInput
|
||||
label="Session Duration"
|
||||
description="The duration of a session in minutes before the user has to sign in again."
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import FormInput from '$lib/components/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { LucideMinus, LucidePlus } from 'lucide-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
callbackURLs = $bindable(),
|
||||
error = $bindable(null),
|
||||
...restProps
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
callbackURLs: string[];
|
||||
error?: string | null;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
|
||||
const limit = 5;
|
||||
</script>
|
||||
|
||||
<div {...restProps}>
|
||||
<FormInput label="Callback URLs">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
{#each callbackURLs as _, i}
|
||||
<div class="flex gap-x-2">
|
||||
<Input data-testid={`callback-url-${i + 1}`} bind:value={callbackURLs[i]} />
|
||||
{#if callbackURLs.length > 1}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
on:click={() => callbackURLs = callbackURLs.filter((_, index) => index !== i)}
|
||||
>
|
||||
<LucideMinus class="h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</FormInput>
|
||||
{#if error}
|
||||
<p class="mt-1 text-sm text-red-500">{error}</p>
|
||||
{/if}
|
||||
{#if callbackURLs.length < limit}
|
||||
<Button
|
||||
class="mt-2"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
on:click={() => callbackURLs = [...callbackURLs, '']}
|
||||
>
|
||||
<LucidePlus class="mr-1 h-4 w-4" />
|
||||
Add another
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -10,6 +10,7 @@
|
||||
} from '$lib/types/oidc.type';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { z } from 'zod';
|
||||
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
|
||||
|
||||
let {
|
||||
callback,
|
||||
@@ -27,12 +28,12 @@
|
||||
|
||||
const client: OidcClientCreate = {
|
||||
name: existingClient?.name || '',
|
||||
callbackURL: existingClient?.callbackURL || ''
|
||||
callbackURLs: existingClient?.callbackURLs || [""]
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
callbackURL: z.string().url()
|
||||
callbackURLs: z.array(z.string().url()).nonempty()
|
||||
});
|
||||
|
||||
type FormSchema = typeof formSchema;
|
||||
@@ -70,15 +71,24 @@
|
||||
</script>
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="mt-3 grid grid-cols-2 gap-3">
|
||||
<FormInput label="Name" bind:input={$inputs.name} />
|
||||
<FormInput label="Callback URL" bind:input={$inputs.callbackURL} />
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<FormInput label="Name" class="w-full" bind:input={$inputs.name} />
|
||||
<OidcCallbackUrlInput
|
||||
class="w-full"
|
||||
bind:callbackURLs={$inputs.callbackURLs.value}
|
||||
bind:error={$inputs.callbackURLs.error}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<Label for="logo">Logo</Label>
|
||||
<div class="mt-2 flex items-end gap-3">
|
||||
{#if logoDataURL}
|
||||
<div class="h-32 w-32 rounded-2xl bg-muted p-3">
|
||||
<img class="m-auto max-h-full max-w-full object-contain" src={logoDataURL} alt={`${$inputs.name.value} logo`} />
|
||||
<div class="bg-muted h-32 w-32 rounded-2xl p-3">
|
||||
<img
|
||||
class="m-auto max-h-full max-w-full object-contain"
|
||||
src={logoDataURL}
|
||||
alt={`${$inputs.name.value} logo`}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -98,7 +108,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full"></div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
|
||||
@@ -26,9 +26,13 @@
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
firstName: z.string().min(2).max(50),
|
||||
lastName: z.string().min(2).max(50),
|
||||
username: z.string().min(2).max(50),
|
||||
firstName: z.string().min(2).max(30),
|
||||
lastName: z.string().min(2).max(30),
|
||||
username: z
|
||||
.string()
|
||||
.min(2)
|
||||
.max(30)
|
||||
.regex(/^[a-z0-9_]+$/, 'Only lowercase letters, numbers, and underscores are allowed'),
|
||||
email: z.string().email(),
|
||||
isAdmin: z.boolean()
|
||||
});
|
||||
@@ -66,10 +70,10 @@
|
||||
<div class="items-top mt-5 flex space-x-2">
|
||||
<Checkbox id="admin-privileges" bind:checked={$inputs.isAdmin.value} />
|
||||
<div class="grid gap-1.5 leading-none">
|
||||
<Label for="admin-privileges" class="text-sm font-medium leading-none mb-0">
|
||||
<Label for="admin-privileges" class="mb-0 text-sm font-medium leading-none">
|
||||
Admin Privileges
|
||||
</Label>
|
||||
<p class="text-[0.8rem] text-muted-foreground">Admins have full access to the admin panel.</p>
|
||||
<p class="text-muted-foreground text-[0.8rem]">Admins have full access to the admin panel.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
|
||||
@@ -23,17 +23,18 @@ export const users = {
|
||||
|
||||
export const oidcClients = {
|
||||
nextcloud: {
|
||||
id: "3654a746-35d4-4321-ac61-0bdcff2b4055",
|
||||
id: '3654a746-35d4-4321-ac61-0bdcff2b4055',
|
||||
name: 'Nextcloud',
|
||||
callbackUrl: 'http://nextcloud/auth/callback'
|
||||
},
|
||||
immich: {
|
||||
id: "606c7782-f2b1-49e5-8ea9-26eb1b06d018",
|
||||
id: '606c7782-f2b1-49e5-8ea9-26eb1b06d018',
|
||||
name: 'Immich',
|
||||
callbackUrl: 'http://immich/auth/callback'
|
||||
},
|
||||
pingvinShare: {
|
||||
name: 'Pingvin Share',
|
||||
callbackUrl: 'http://pingvin.share/auth/callback'
|
||||
callbackUrl: 'http://pingvin.share/auth/callback',
|
||||
secondCallbackUrl: 'http://pingvin.share/auth/callback2'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,11 @@ test('Create OIDC client', async ({ page }) => {
|
||||
|
||||
await page.getByRole('button', { name: 'Add OIDC Client' }).click();
|
||||
await page.getByLabel('Name').fill(oidcClient.name);
|
||||
await page.getByLabel('Callback URL').fill(oidcClient.callbackUrl);
|
||||
|
||||
await page.getByTestId('callback-url-1').fill(oidcClient.callbackUrl);
|
||||
await page.getByRole('button', { name: 'Add another' }).click();
|
||||
await page.getByTestId('callback-url-2').fill(oidcClient.secondCallbackUrl!);
|
||||
|
||||
await page.getByLabel('logo').setInputFiles('tests/assets/pingvin-share-logo.png');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
@@ -20,7 +24,8 @@ test('Create OIDC client', async ({ page }) => {
|
||||
expect(clientId?.length).toBe(36);
|
||||
expect((await page.getByTestId('client-secret').textContent())?.length).toBe(32);
|
||||
await expect(page.getByLabel('Name')).toHaveValue(oidcClient.name);
|
||||
await expect(page.getByLabel('Callback URL')).toHaveValue(oidcClient.callbackUrl);
|
||||
await expect(page.getByTestId('callback-url-1')).toHaveValue(oidcClient.callbackUrl);
|
||||
await expect(page.getByTestId('callback-url-2')).toHaveValue(oidcClient.secondCallbackUrl!);
|
||||
await expect(page.getByRole('img', { name: `${oidcClient.name} logo` })).toBeVisible();
|
||||
await page.request
|
||||
.get(`/api/oidc/clients/${clientId}/logo`)
|
||||
@@ -32,7 +37,7 @@ test('Edit OIDC client', async ({ page }) => {
|
||||
await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`);
|
||||
|
||||
await page.getByLabel('Name').fill('Nextcloud updated');
|
||||
await page.getByLabel('Callback URL').fill('http://nextcloud-updated/auth/callback');
|
||||
await page.getByTestId('callback-url-1').fill('http://nextcloud-updated/auth/callback');
|
||||
await page.getByLabel('logo').setInputFiles('tests/assets/nextcloud-logo.png');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user