Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ALS-8234] Enforce non-duplicate users #375

Open
wants to merge 1 commit into
base: release
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 68 additions & 41 deletions src/lib/components/admin/user/UserForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,77 +3,98 @@
import { getToastStore } from '@skeletonlabs/skeleton';
const toastStore = getToastStore();

import { type ExtendedUser } from '$lib/models/User';
import { type Connection } from '$lib/models/Connection';
import UsersStore from '$lib/stores/Users';
import ConnectionStore from '$lib/stores/Connections';
import RoleStore from '$lib/stores/Roles';
import PrivilegesStore from '$lib/stores/Privileges';
const { addUser, updateUser } = UsersStore;
const { getConnection } = ConnectionStore;
const { getRole } = RoleStore;
const { getPrivilege } = PrivilegesStore;
import type { ExtendedUser, UserRequest } from '$lib/models/User';
import type { Connection } from '$lib/models/Connection';

import { addUser, updateUser, getUserByEmailAndConnection } from '$lib/stores/Users';
import { getConnection } from '$lib/stores/Connections';
import { getRole } from '$lib/stores/Roles';
import { getPrivilege } from '$lib/stores/Privileges';

export let user: ExtendedUser | undefined = undefined;
export let roleList: string[][];
export let connections: Connection[];

let email = user ? user.email : '';
let connection = user ? user.connection : '';
let active = user ? user.active : true;
let email: string = user && user.email ? user.email : '';
let connection: string = user && user.connection ? user.connection : '';
let active: boolean = user ? user.active : true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let roles = roleList.map(([_name, uuid]) => ({
uuid,
checked: user ? user.roles.includes(uuid) : false,
}));
let validationError: string = '';

