Skip to content

Commit

Permalink
Add access dashboard for role management
Browse files Browse the repository at this point in the history
  • Loading branch information
xuxey committed Aug 21, 2023
1 parent 6eec065 commit ce4a644
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 2 deletions.
85 changes: 85 additions & 0 deletions src/lib/components/role-modal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<script lang="ts">
import Modal from '$lib/util/modal.svelte';
import { API_URL } from '../../constants';
import type { Role } from '$lib/types';
import SmartInput from '../util/smart-input.svelte';
const initialRole = {
email: '',
role: ''
};
let roleData: Role = initialRole;
export let show: boolean;
export let loadRoles: Function;
let errors: { [K in keyof Role]?: string } = {};
let apiError: string | null = null;
const validationFails = () => {
let email = roleData.email.trim();
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(email)) {
errors.email = 'This is not a valid email';
return true;
}
return false;
};
const createEvent = async () => {
if (validationFails()) return;
const response = await fetch(`${$API_URL}/roles`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(roleData)
});
if (response.ok) {
roleData = initialRole;
show = false;
loadRoles();
} else {
apiError = response.status + ' ' + response.statusText;
}
};
</script>

<Modal bind:show>
<div class="flex flex-col p-2">
<h1 class="text-xl mb-3 font-serif">Create Role</h1>

