mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-04 15:39:45 +00:00
feat: improve initial admin creation workflow
This commit is contained in:
@@ -33,6 +33,7 @@ func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
skipLdap := c.Query("skip-ldap") == "true"
|
skipLdap := c.Query("skip-ldap") == "true"
|
||||||
|
skipSeed := c.Query("skip-seed") == "true"
|
||||||
|
|
||||||
if err := tc.TestService.ResetDatabase(); err != nil {
|
if err := tc.TestService.ResetDatabase(); err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
@@ -44,9 +45,11 @@ func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tc.TestService.SeedDatabase(baseURL); err != nil {
|
if !skipSeed {
|
||||||
_ = c.Error(err)
|
if err := tc.TestService.SeedDatabase(baseURL); err != nil {
|
||||||
return
|
_ = c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tc.TestService.ResetAppConfig(c.Request.Context()); err != nil {
|
if err := tc.TestService.ResetAppConfig(c.Request.Context()); err != nil {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
|||||||
group.POST("/users/:id/one-time-access-token", authMiddleware.Add(), uc.createAdminOneTimeAccessTokenHandler)
|
group.POST("/users/:id/one-time-access-token", authMiddleware.Add(), uc.createAdminOneTimeAccessTokenHandler)
|
||||||
group.POST("/users/:id/one-time-access-email", authMiddleware.Add(), uc.RequestOneTimeAccessEmailAsAdminHandler)
|
group.POST("/users/:id/one-time-access-email", authMiddleware.Add(), uc.RequestOneTimeAccessEmailAsAdminHandler)
|
||||||
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
||||||
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
|
|
||||||
group.POST("/one-time-access-email", rateLimitMiddleware.Add(rate.Every(10*time.Minute), 3), uc.RequestOneTimeAccessEmailAsUnauthenticatedUserHandler)
|
group.POST("/one-time-access-email", rateLimitMiddleware.Add(rate.Every(10*time.Minute), 3), uc.RequestOneTimeAccessEmailAsUnauthenticatedUserHandler)
|
||||||
|
|
||||||
group.DELETE("/users/:id/profile-picture", authMiddleware.Add(), uc.resetUserProfilePictureHandler)
|
group.DELETE("/users/:id/profile-picture", authMiddleware.Add(), uc.resetUserProfilePictureHandler)
|
||||||
@@ -54,6 +53,7 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
|||||||
group.GET("/signup-tokens", authMiddleware.Add(), uc.listSignupTokensHandler)
|
group.GET("/signup-tokens", authMiddleware.Add(), uc.listSignupTokensHandler)
|
||||||
group.DELETE("/signup-tokens/:id", authMiddleware.Add(), uc.deleteSignupTokenHandler)
|
group.DELETE("/signup-tokens/:id", authMiddleware.Add(), uc.deleteSignupTokenHandler)
|
||||||
group.POST("/signup", rateLimitMiddleware.Add(rate.Every(1*time.Minute), 10), uc.signupHandler)
|
group.POST("/signup", rateLimitMiddleware.Add(rate.Every(1*time.Minute), 10), uc.signupHandler)
|
||||||
|
group.POST("/signup/setup", uc.signUpInitialAdmin)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,14 +446,23 @@ func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSetupAccessTokenHandler godoc
|
// signUpInitialAdmin godoc
|
||||||
// @Summary Setup initial admin
|
// @Summary Sign up initial admin user
|
||||||
// @Description Generate setup access token for initial admin user configuration
|
// @Description Sign up and generate setup access token for initial admin user
|
||||||
// @Tags Users
|
// @Tags Users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body dto.SignUpDto true "User information"
|
||||||
// @Success 200 {object} dto.UserDto
|
// @Success 200 {object} dto.UserDto
|
||||||
// @Router /api/one-time-access-token/setup [post]
|
// @Router /api/signup/setup [post]
|
||||||
func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
func (uc *UserController) signUpInitialAdmin(c *gin.Context) {
|
||||||
user, token, err := uc.userService.SetupInitialAdmin(c.Request.Context())
|
var input dto.SignUpDto
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
_ = c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, token, err := uc.userService.SignUpInitialAdmin(c.Request.Context(), input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) SetupInitialAdmin(ctx context.Context) (model.User, string, error) {
|
func (s *UserService) SignUpInitialAdmin(ctx context.Context, signUpData dto.SignUpDto) (model.User, string, error) {
|
||||||
tx := s.db.Begin()
|
tx := s.db.Begin()
|
||||||
defer func() {
|
defer func() {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -539,25 +539,19 @@ func (s *UserService) SetupInitialAdmin(ctx context.Context) (model.User, string
|
|||||||
if err := tx.WithContext(ctx).Model(&model.User{}).Count(&userCount).Error; err != nil {
|
if err := tx.WithContext(ctx).Model(&model.User{}).Count(&userCount).Error; err != nil {
|
||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
}
|
}
|
||||||
if userCount > 1 {
|
if userCount != 0 {
|
||||||
return model.User{}, "", &common.SetupAlreadyCompletedError{}
|
return model.User{}, "", &common.SetupAlreadyCompletedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
user := model.User{
|
userToCreate := dto.UserCreateDto{
|
||||||
FirstName: "Admin",
|
FirstName: signUpData.FirstName,
|
||||||
LastName: "Admin",
|
LastName: signUpData.LastName,
|
||||||
Username: "admin",
|
Username: signUpData.Username,
|
||||||
Email: "admin@admin.com",
|
Email: signUpData.Email,
|
||||||
IsAdmin: true,
|
IsAdmin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.WithContext(ctx).Model(&model.User{}).Preload("Credentials").FirstOrCreate(&user).Error; err != nil {
|
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
|
||||||
return model.User{}, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(user.Credentials) > 0 {
|
|
||||||
return model.User{}, "", &common.SetupAlreadyCompletedError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := s.jwtService.GenerateAccessToken(user)
|
token, err := s.jwtService.GenerateAccessToken(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -67,9 +67,7 @@
|
|||||||
"please_try_to_sign_in_again": "Please try to sign in again.",
|
"please_try_to_sign_in_again": "Please try to sign in again.",
|
||||||
"authenticate_with_passkey_to_access_account": "Authenticate yourself with your passkey to access your account.",
|
"authenticate_with_passkey_to_access_account": "Authenticate yourself with your passkey to access your account.",
|
||||||
"authenticate": "Authenticate",
|
"authenticate": "Authenticate",
|
||||||
"appname_setup": "{appName} Setup",
|
|
||||||
"please_try_again": "Please try again.",
|
"please_try_again": "Please try again.",
|
||||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "You're about to sign in to the initial admin account. Anyone with this link can access the account until a passkey is added. Please set up a passkey as soon as possible to prevent unauthorized access.",
|
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"alternative_sign_in": "Alternative Sign In",
|
"alternative_sign_in": "Alternative Sign In",
|
||||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you don't have access to your passkey, you can sign in using one of the following methods.",
|
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you don't have access to your passkey, you can sign in using one of the following methods.",
|
||||||
@@ -391,6 +389,7 @@
|
|||||||
"go_to_login": "Go to login",
|
"go_to_login": "Go to login",
|
||||||
"signup_to_appname": "Sign Up to {appName}",
|
"signup_to_appname": "Sign Up to {appName}",
|
||||||
"create_your_account_to_get_started": "Create your account to get started.",
|
"create_your_account_to_get_started": "Create your account to get started.",
|
||||||
|
"initial_account_creation_description": "Please create your account to get started. You will be able to set up a passkey later.",
|
||||||
"setup_your_passkey": "Set up your passkey",
|
"setup_your_passkey": "Set up your passkey",
|
||||||
"create_a_passkey_to_securely_access_your_account": "Create a passkey to securely access your account. This will be your primary way to sign in.",
|
"create_a_passkey_to_securely_access_your_account": "Create a passkey to securely access your account. This will be your primary way to sign in.",
|
||||||
"skip_for_now": "Skip for now",
|
"skip_for_now": "Skip for now",
|
||||||
@@ -417,5 +416,7 @@
|
|||||||
"signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.",
|
"signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.",
|
||||||
"signup_open": "Open Signup",
|
"signup_open": "Open Signup",
|
||||||
"signup_open_description": "Anyone can create a new account without restrictions.",
|
"signup_open_description": "Anyone can create a new account without restrictions.",
|
||||||
"of": "of"
|
"of": "of",
|
||||||
|
"skip_passkey_setup": "Skip Passkey Setup",
|
||||||
|
"skip_passkey_setup_description": "It's highly recommended to set up a passkey because without one, you will be locked out of your account as soon as the session expires."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ export default class UserService extends APIService {
|
|||||||
return res.data as User;
|
return res.data as User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async signupInitialUser(data: UserSignUp) {
|
||||||
|
const res = await this.api.post(`/signup/setup`, data);
|
||||||
|
return res.data as User;
|
||||||
|
}
|
||||||
|
|
||||||
async listSignupTokens(options?: SearchPaginationSortRequest) {
|
async listSignupTokens(options?: SearchPaginationSortRequest) {
|
||||||
const res = await this.api.get('/signup-tokens', {
|
const res = await this.api.get('/signup-tokens', {
|
||||||
params: options
|
params: options
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function getAuthRedirectPath(path: string, user: User | null) {
|
|||||||
path == '/lc' ||
|
path == '/lc' ||
|
||||||
path.startsWith('/lc/') ||
|
path.startsWith('/lc/') ||
|
||||||
path == '/signup' ||
|
path == '/signup' ||
|
||||||
path.startsWith('/signup/') ||
|
path == '/signup/setup' ||
|
||||||
path.startsWith('/st/');
|
path.startsWith('/st/');
|
||||||
const isPublicPath = ['/authorize', '/device', '/health', '/healthz'].includes(path);
|
const isPublicPath = ['/authorize', '/device', '/health', '/healthz'].includes(path);
|
||||||
const isAdminPath = path == '/settings/admin' || path.startsWith('/settings/admin/');
|
const isAdminPath = path == '/settings/admin' || path.startsWith('/settings/admin/');
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
|
||||||
import { Button } from '$lib/components/ui/button';
|
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
import UserService from '$lib/services/user-service';
|
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store.js';
|
|
||||||
import userStore from '$lib/stores/user-store.js';
|
|
||||||
import { getAxiosErrorMessage } from '$lib/utils/error-util';
|
|
||||||
import LoginLogoErrorSuccessIndicator from '../components/login-logo-error-success-indicator.svelte';
|
|
||||||
|
|
||||||
let isLoading = $state(false);
|
|
||||||
let error: string | undefined = $state();
|
|
||||||
|
|
||||||
const userService = new UserService();
|
|
||||||
|
|
||||||
async function authenticate() {
|
|
||||||
isLoading = true;
|
|
||||||
try {
|
|
||||||
const user = await userService.exchangeOneTimeAccessToken('setup');
|
|
||||||
userStore.setUser(user);
|
|
||||||
|
|
||||||
goto('/settings');
|
|
||||||
} catch (e) {
|
|
||||||
error = getAxiosErrorMessage(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SignInWrapper animate={!$appConfigStore.disableAnimations}>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
|
||||||
</div>
|
|
||||||
<h1 class="font-playfair mt-5 text-4xl font-bold">
|
|
||||||
{m.appname_setup({ appName: $appConfigStore.appName })}
|
|
||||||
</h1>
|
|
||||||
{#if error}
|
|
||||||
<p class="text-muted-foreground mt-2">
|
|
||||||
{error}. {m.please_try_again()}
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<p class="text-muted-foreground mt-2">
|
|
||||||
{m.you_are_about_to_sign_in_to_the_initial_admin_account()}
|
|
||||||
</p>
|
|
||||||
<Button class="mt-5" {isLoading} onclick={authenticate}>{m.continue()}</Button>
|
|
||||||
{/if}
|
|
||||||
</SignInWrapper>
|
|
||||||
5
frontend/src/routes/setup/+page.ts
Normal file
5
frontend/src/routes/setup/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
// Alias for /signup/setup
|
||||||
|
export const load: PageLoad = async () => redirect(307, '/signup/setup');
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
@@ -44,6 +45,20 @@
|
|||||||
goto('/settings/account');
|
goto('/settings/account');
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function skipForNow() {
|
||||||
|
openConfirmDialog({
|
||||||
|
title: m.skip_passkey_setup(),
|
||||||
|
message: m.skip_passkey_setup_description(),
|
||||||
|
confirm: {
|
||||||
|
label: m.skip_for_now(),
|
||||||
|
destructive: true,
|
||||||
|
action: () => {
|
||||||
|
goto('/settings/account');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -66,12 +81,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-10 flex w-full justify-between gap-2">
|
<div class="mt-10 flex w-full justify-between gap-2">
|
||||||
<Button
|
<Button variant="secondary" onclick={skipForNow} disabled={isLoading} class="flex-1">
|
||||||
variant="secondary"
|
|
||||||
onclick={() => goto('/settings/account')}
|
|
||||||
disabled={isLoading}
|
|
||||||
class="flex-1"
|
|
||||||
>
|
|
||||||
{m.skip_for_now()}
|
{m.skip_for_now()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onclick={createPasskeyAndContinue} {isLoading} class="flex-1">
|
<Button onclick={createPasskeyAndContinue} {isLoading} class="flex-1">
|
||||||
|
|||||||
70
frontend/src/routes/signup/setup/+page.svelte
Normal file
70
frontend/src/routes/signup/setup/+page.svelte
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||||
|
import SignupForm from '$lib/components/signup/signup-form.svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import UserService from '$lib/services/user-service';
|
||||||
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
|
import userStore from '$lib/stores/user-store';
|
||||||
|
import type { UserSignUp } from '$lib/types/user.type';
|
||||||
|
import { getAxiosErrorMessage } from '$lib/utils/error-util';
|
||||||
|
import { tryCatch } from '$lib/utils/try-catch-util';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import LoginLogoErrorSuccessIndicator from '../../login/components/login-logo-error-success-indicator.svelte';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
const userService = new UserService();
|
||||||
|
|
||||||
|
let isLoading = $state(false);
|
||||||
|
let error: string | undefined = $state();
|
||||||
|
|
||||||
|
async function handleSignup(userData: UserSignUp) {
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
const result = await tryCatch(userService.signupInitialUser(userData));
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
error = getAxiosErrorMessage(result.error);
|
||||||
|
isLoading = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
userStore.setUser(result.data);
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
|
goto('/signup/add-passkey');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{m.signup()}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<SignInWrapper animate={!$appConfigStore.disableAnimations}>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||||
|
{m.signup_to_appname({ appName: $appConfigStore.appName })}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{#if !error}
|
||||||
|
<p class="text-muted-foreground mt-2" in:fade>
|
||||||
|
{m.initial_account_creation_description()}
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-muted-foreground mt-2" in:fade>
|
||||||
|
{error}.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<SignupForm callback={handleSignup} {isLoading} />
|
||||||
|
<div class="mt-10 flex w-full justify-end">
|
||||||
|
<Button type="submit" form="sign-up-form" onclick={() => (error = undefined)}
|
||||||
|
>{m.signup()}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</SignInWrapper>
|
||||||
@@ -4,7 +4,7 @@ import authUtil from '../utils/auth.util';
|
|||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
import passkeyUtil from '../utils/passkey.util';
|
import passkeyUtil from '../utils/passkey.util';
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(() => cleanupBackend());
|
||||||
|
|
||||||
test('Update account details', async ({ page }) => {
|
test('Update account details', async ({ page }) => {
|
||||||
await page.goto('/settings/account');
|
await page.goto('/settings/account');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(() => cleanupBackend());
|
||||||
|
|
||||||
test('Update general configuration', async ({ page }) => {
|
test('Update general configuration', async ({ page }) => {
|
||||||
await page.goto('/settings/admin/application-configuration');
|
await page.goto('/settings/admin/application-configuration');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(() => cleanupBackend());
|
||||||
|
|
||||||
test.describe('LDAP Integration', () => {
|
test.describe('LDAP Integration', () => {
|
||||||
test.skip(
|
test.skip(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import test, { expect } from '@playwright/test';
|
|||||||
import { oidcClients } from '../data';
|
import { oidcClients } from '../data';
|
||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(() => cleanupBackend());
|
||||||
|
|
||||||
test('Create OIDC client', async ({ page }) => {
|
test('Create OIDC client', async ({ page }) => {
|
||||||
await page.goto('/settings/admin/oidc-clients');
|
await page.goto('/settings/admin/oidc-clients');
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { generateIdToken, generateOauthAccessToken } from '../utils/jwt.util';
|
|||||||
import * as oidcUtil from '../utils/oidc.util';
|
import * as oidcUtil from '../utils/oidc.util';
|
||||||
import passkeyUtil from '../utils/passkey.util';
|
import passkeyUtil from '../utils/passkey.util';
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(() => cleanupBackend());
|
||||||
|
|
||||||
test('Authorize existing client', async ({ page }) => {
|
test('Authorize existing client', async ({ page }) => {
|
||||||
const oidcClient = oidcClients.nextcloud;
|
const oidcClient = oidcClients.nextcloud;
|
||||||
@@ -189,19 +189,6 @@ test('Refresh token fails when used for the wrong client', async ({ request }) =
|
|||||||
})
|
})
|
||||||
.then((r) => r.text());
|
.then((r) => r.text());
|
||||||
|
|
||||||
// Perform the exchange
|
|
||||||
const refreshResponse = await request.post('/api/oidc/token', {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
client_id: clientId,
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
client_secret: clientSecret
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(refreshResponse.status()).toBe(400);
|
expect(refreshResponse.status()).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import test, { expect } from '@playwright/test';
|
|||||||
import { oneTimeAccessTokens } from '../data';
|
import { oneTimeAccessTokens } from '../data';
|
||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(() => cleanupBackend());
|
||||||
|
|
||||||
// Disable authentication for these tests
|
// Disable authentication for these tests
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import test, { expect } from '@playwright/test';
|
|||||||
import { userGroups, users } from '../data';
|
import { userGroups, users } from '../data';
|
||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(() => cleanupBackend());
|
||||||
|
|
||||||
test('Create user group', async ({ page }) => {
|
test('Create user group', async ({ page }) => {
|
||||||
await page.goto('/settings/admin/user-groups');
|
await page.goto('/settings/admin/user-groups');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import test, { expect } from '@playwright/test';
|
|||||||
import { userGroups, users } from '../data';
|
import { userGroups, users } from '../data';
|
||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(() => cleanupBackend());
|
||||||
|
|
||||||
test('Create user', async ({ page }) => {
|
test('Create user', async ({ page }) => {
|
||||||
const user = users.steve;
|
const user = users.steve;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
|
import { signupTokens, users } from 'data';
|
||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
import passkeyUtil from '../utils/passkey.util';
|
import passkeyUtil from '../utils/passkey.util';
|
||||||
import { users, signupTokens } from 'data';
|
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(() => cleanupBackend());
|
||||||
|
|
||||||
test.describe('User Signup', () => {
|
test.describe('User Signup', () => {
|
||||||
async function setSignupMode(page: any, mode: 'Disabled' | 'Signup with token' | 'Open Signup') {
|
async function setSignupMode(page: any, mode: 'Disabled' | 'Signup with token' | 'Open Signup') {
|
||||||
@@ -177,3 +177,36 @@ test.describe('User Signup', () => {
|
|||||||
await expect(page.getByText('Token is invalid or expired.')).toBeVisible();
|
await expect(page.getByText('Token is invalid or expired.')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Initial User Signup', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.context().clearCookies();
|
||||||
|
});
|
||||||
|
test('Initial Signup - success flow', async ({ page }) => {
|
||||||
|
await cleanupBackend(true);
|
||||||
|
await page.goto('/setup');
|
||||||
|
|
||||||
|
await page.getByLabel('First name').fill('Jane');
|
||||||
|
await page.getByLabel('Last name').fill('Smith');
|
||||||
|
await page.getByLabel('Username').fill('janesmith');
|
||||||
|
await page.getByLabel('Email').fill('jane.smith@test.com');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Sign Up' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/signup/add-passkey');
|
||||||
|
await expect(page.getByText('Set up your passkey')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Initial Signup - setup already completed', async ({ page }) => {
|
||||||
|
await page.goto('/setup');
|
||||||
|
|
||||||
|
await page.getByLabel('First name').fill('Test');
|
||||||
|
await page.getByLabel('Last name').fill('User');
|
||||||
|
await page.getByLabel('Username').fill('testuser123');
|
||||||
|
await page.getByLabel('Email').fill(users.tim.email);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Sign Up' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Setup already completed')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import playwrightConfig from '../playwright.config';
|
import playwrightConfig from '../playwright.config';
|
||||||
|
|
||||||
export async function cleanupBackend() {
|
export async function cleanupBackend(skipSeed = false) {
|
||||||
const url = new URL('/api/test/reset', playwrightConfig.use!.baseURL);
|
const url = new URL('/api/test/reset', playwrightConfig.use!.baseURL);
|
||||||
|
|
||||||
if (process.env.SKIP_LDAP_TESTS === 'true') {
|
if (process.env.SKIP_LDAP_TESTS === 'true' || skipSeed) {
|
||||||
url.searchParams.append('skip-ldap', 'true');
|
url.searchParams.append('skip-ldap', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skipSeed) {
|
||||||
|
url.searchParams.append('skip-seed', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user