async function saveUser() {
const selectedRoles = roles.filter((role) => role.checked);
if (selectedRoles.length === 0) {
validationError = 'At least one role must be selected.';
return;
} else {
validationError = '';
}

const generalMetadata = JSON.parse(user?.generalMetadata || '{"email":""}');
generalMetadata.email = email;

let newUser = {
let newUser: UserRequest = {
email,
connection: await getConnection(connection),
generalMetadata: JSON.stringify(generalMetadata),
active,
roles: await Promise.all(
roles
.filter((role) => role.checked)
.map((role) =>
getRole(role.uuid).then((role) => ({
...role,
privileges: role.privileges.map((uuid: string) => getPrivilege(uuid)),
})),
),
selectedRoles.map((role) =>
getRole(role.uuid).then((role) => ({
...role,
privileges: role.privileges.map((uuid: string) => getPrivilege(uuid)),
})),
),
),
};
try {
if (user) {
await updateUser({ ...newUser, uuid: user.uuid });
} else {
const findUser = await getUserByEmailAndConnection(email, connection);
if (findUser) {
toastStore.trigger({
message: 'Cannot add a user that already exists.',
background: 'variant-filled-error',
});
return;
}

await addUser(newUser);
}

toastStore.trigger({
message: `Successfully saved ${newUser ? 'new user' : 'user'} '${email}'`,
message: `Successfully saved ${user ? '' : 'new '}user '${email}'`,
background: 'variant-filled-success',
});
goto('/admin/users');
} catch (error) {
console.error(error);
toastStore.trigger({
message: `An error occured while saving ${newUser ? 'new user' : 'user'} '${email}'`,
message: `An error occured while saving ${user ? '' : 'new '}user '${email}'`,
background: 'variant-filled-error',
});
}
}
</script>

<form on:submit|preventDefault={saveUser}>
<form on:submit|preventDefault={saveUser} class="grid gap-4 my-3">
<label class="flex items-center space-x-2">
<input class="checkbox" type="checkbox" bind:checked={active} />
<p>Active</p>
</label>

{#if user?.uuid}
<label class="label">
<span>UUID:</span>
<input type="text" class="input" value={user?.uuid} disabled={true} />
</label>
{/if}

<label class="label required">
<span>Email:</span>
<input type="email" bind:value={email} class="input" required minlength="1" maxlength="255" />
Expand Down Expand Up @@ -101,20 +122,26 @@
{/each}
</fieldset>

<button type="submit" class="btn variant-ghost-primary hover:variant-filled-primary">
Save
</button>
<a href="/admin/users" class="btn variant-ghost-secondary hover:variant-filled-secondary">
Cancel
</a>
</form>
{#if validationError}
<aside data-testid="validation-error" class="alert variant-ghost-error">
<div class="alert-message">
<p>{validationError}</p>
</div>
<div class="alert-actions">
<button on:click={() => (validationError = '')}>
<i class="fa-solid fa-xmark"></i>
<span class="sr-only">Close</span>
</button>
</div>
</aside>
{/if}

<style>
label,
fieldset {
margin: 0.5em 0;
}
fieldset label {
margin: 0;
}
</style>
<div>
<button type="submit" class="btn variant-ghost-primary hover:variant-filled-primary">
Save
</button>
<a href="/admin/users" class="btn variant-ghost-secondary hover:variant-filled-secondary">
Cancel
</a>
</div>
</form>
27 changes: 6 additions & 21 deletions src/lib/components/admin/user/cell/Actions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@

import { goto } from '$app/navigation';

import UsersStore from '$lib/stores/Users';
import ConnectionStore from '$lib/stores/Connections';
import RoleStore from '$lib/stores/Roles';
import PrivilegesStore from '$lib/stores/Privileges';
const { getUser, updateUser } = UsersStore;
const { getConnection } = ConnectionStore;
const { getRole } = RoleStore;
const { getPrivilege } = PrivilegesStore;
import { getUser, updateUser } from '$lib/stores/Users';
import { getConnection } from '$lib/stores/Connections';
import { getRole } from '$lib/stores/Roles';
import { getPrivilege } from '$lib/stores/Privileges';

export let data = { cell: '', row: { status: '', email: '' } };

Expand Down Expand Up @@ -43,13 +39,13 @@
try {
await updateUser(newUser);
toastStore.trigger({
message: `Successfully ${activate ? 'activated' : 'deactivated'} user '${user.email}'`,
message: `Successfully ${activate ? 'r' : 'd'}eactivated user '${user.email}'`,
background: 'variant-filled-success',
});
} catch (error) {
console.error(error);
toastStore.trigger({
message: `An error occured while ${activate ? 'activating' : 'deactivating'} user '${
message: `An error occured while ${activate ? 'r' : 'd'}eactivating user '${
user.email
}'`,
background: 'variant-filled-error',
Expand All @@ -60,17 +56,6 @@
}
</script>

<button
data-testid={`user-view-btn-${data.cell}`}
type="button"
title="View"
aria-label="View"
class="btn-icon-color"
on:click|stopPropagation={() => goto(`/admin/users/${data.cell}`)}
>
<i class="fa-solid fa-circle-info fa-xl"></i>
</button>

{#if data.row.status === 'Active'}
<button
data-testid={`user-edit-btn-${data.cell}`}
Expand Down
12 changes: 11 additions & 1 deletion src/lib/models/User.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { Role } from '$lib/models/Role';
import type { Connection } from '$lib/models/Connection';

import type { QueryInterface } from './query/Query';

export interface User {
Expand All @@ -23,7 +26,14 @@ export interface OktaUser extends User {
readonly oktaIdToken: string;
}

// TODO: Replace metadata nad query types
export interface UserRequest extends User {
connection?: Connection;
generalMetadata: string;
active: boolean;
roles?: Role[];
}

// TODO: Replace metadata and query types
/* eslint-disable @typescript-eslint/no-explicit-any */
export function mapExtendedUser(data: any) {
return {
Expand Down
34 changes: 21 additions & 13 deletions src/lib/stores/Users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { get, writable, type Writable } from 'svelte/store';
import { mapExtendedUser, type ExtendedUser } from '../models/User';
import { mapExtendedUser, type ExtendedUser, type UserRequest, type User } from '../models/User';

import * as api from '$lib/api';

Expand All @@ -11,36 +11,44 @@ export const users: Writable<ExtendedUser[]> = writable([]);
export async function loadUsers() {
if (get(loaded)) return;

const res = await api.get(USER_URL);
const res: User[] = await api.get(USER_URL);
users.set(res.map(mapExtendedUser));
loaded.set(true);
}

async function getUser(uuid: string) {
export async function getUser(uuid: string) {
const store: ExtendedUser[] = get(users);
const user = store.find((u) => u.uuid === uuid);
if (user) {
return user;
}

const res = await api.get(`${USER_URL}/${uuid}`);
const res: User = await api.get(`${USER_URL}/${uuid}`);
return mapExtendedUser(res);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function addUser(user: any) {
const res = await api.post(USER_URL, [user]);
const newRole = mapExtendedUser(res.content[0]);
export async function getUserByEmailAndConnection(email: string, connection: string) {
let store: ExtendedUser[] = get(users);
if (store.length === 0) {
await loadUsers();
store = get(users);
}

return store.find((u) => u.email === email && u.connection === connection);
}

export async function addUser(user: UserRequest) {
const res: User[] = await api.post(USER_URL, [user]);
const newUser: ExtendedUser = mapExtendedUser(res[0]);

const store: ExtendedUser[] = get(users);
store.push(newRole);
store.push(newUser);
users.set(store);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function updateUser(user: any) {
const res = await api.put(USER_URL, [user]);
const newUser = mapExtendedUser(res.content[0]);
export async function updateUser(user: UserRequest) {
const res: User[] = await api.put(USER_URL, [user]);
const newUser: ExtendedUser = mapExtendedUser(res[0]);

const store: ExtendedUser[] = get(users);
const roleIndex: number = store.findIndex((r) => r.uuid === newUser.uuid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@

const rowClickHandler = (row: Indexable) => {
const uuid = row?.uuid;
goto(`/admin/users/${uuid}`);
goto(`/admin/users/${uuid}/edit`);
};
</script>

Expand Down
17 changes: 16 additions & 1 deletion tests/custom-context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { test as base, type Route, type BrowserContext, type Page } from '@playwright/test';
import type { TestInfo } from '@playwright/test';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
/* eslint-disable @typescript-eslint/no-explicit-any */
export function mockApiSuccess(context: BrowserContext | Page, path: string, json: any) {
return context.route(path, async (route: Route) => route.fulfill({ json }));
}

export function mockApiSuccessByMethod(
context: BrowserContext | Page,
path: string,
method: string,
json: any,
) {
return context.route(path, async (route: Route) => {
if (route.request().method() === method) {
await route.fulfill({ json });
return;
}
await route.fallback();
});
}

export function mockApiFail(
context: BrowserContext | Page,
path: string,
Expand Down
Loading