<form class="flex flex-col gap-5">
<SmartInput label="User Email" sublabel="Ensure correctness" bind:error={errors.email}>
<input type="text" class="bg-gray-700 rounded-sm p-1" bind:value={roleData.email} />
</SmartInput>
<SmartInput label="Role" bind:error={errors.role}>
<select
class="duration-300 p-3 bg-white
text-center
bg-opacity-10 hover:bg-opacity-20"
bind:value={roleData.role}
>
{#each ['Admin', 'Corporate'] as role}
<option value={role} class="bg-rp-blue">
{role}
</option>
{/each}
</select>
</SmartInput>

<div class="w-32 md:w-96 flex flex-col">
<button
class="p-2 rounded-md bg-green-600 hover:bg-green-500 duration-300"
on:click={createEvent}>Go</button
>
<div class="text-red-500 ml-1 text-sm {apiError ? '' : 'hidden'}">
{apiError}
</div>
</div>
</form>
</div>
</Modal>
8 changes: 7 additions & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ interface Event {
visible: boolean;
}

type Role = {
_id: string;
email: string;
role: string;
};

interface User {
email: string;
fullName?: string;
}

type Status = 'waiting' | 'in_progress' | 'failed' | 'success';

export type { Event, User, Status };
export type { Event, User, Status, Role };
29 changes: 29 additions & 0 deletions src/routes/dash/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import GlassContainer from '../../components/glass-container.svelte';
</script>

<main class="w-full md:w-2/3 lg:w-1/2 mx-auto">
<GlassContainer>
<div class="flex flex-col gap-5">
<h1 class="text-2xl text-white font-serif">Staff Dashboard</h1>

<div class="flex flex-row items-center text-gray-200 gap-5 flex-wrap">
<a
href="/dash/events"
class="flex flex-row gap-2 justify-center items-center text-lg md:text-xl w-60 p-2 h-20 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-md duration-200"
>
<Icon icon="ic:outline-event" class="text-xl md:text-3xl" />
<p>Events Dashboard</p>
</a>
<a
href="/dash/access"
class="flex flex-row gap-2 justify-center items-center text-lg md:text-xl w-60 p-2 h-20 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-md duration-200"
>
<Icon icon="eos-icons:role-binding" class="text-xl md:text-3xl" />
<p>Access Dashboard</p>
</a>
</div>
</div>
</GlassContainer>
</main>
20 changes: 20 additions & 0 deletions src/routes/dash/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { error } from '@sveltejs/kit';
import { API_URL } from '../../constants';
import type { PageLoad } from './$types';
import { get } from 'svelte/store';

export const load: PageLoad<void> = async ({ fetch }) => {
// TODO: Change to some access specific endpoint
const url = get(API_URL);
const check = await fetch(`${url}/auth/access/admin`, {
credentials: 'include'
});

// Not authorized or signed in:
// They don't need to know this is an actual route.
if (check.status == 403 || check.status == 401) {
throw error(404, { message: 'Not found' });
}
};

export const ssr = false;
146 changes: 146 additions & 0 deletions src/routes/dash/access/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<script lang="ts">
import { onMount } from 'svelte';
import { API_URL } from '../../../constants';
import type { Role } from '$lib/types';
import Icon from '@iconify/svelte';
import RoleModal from '../../../lib/components/role-modal.svelte';
let showModal = false;
let searchTerm = '';
const filter = (role: Role): boolean => {
const search = searchTerm.toLowerCase();
return role.email.toLowerCase().includes(search) || role.role.toLowerCase().includes(search);
};
let allRoles: Role[] = [];
let roles: Role[] = [];
let errorMessage: string;
$: if (searchTerm.length > 0) {
roles = allRoles.filter(filter);
} else {
roles = allRoles;
}
const loadRoles = async () => {
const response = await fetch(`${$API_URL}/roles`, {
credentials: 'include',
cache: 'no-cache'
});
const data = await response.json();
if (response.ok) {
allRoles = data;
} else {
errorMessage = data.message;
console.error(response.statusText);
console.error(data.message);
}
};
const onDeleteEventClick = async (role: Role) => {
const response = await fetch(`${$API_URL}/roles/${role._id}`, {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
loadRoles();
} else {
errorMessage = response.status + ' ' + response.statusText;
}
};
onMount(loadRoles);
</script>

<RoleModal bind:show={showModal} {loadRoles} />
<div class="h-full text-white flex justify-between flex-col my-20">
<div
class="bg-black bg-opacity-10 rounded-lg p-2 md:p-7 mx-auto w-full md:w-10/12 lg:w-11/12 text-sm md:text-base"
>
<div class="flex flex-row justify-between items-baseline">
<h1 class="text-xl mb-3 p-2 font-serif">Access Dashboard</h1>
<span class="flex flex-row">
<button
class="py-1 px-3 bg-pink-600 bg-opacity-80 hover:bg-opacity-100 duration-300 rounded-md h-min"
on:click={() => {
showModal = true;
}}>New</button
>
</span>
</div>

<div class="flex flex-row gap-2 mb-3 items-center w-full md:w-1/2">
<label for="search" class="text-3xl"> <Icon icon="material-symbols:search" /> </label>
<input
type="text"
name="search"
id="search"
bind:value={searchTerm}
placeholder="Email or Role"
class="bg-transparent border border-gray-400 rounded-md p-1"
/>
</div>

<div class="p-3 bg-white bg-opacity-10 rounded-md overflow-x-auto">
<table class="w-full table-auto border-spacing-10 border-collapse border-hidden">
<thead class="text-left tracking-wider">
<th>Email</th>
<th>Role</th>
<th>Revoke</th>
</thead>
<tbody>
{#each roles as role}
<tr
class=" {role.role === 'Admin' &&
'text-red-200 font-bold'} bg-opacity-0 hover:bg-opacity-10 duration-300"
>
<td title={role.email}>
{role.email}
</td>
<td title={role.role}>
{role.role}
</td>
<td title="Revoke Role">
<button aria-label="Revoke Access" on:click={() => onDeleteEventClick(role)}>
<Icon
icon="ph:x-fill"
class="text-white hover:text-red-600 duration-200 text-2xl"
/>
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div class="p-1 flex w-full flex-row justify-between text-sm text-gray-300">
<div class="text-black {errorMessage ? 'visible' : 'invisible'}">
{errorMessage}
</div>
<div>
Showing {roles.length} rows
</div>
</div>
</div>
</div>

<style>
th,
td {
padding: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
border-width: 1px;
--tw-border-opacity: 0.3;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
text-overflow: ellipsis;
overflow: hidden;
max-height: 8rem;
white-space: nowrap;
max-width: 280px;
}
</style>
19 changes: 19 additions & 0 deletions src/routes/dash/access/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { error } from '@sveltejs/kit';
import { API_URL } from '../../../constants';
import type { PageLoad } from './$types';
import { get } from 'svelte/store';

export const load: PageLoad<void> = async ({ fetch }) => {
const url = get(API_URL);
const check = await fetch(`${url}/auth/access/admin`, {
credentials: 'include'
});

// Not authorized or signed in:
// They don't need to know this is an actual route.
if (check.status == 403 || check.status == 401) {
throw error(404, { message: 'Not found' });
}
};

export const ssr = false;
1 change: 0 additions & 1 deletion src/routes/dash/events/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { PageLoad } from './$types';
import { get } from 'svelte/store';

export const load: PageLoad<void> = async ({ fetch }) => {
// TODO: Change to some access specific endpoint
const url = get(API_URL);
const check = await fetch(`${url}/auth/access/admin`, {
credentials: 'include'
Expand Down

0 comments on commit ce4a644

Please sign in to comment.