mirror of
https://github.com/thiloho/archtika.git
synced 2025-11-22 10:51:36 +01:00
Add administrator role plus manage dashboard and cleanup database migrations
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||
import type { User } from "$lib/db-schema";
|
||||
|
||||
export const handle = async ({ event, resolve }) => {
|
||||
if (!event.url.pathname.startsWith("/api/")) {
|
||||
@@ -20,6 +21,13 @@ export const handle = async ({ event, resolve }) => {
|
||||
throw redirect(303, "/");
|
||||
}
|
||||
|
||||
if (
|
||||
(userData.data as User).user_role !== "administrator" &&
|
||||
event.url.pathname.includes("/manage")
|
||||
) {
|
||||
throw redirect(303, "/");
|
||||
}
|
||||
|
||||
event.locals.user = userData.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,12 +46,16 @@
|
||||
const fileUrl = `${apiPrefix}/rpc/retrieve_file?id=${fileId}`;
|
||||
|
||||
const target = event.target as HTMLTextAreaElement;
|
||||
const newContent =
|
||||
target.value.slice(0, target.selectionStart) +
|
||||
`` +
|
||||
target.value.slice(target.selectionStart);
|
||||
const markdownToInsert = ``;
|
||||
const cursorPosition = target.selectionStart;
|
||||
const newContent = target.value.slice(0, target.selectionStart) + markdownToInsert;
|
||||
target.value.slice(target.selectionStart);
|
||||
|
||||
previewContent.value = newContent;
|
||||
|
||||
const newCursorPosition = cursorPosition + markdownToInsert.length;
|
||||
target.setSelectionRange(newCursorPosition, newCursorPosition);
|
||||
target.focus();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
background-color: var(--bg-primary);
|
||||
border-radius: var(--border-radius);
|
||||
border: var(--border-primary);
|
||||
inline-size: var(--modal-width);
|
||||
max-inline-size: 100%;
|
||||
inline-size: min(var(--modal-width), 100%);
|
||||
max-block-size: calc(100vh - var(--space-m));
|
||||
overflow-y: auto;
|
||||
z-index: 20;
|
||||
|
||||
@@ -438,19 +438,21 @@ export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
role: string;
|
||||
user_role: string;
|
||||
max_number_websites: number;
|
||||
created_at: Date;
|
||||
}
|
||||
export interface UserInput {
|
||||
id?: string;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
role?: string;
|
||||
user_role?: string;
|
||||
max_number_websites?: number;
|
||||
created_at?: Date;
|
||||
}
|
||||
const user = {
|
||||
tableName: "user",
|
||||
columns: ["id", "username", "password_hash", "role", "created_at"],
|
||||
columns: ["id", "username", "password_hash", "user_role", "max_number_websites", "created_at"],
|
||||
requiredForInsert: ["username", "password_hash"],
|
||||
primaryKey: "id",
|
||||
foreignKeys: {},
|
||||
@@ -464,6 +466,7 @@ export interface Website {
|
||||
user_id: string;
|
||||
content_type: string;
|
||||
title: string;
|
||||
max_storage_size: number;
|
||||
is_published: boolean;
|
||||
created_at: Date;
|
||||
last_modified_at: Date;
|
||||
@@ -474,6 +477,7 @@ export interface WebsiteInput {
|
||||
user_id?: string;
|
||||
content_type: string;
|
||||
title: string;
|
||||
max_storage_size?: number;
|
||||
is_published?: boolean;
|
||||
created_at?: Date;
|
||||
last_modified_at?: Date;
|
||||
@@ -486,6 +490,7 @@ const website = {
|
||||
"user_id",
|
||||
"content_type",
|
||||
"title",
|
||||
"max_storage_size",
|
||||
"is_published",
|
||||
"created_at",
|
||||
"last_modified_at",
|
||||
|
||||
@@ -151,8 +151,8 @@ export const md = (markdownContent: string, showToc = true) => {
|
||||
|
||||
try {
|
||||
html = DOMPurify.sanitize(marked.parse(markdownContent, { async: false }));
|
||||
} catch (_) {
|
||||
html = "Failed to parse markdown";
|
||||
} catch (error) {
|
||||
html = JSON.stringify(error);
|
||||
}
|
||||
|
||||
return html;
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||
const storageSizes = await apiRequest(
|
||||
fetch,
|
||||
`${API_BASE_PREFIX}/rpc/user_websites_storage_size`,
|
||||
"GET",
|
||||
{
|
||||
returnData: true
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
user: locals.user
|
||||
user: locals.user,
|
||||
storageSizes
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,30 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{#if data.storageSizes.data.length > 0}
|
||||
<section id="storage">
|
||||
<h2>
|
||||
<a href="#storage">Storage</a>
|
||||
</h2>
|
||||
<ul class="unpadded storage-grid">
|
||||
{#each data.storageSizes.data as { website_title, storage_size_bytes, max_storage_bytes, max_storage_pretty, diff_storage_pretty }}
|
||||
<li>
|
||||
<strong>{website_title}</strong>
|
||||
<label>
|
||||
{max_storage_pretty} total — {diff_storage_pretty} free<br />
|
||||
<meter
|
||||
value={storage_size_bytes}
|
||||
min="0"
|
||||
max={max_storage_bytes}
|
||||
high={max_storage_bytes * 0.75}
|
||||
></meter>
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section id="logout">
|
||||
<h2>
|
||||
<a href="#logout">Logout</a>
|
||||
@@ -71,4 +95,22 @@
|
||||
form[action="?/logout"] > button {
|
||||
max-inline-size: fit-content;
|
||||
}
|
||||
|
||||
.storage-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 35ch), 1fr));
|
||||
row-gap: var(--space-s);
|
||||
column-gap: var(--space-m);
|
||||
}
|
||||
|
||||
.storage-grid > li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3xs);
|
||||
}
|
||||
|
||||
meter {
|
||||
inline-size: min(512px, 100%);
|
||||
block-size: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
68
web-app/src/routes/(authenticated)/manage/+page.server.ts
Normal file
68
web-app/src/routes/(authenticated)/manage/+page.server.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import { apiRequest } from "$lib/server/utils";
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch }) => {
|
||||
const allUsers = (
|
||||
await apiRequest(
|
||||
fetch,
|
||||
`${API_BASE_PREFIX}/all_user_websites?order=user_created_at.desc`,
|
||||
"GET",
|
||||
{
|
||||
returnData: true
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
return {
|
||||
allUsers,
|
||||
API_BASE_PREFIX
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
updateMaxWebsiteAmount: async ({ request, fetch }) => {
|
||||
const data = await request.formData();
|
||||
|
||||
return await apiRequest(
|
||||
fetch,
|
||||
`${API_BASE_PREFIX}/user?id=eq.${data.get("user-id")}`,
|
||||
"PATCH",
|
||||
{
|
||||
body: {
|
||||
max_number_websites: data.get("number-of-websites")
|
||||
},
|
||||
successMessage: "Successfully updated user website limit"
|
||||
}
|
||||
);
|
||||
},
|
||||
updateStorageLimit: async ({ request, fetch }) => {
|
||||
const data = await request.formData();
|
||||
|
||||
console.log(`${API_BASE_PREFIX}/website?id=eq.${data.get("website-id")}`);
|
||||
|
||||
return await apiRequest(
|
||||
fetch,
|
||||
`${API_BASE_PREFIX}/website?id=eq.${data.get("website-id")}`,
|
||||
"PATCH",
|
||||
{
|
||||
body: {
|
||||
max_storage_size: data.get("storage-size")
|
||||
},
|
||||
successMessage: "Successfully updated user website storage size"
|
||||
}
|
||||
);
|
||||
},
|
||||
deleteUser: async ({ request, fetch }) => {
|
||||
const data = await request.formData();
|
||||
|
||||
return await apiRequest(
|
||||
fetch,
|
||||
`${API_BASE_PREFIX}/user?id=eq.${data.get("user-id")}`,
|
||||
"DELETE",
|
||||
{
|
||||
successMessage: "Successfully deleted user"
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
126
web-app/src/routes/(authenticated)/manage/+page.svelte
Normal file
126
web-app/src/routes/(authenticated)/manage/+page.svelte
Normal file
@@ -0,0 +1,126 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from "$app/forms";
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||
import type { ActionData, PageServerData } from "./$types";
|
||||
import { enhanceForm } from "$lib/utils";
|
||||
import { sending } from "$lib/runes.svelte";
|
||||
import DateTime from "$lib/components/DateTime.svelte";
|
||||
|
||||
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||
</script>
|
||||
|
||||
<SuccessOrError success={form?.success} message={form?.message} />
|
||||
|
||||
{#if sending.value}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
|
||||
<section id="all-users">
|
||||
<h2>
|
||||
<a href="#all-users">All users</a>
|
||||
</h2>
|
||||
<div class="scroll-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account creation</th>
|
||||
<th>UUID</th>
|
||||
<th>Username</th>
|
||||
<th>Manage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.allUsers as { user_id, user_created_at, username, max_number_websites, websites }}
|
||||
<tr>
|
||||
<td>
|
||||
<DateTime date={user_created_at} />
|
||||
</td>
|
||||
<td>{user_id}</td>
|
||||
<td>{username}</td>
|
||||
<td>
|
||||
<Modal id="manage-user-{user_id}" text="Manage">
|
||||
<hgroup>
|
||||
<h3>Manage user</h3>
|
||||
<p>User "{username}"</p>
|
||||
</hgroup>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateMaxWebsiteAmount"
|
||||
use:enhance={enhanceForm({ reset: false })}
|
||||
>
|
||||
<input type="hidden" name="user-id" value={user_id} />
|
||||
<label>
|
||||
Number of websites allowed:
|
||||
<input
|
||||
type="number"
|
||||
name="number-of-websites"
|
||||
min="0"
|
||||
value={max_number_websites}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
{#if websites.length > 0}
|
||||
<h4>Websites</h4>
|
||||
{#each websites as { id, title, max_storage_size }}
|
||||
<details>
|
||||
<summary>{title}</summary>
|
||||
<div>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateStorageLimit"
|
||||
use:enhance={enhanceForm({ reset: false })}
|
||||
>
|
||||
<input type="hidden" name="website-id" value={id} />
|
||||
<label>
|
||||
Storage limit in MB:
|
||||
<input
|
||||
type="number"
|
||||
name="storage-size"
|
||||
min="0"
|
||||
value={max_storage_size}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<h4>Delete user</h4>
|
||||
<details>
|
||||
<summary>Delete</summary>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Caution!</strong>
|
||||
Deleting the user will irretrievably erase all their data.
|
||||
</p>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteUser"
|
||||
use:enhance={enhanceForm({ closeModal: true })}
|
||||
>
|
||||
<input type="hidden" name="user-id" value={user_id} />
|
||||
<button type="submit">Delete user</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</Modal>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
form[action="?/deleteUser"] {
|
||||
margin-block-start: var(--space-2xs);
|
||||
}
|
||||
</style>
|
||||
@@ -46,8 +46,7 @@
|
||||
<a href="#publication-status">Publication status</a>
|
||||
</h2>
|
||||
<p>
|
||||
Your website is published at:
|
||||
<br />
|
||||
Your website is published at:<br />
|
||||
<a href={data.websiteProdUrl}>{data.websiteProdUrl}</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
{/if}
|
||||
<ul class="link-wrapper unpadded">
|
||||
{#if data.user}
|
||||
{#if data.user.user_role === "administrator"}
|
||||
<li>
|
||||
<a href="/manage">Manage</a>
|
||||
</li>
|
||||
{/if}
|
||||
<li>
|
||||
<a href="/account">Account</a>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user