Merge branch 'main' of https://github.com/orosmatthew/bw-hspc-contest-env
This commit is contained in:
commit
002f9bfa38
2
sandbox/.gitignore
vendored
2
sandbox/.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
build
|
||||||
.env
|
.env
|
2
sandbox/.prettierignore
Normal file
2
sandbox/.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
build
|
||||||
|
node_modules
|
@ -3,6 +3,5 @@
|
|||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 100,
|
"printWidth": 100
|
||||||
"pluginSearchDirs": ["."]
|
|
||||||
}
|
}
|
||||||
|
@ -36,4 +36,4 @@ COPY . .
|
|||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
CMD ["node", "dist"]
|
CMD ["node", "build"]
|
16
sandbox/package-lock.json
generated
16
sandbox/package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
|
"prettier": "^3.0.2",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -119,6 +120,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/simple-git": {
|
"node_modules/simple-git": {
|
||||||
"version": "3.19.1",
|
"version": "3.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.19.1.tgz",
|
||||||
|
@ -5,12 +5,15 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc"
|
"build": "tsc",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"start": "node build"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
|
"prettier": "^3.0.2",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./build",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"strict": true
|
"strict": true
|
||||||
},
|
},
|
||||||
|
697
web/package-lock.json
generated
697
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "prisma generate && vite build",
|
"build": "svelte-kit sync && prisma generate && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
@ -14,20 +14,23 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.1.0",
|
"@sveltejs/adapter-auto": "^2.1.0",
|
||||||
"@sveltejs/kit": "^1.23.0",
|
"@sveltejs/kit": "^1.23.0",
|
||||||
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bootstrap": "^5.2.6",
|
"@types/bootstrap": "^5.2.6",
|
||||||
"@types/diff": "^5.0.3",
|
"@types/diff": "^5.0.3",
|
||||||
"@types/node": "^20.5.4",
|
"@types/js-cookie": "^3.0.3",
|
||||||
|
"@types/node": "^20.5.6",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||||
"@typescript-eslint/parser": "^6.4.1",
|
"@typescript-eslint/parser": "^6.4.1",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.48.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"prettier": "^3.0.2",
|
"prettier": "^3.0.2",
|
||||||
"prettier-plugin-svelte": "^3.0.3",
|
"prettier-plugin-svelte": "^3.0.3",
|
||||||
|
"sass": "^1.66.1",
|
||||||
"svelte": "^4.2.0",
|
"svelte": "^4.2.0",
|
||||||
"svelte-check": "^3.5.0",
|
"svelte-check": "^3.5.0",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.4.9"
|
"vite": "^4.4.9"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -35,14 +38,14 @@
|
|||||||
"@prisma/client": "^5.2.0",
|
"@prisma/client": "^5.2.0",
|
||||||
"@sveltejs/adapter-node": "^1.3.1",
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"axios": "^1.4.0",
|
"bcrypt": "^5.1.1",
|
||||||
"bootstrap": "^5.3.1",
|
"bootstrap": "^5.3.1",
|
||||||
|
"bootstrap-icons": "^1.10.5",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"diff2html": "^3.4.40",
|
"diff2html": "^3.4.40",
|
||||||
"eslint-plugin-svelte": "^2.33.0",
|
"eslint-plugin-svelte": "^2.33.0",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
"highlight.js": "^11.8.0",
|
"js-cookie": "^3.0.5",
|
||||||
"memfs": "^4.2.1",
|
|
||||||
"node-git-server": "^1.0.0",
|
"node-git-server": "^1.0.0",
|
||||||
"prisma": "^5.2.0",
|
"prisma": "^5.2.0",
|
||||||
"simple-git": "^3.19.1",
|
"simple-git": "^3.19.1",
|
||||||
|
@ -8,14 +8,15 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String @unique
|
username String @unique
|
||||||
password String
|
passwordHash String
|
||||||
sessions Session[]
|
passwordSalt String
|
||||||
|
sessions Session[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
token String @id
|
token String @id @default(uuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
userId Int
|
userId Int
|
||||||
|
4
web/src/app.d.ts
vendored
4
web/src/app.d.ts
vendored
@ -3,7 +3,9 @@
|
|||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
interface Locals {
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html id="html-element" lang="en" data-bs-theme="%theme%">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
@ -1,26 +1,32 @@
|
|||||||
import { redirect, type Handle } from '@sveltejs/kit';
|
import { redirect, type Handle } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/prisma';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import type { Session } from '@prisma/client';
|
|
||||||
import { startGitServer } from '$lib/server/gitserver';
|
import { startGitServer } from '$lib/server/gitserver';
|
||||||
|
import { hashPassword, isSessionValid, logout } from '$lib/server/auth';
|
||||||
|
|
||||||
startGitServer();
|
async function createDefaultAccount(db: PrismaClient) {
|
||||||
|
const count = await db.user.count();
|
||||||
const sessionExpireMilliseconds = 1000 * 60 * 60 * 24; // 24 hours
|
if (count !== 0) {
|
||||||
|
return;
|
||||||
function isSessionExpired(session: Session): boolean {
|
}
|
||||||
return session.createdAt.valueOf() + sessionExpireMilliseconds < new Date().valueOf();
|
const password = await hashPassword('bw123');
|
||||||
}
|
await db.user.create({
|
||||||
|
data: { username: 'admin', passwordHash: password.hash, passwordSalt: password.salt }
|
||||||
async function removeExpiredSessions(userId: number) {
|
|
||||||
const sessions: Session[] = await db.session.findMany({ where: { userId: userId } });
|
|
||||||
sessions.forEach(async (session) => {
|
|
||||||
if (isSessionExpired(session)) {
|
|
||||||
await db.session.delete({ where: { token: session.token } });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = new PrismaClient();
|
||||||
|
createDefaultAccount(db);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Initialization in hooks failed (Normal on build)');
|
||||||
|
}
|
||||||
|
|
||||||
|
startGitServer();
|
||||||
|
|
||||||
export const handle = (async ({ event, resolve }) => {
|
export const handle = (async ({ event, resolve }) => {
|
||||||
|
const theme = event.cookies.get('theme') as 'light' | 'dark' | undefined;
|
||||||
|
event.locals.theme = theme ?? 'dark';
|
||||||
|
|
||||||
if (event.request.method === 'OPTIONS') {
|
if (event.request.method === 'OPTIONS') {
|
||||||
return new Response('ok', {
|
return new Response('ok', {
|
||||||
headers: {
|
headers: {
|
||||||
@ -33,42 +39,18 @@ export const handle = (async ({ event, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.url.pathname.startsWith('/login')) {
|
if (event.url.pathname.startsWith('/login')) {
|
||||||
if (event.cookies.get('token')) {
|
if ((await isSessionValid(event.cookies)) === true) {
|
||||||
const session = await db.session.findUnique({ where: { token: event.cookies.get('token') } });
|
throw redirect(302, '/admin');
|
||||||
if (session) {
|
|
||||||
removeExpiredSessions(session.userId);
|
|
||||||
if (!isSessionExpired(session)) {
|
|
||||||
throw redirect(302, '/admin');
|
|
||||||
} else {
|
|
||||||
event.cookies.delete('token');
|
|
||||||
const res = resolve(event);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = resolve(event);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event.url.pathname.startsWith('/admin')) {
|
if (event.url.pathname.startsWith('/admin')) {
|
||||||
if (event.cookies.get('token')) {
|
if ((await isSessionValid(event.cookies)) !== true) {
|
||||||
const session = await db.session.findUnique({ where: { token: event.cookies.get('token') } });
|
logout(event.cookies);
|
||||||
if (session) {
|
|
||||||
removeExpiredSessions(session.userId);
|
|
||||||
if (!isSessionExpired(session)) {
|
|
||||||
const res = await resolve(event);
|
|
||||||
return res;
|
|
||||||
} else {
|
|
||||||
event.cookies.delete('token');
|
|
||||||
throw redirect(302, '/login');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw redirect(302, '/login');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw redirect(302, '/login');
|
throw redirect(302, '/login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = await resolve(event);
|
const res = await resolve(event, {
|
||||||
|
transformPageChunk: ({ html }) => html.replace('%theme%', event.locals.theme)
|
||||||
|
});
|
||||||
return res;
|
return res;
|
||||||
}) satisfies Handle;
|
}) satisfies Handle;
|
||||||
|
76
web/src/lib/ConfirmModal.svelte
Normal file
76
web/src/lib/ConfirmModal.svelte
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import type bootstrap from 'bootstrap';
|
||||||
|
import { beforeNavigate } from '$app/navigation';
|
||||||
|
|
||||||
|
export let modalTitle = 'Confirm';
|
||||||
|
export let modalText = 'Are you sure?';
|
||||||
|
|
||||||
|
let confirmModal: HTMLDivElement;
|
||||||
|
let modal: bootstrap.Modal | undefined;
|
||||||
|
|
||||||
|
let confirmAction: (() => void) | undefined;
|
||||||
|
let cancelAction: (() => void) | undefined;
|
||||||
|
|
||||||
|
export function confirm() {
|
||||||
|
if (confirmAction !== undefined) {
|
||||||
|
confirmAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancel() {
|
||||||
|
if (cancelAction !== undefined) {
|
||||||
|
cancelAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prompt(text = 'Are you sure?', title = 'Confirm'): Promise<boolean> {
|
||||||
|
modalText = text;
|
||||||
|
modalTitle = title;
|
||||||
|
if (modal === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
modal.show();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
confirmAction = () => {
|
||||||
|
resolve(true);
|
||||||
|
modal?.hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelAction = () => {
|
||||||
|
resolve(false);
|
||||||
|
modal?.hide();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const bootstrap = await import('bootstrap');
|
||||||
|
modal = new bootstrap.Modal(confirmModal);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeNavigate(() => {
|
||||||
|
modal?.hide();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={confirmModal} class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="exampleModalLabel">{modalTitle}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
{#if $$slots.default}
|
||||||
|
<slot />
|
||||||
|
{:else}
|
||||||
|
{modalText}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button on:click={cancel} type="button" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button on:click={confirm} type="button" class="btn btn-primary">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
46
web/src/lib/FormAlert.svelte
Normal file
46
web/src/lib/FormAlert.svelte
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { afterUpdate } from 'svelte';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
|
||||||
|
let dismissed = false;
|
||||||
|
let success = false;
|
||||||
|
let message: string | undefined;
|
||||||
|
let manualPopup = false;
|
||||||
|
|
||||||
|
$: if ($page.form !== null) {
|
||||||
|
manualPopup = false;
|
||||||
|
dismissed = false;
|
||||||
|
success = $page.form.success;
|
||||||
|
message = $page.form.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
if ($page.form !== null) {
|
||||||
|
success = $page.form.success;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function popup(data: { success: boolean; message?: string }) {
|
||||||
|
dismissed = false;
|
||||||
|
manualPopup = true;
|
||||||
|
success = data.success;
|
||||||
|
message = data.message;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if ($page.form !== null && dismissed === false) || (manualPopup === true && dismissed === false)}
|
||||||
|
<div
|
||||||
|
transition:slide|local
|
||||||
|
class={`mt-2 mb-2 alert alert-dismissible alert-${success ? 'success' : 'danger'}`}
|
||||||
|
>
|
||||||
|
{success ? 'Success' : message ?? 'Unknown error'}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
dismissed = true;
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
89
web/src/lib/server/auth.ts
Normal file
89
web/src/lib/server/auth.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { redirect, type Cookies } from '@sveltejs/kit';
|
||||||
|
import { db } from './prisma';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
export const sessionExpireSeconds = 60 * 60 * 24; // A day
|
||||||
|
|
||||||
|
export async function hashPassword(
|
||||||
|
password: string,
|
||||||
|
salt?: string
|
||||||
|
): Promise<{ salt: string; hash: string }> {
|
||||||
|
if (salt === undefined) {
|
||||||
|
salt = await bcrypt.genSalt();
|
||||||
|
}
|
||||||
|
const hash = await bcrypt.hash(password, salt);
|
||||||
|
return { salt: salt, hash: hash };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteExpiredSessions(userId: number) {
|
||||||
|
const expirationDate = new Date(new Date().valueOf() - 1000 * sessionExpireSeconds);
|
||||||
|
await db.session.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: userId,
|
||||||
|
createdAt: { lt: expirationDate }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout(cookies: Cookies): Promise<boolean> {
|
||||||
|
const sessionCookie = cookies.get('session');
|
||||||
|
if (sessionCookie === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cookies.delete('session');
|
||||||
|
try {
|
||||||
|
await db.session.delete({ where: { token: sessionCookie } });
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function attemptLogin(
|
||||||
|
cookies: Cookies,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (username === '' || password === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const user = await db.user.findUnique({ where: { username: username.toString() } });
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
deleteExpiredSessions(user.id);
|
||||||
|
const hash = await hashPassword(password, user.passwordSalt);
|
||||||
|
if (user.passwordHash === hash.hash.toString()) {
|
||||||
|
const session = await db.session.create({ data: { userId: user.id } });
|
||||||
|
cookies.set('session', session.token, {
|
||||||
|
secure: process.env.NODE_ENV === 'development' ? false : true,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
maxAge: sessionExpireSeconds
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isSessionValid(cookies: Cookies): Promise<boolean> {
|
||||||
|
const sessionCookie = cookies.get('session');
|
||||||
|
if (sessionCookie === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const session = await db.session.findUnique({ where: { token: sessionCookie } });
|
||||||
|
if (!session) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (new Date().valueOf() - session.createdAt.valueOf() > 1000 * sessionExpireSeconds) {
|
||||||
|
await db.session.delete({ where: { token: session.token } });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function redirectIfSessionInvalid(url: string, cookies: Cookies): Promise<void> {
|
||||||
|
if (!(await isSessionValid(cookies))) {
|
||||||
|
throw redirect(302, url);
|
||||||
|
}
|
||||||
|
}
|
5
web/src/routes/+layout.server.ts
Normal file
5
web/src/routes/+layout.server.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ locals }) => {
|
||||||
|
return { theme: locals.theme };
|
||||||
|
}) satisfies LayoutServerLoad;
|
@ -2,9 +2,26 @@
|
|||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import '$lib/styles/global.css';
|
import '$lib/styles/global.css';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import type { LayoutData } from './$types';
|
||||||
|
import { theme } from './stores';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import 'bootstrap-icons/font/bootstrap-icons.min.css';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await import('bootstrap');
|
await import('bootstrap');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export let data: LayoutData;
|
||||||
|
|
||||||
|
$theme = data.theme;
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
theme.subscribe((value) => {
|
||||||
|
document.getElementById('html-element')?.setAttribute('data-bs-theme', value);
|
||||||
|
Cookies.set('theme', value, { sameSite: 'strict' });
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<body class="container">
|
<body class="container">
|
||||||
|
@ -1,15 +1,83 @@
|
|||||||
<header style="font-size: 24px" class="mt-2 d-flex align-items-center justify-content-center">
|
<script lang="ts">
|
||||||
<ul class="nav col-12 col-md-auto justify-content-center">
|
import { goto } from '$app/navigation';
|
||||||
<li><a href="/admin" class="nav-link px-2">Home</a></li>
|
import { theme } from '../stores';
|
||||||
<li><a href="/admin/reviews" class="nav-link px-2">Reviews</a></li>
|
</script>
|
||||||
<li><a href="/admin/submissions" class="nav-link px-2">Submissions</a></li>
|
|
||||||
<li><a href="/admin/problems" class="nav-link px-2">Problems</a></li>
|
<nav class="main-nav mt-2 mb-3 navbar navbar-expand-lg bg-body-secondary">
|
||||||
<li><a href="/admin/scoreboard" class="nav-link px-2">Scoreboards</a></li>
|
<div class="container-fluid">
|
||||||
<li><a href="/admin/teams" class="nav-link px-2">Teams</a></li>
|
<button
|
||||||
<li><a href="/admin/contests" class="nav-link px-2">Contests</a></li>
|
class="navbar-toggler"
|
||||||
<li><a href="/logout" class="nav-link px-2" data-sveltekit-preload-data="off">Logout</a></li>
|
type="button"
|
||||||
</ul>
|
data-bs-toggle="collapse"
|
||||||
</header>
|
data-bs-target="#navbarSupportedContent"
|
||||||
<hr />
|
aria-controls="navbarSupportedContent"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon" />
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin" class="nav-link"><i class="bi bi-speedometer2"></i> Dashboard</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/reviews" class="nav-link"><i class="bi bi-eye"></i> Reviews</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/submissions" class="nav-link"
|
||||||
|
><i class="bi bi-envelope-paper"></i> Submissions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/problems" class="nav-link"
|
||||||
|
><i class="bi bi-question-circle"></i> Problems</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/scoreboard" class="nav-link"><i class="bi bi-trophy"></i> Scoreboards</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/teams" class="nav-link"><i class="bi bi-people"></i> Teams</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contests" class="nav-link"><i class="bi bi-flag"></i> Contests</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-sticky-right">
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
$theme = $theme === 'light' ? 'dark' : 'light';
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
aria-label="theme"
|
||||||
|
class="btn"><i class={`bi bi-${$theme == 'light' ? 'sun' : 'moon'}`} /></button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
on:click={async () => {
|
||||||
|
const res = await fetch('/logout', { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
goto('/login');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary">Logout</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
<slot />
|
<slot />
|
||||||
<div style="height: 100px" />
|
|
||||||
|
<style>
|
||||||
|
.main-nav {
|
||||||
|
border-radius: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nav-sticky-right {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
3
web/src/routes/admin/+page.server.ts
Normal file
3
web/src/routes/admin/+page.server.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async () => {}) satisfies PageServerLoad;
|
@ -1,5 +1,5 @@
|
|||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Admin Home</title>
|
<title>Dashboard</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Admin Home</h1>
|
<h1 style="text-align:center" class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h1>
|
||||||
|
@ -8,12 +8,10 @@
|
|||||||
<title>Contests</title>
|
<title>Contests</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Contests</h1>
|
<h1 style="text-align:center" class="mb-1"><i class="bi bi-flag"></i> Contests</h1>
|
||||||
|
|
||||||
<div class="row">
|
<div class="d-flex flex-row justify-content-end">
|
||||||
<div class="text-end">
|
<a href="/admin/contests/create" class="btn btn-outline-success">Create</a>
|
||||||
<a href="/admin/contests/create" class="btn btn-outline-success">Create</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 list-group">
|
<div class="mt-3 list-group">
|
||||||
|
@ -1,26 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import type { Actions, PageData } from './$types';
|
import ConfirmModal from '$lib/ConfirmModal.svelte';
|
||||||
|
import FormAlert from '$lib/FormAlert.svelte';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: Actions;
|
|
||||||
|
let confirmModal: ConfirmModal;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{data.name}</title>
|
<title>{data.name}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<ConfirmModal bind:this={confirmModal} />
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">{data.name}</h1>
|
<h1 style="text-align:center" class="mb-4">{data.name}</h1>
|
||||||
|
|
||||||
|
<FormAlert />
|
||||||
|
|
||||||
{#if data.activeTeams !== 0}
|
{#if data.activeTeams !== 0}
|
||||||
<div class="alert alert-success">In Progress</div>
|
<div class="alert alert-success">In Progress</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if form && !form.success}
|
|
||||||
<div class="alert alert-danger">An error occured</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<a href="/admin/contests" class="btn btn-outline-primary">All Contests</a>
|
<a href="/admin/contests" class="btn btn-outline-primary">All Contests</a>
|
||||||
@ -28,12 +31,12 @@
|
|||||||
<div class="col-6 text-end">
|
<div class="col-6 text-end">
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
use:enhance={({ cancel }) => {
|
use:enhance={async ({ cancel }) => {
|
||||||
if (!confirm('Are you sure?')) {
|
if ((await confirmModal.prompt('Are you sure?')) !== true) {
|
||||||
cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
return async ({ update }) => {
|
return async ({ update }) => {
|
||||||
update();
|
await update();
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import FormAlert from '$lib/FormAlert.svelte';
|
||||||
import type { Actions, PageData } from './$types';
|
import type { Actions, PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
@ -39,14 +40,12 @@
|
|||||||
<title>Create Contest</title>
|
<title>Create Contest</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Create Contest</h1>
|
<h1 style="text-align:center" class="mb-4"><i class="bi bi-flag"></i> Create Contest</h1>
|
||||||
|
|
||||||
|
<FormAlert />
|
||||||
|
|
||||||
<a href="/admin/contests" class="mb-3 btn btn-outline-secondary">Cancel</a>
|
<a href="/admin/contests" class="mb-3 btn btn-outline-secondary">Cancel</a>
|
||||||
|
|
||||||
{#if form && !form.success}
|
|
||||||
<div class="alert alert-danger">Invalid entry</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<form method="POST" action="?/create" use:enhance>
|
<form method="POST" action="?/create" use:enhance>
|
||||||
<h4>Name</h4>
|
<h4>Name</h4>
|
||||||
<input name="name" class="form-control" />
|
<input name="name" class="form-control" />
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { stretchTextarea } from '$lib/util';
|
import { stretchTextarea } from '$lib/util';
|
||||||
|
import FormAlert from '$lib/FormAlert.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: Actions;
|
export let form: Actions;
|
||||||
@ -58,11 +59,7 @@
|
|||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Review Submission</h1>
|
<h1 style="text-align:center" class="mb-4">Review Submission</h1>
|
||||||
|
|
||||||
{#if form && !form.success}
|
<FormAlert />
|
||||||
<div class="alert alert-danger">Submission was not successful</div>
|
|
||||||
{:else if form && form.success}
|
|
||||||
<div class="alert alert-success">Success!</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="mb-3 col">
|
<div class="mb-3 col">
|
||||||
<a href="/admin/reviews" class="btn btn-outline-primary">All Reviews</a>
|
<a href="/admin/reviews" class="btn btn-outline-primary">All Reviews</a>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<title>Problems</title>
|
<title>Problems</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Problems</h1>
|
<h1 style="text-align:center" class="mb-1"><i class="bi bi-question-circle"></i> Problems</h1>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import ConfirmModal from '$lib/ConfirmModal.svelte';
|
||||||
import { stretchTextarea } from '$lib/util';
|
import { stretchTextarea } from '$lib/util';
|
||||||
import type { Actions, PageData } from './$types';
|
import type { Actions, PageData } from './$types';
|
||||||
|
|
||||||
@ -11,7 +12,7 @@
|
|||||||
export let form: Actions;
|
export let form: Actions;
|
||||||
|
|
||||||
async function deleteProblem() {
|
async function deleteProblem() {
|
||||||
const sure = confirm('Are you sure?');
|
const sure = await confirmModal.prompt('Are you sure?');
|
||||||
if (!sure) {
|
if (!sure) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -23,9 +24,15 @@
|
|||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let confirmModal: ConfirmModal;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">{data.problemData.friendlyName}</h1>
|
<ConfirmModal bind:this={confirmModal} />
|
||||||
|
|
||||||
|
<h1 style="text-align:center" class="mb-1">
|
||||||
|
<i class="bi bi-question-circle"></i> Problem - {data.problemData.friendlyName}
|
||||||
|
</h1>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<title>Reviews</title>
|
<title>Reviews</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Reviews</h1>
|
<h1 style="text-align:center" class="mb-1"><i class="bi bi-eye"></i> Reviews</h1>
|
||||||
|
|
||||||
<div class="mb-3 text-end">
|
<div class="mb-3 text-end">
|
||||||
{#if updating}
|
{#if updating}
|
||||||
|
@ -22,10 +22,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Admin Scoreboard</title>
|
<title>Admin Scoreboards</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Admin Scoreboards</h1>
|
<h1 style="text-align:center" class="mb-1"><i class="bi bi-trophy"></i> Admin Scoreboards</h1>
|
||||||
|
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
{#if updating}
|
{#if updating}
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<title>Submissions</title>
|
<title>Submissions</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Submissions</h1>
|
<h1 style="text-align:center" class="mb-4"><i class="bi bi-envelope-paper"></i> Submissions</h1>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'diff2html/bundles/css/diff2html.min.css';
|
import 'diff2html/bundles/css/diff2html.min.css';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { stretchTextarea } from '$lib/util';
|
import { stretchTextarea } from '$lib/util';
|
||||||
|
import ConfirmModal from '$lib/ConfirmModal.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: Actions;
|
export let form: Actions;
|
||||||
@ -25,12 +26,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let confirmModal: ConfirmModal;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Submission</title>
|
<title>Submission</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<ConfirmModal bind:this={confirmModal} />
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Submission</h1>
|
<h1 style="text-align:center" class="mb-4">Submission</h1>
|
||||||
|
|
||||||
{#if form && !form.success}
|
{#if form && !form.success}
|
||||||
@ -45,12 +50,12 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/delete"
|
action="?/delete"
|
||||||
use:enhance={({ cancel }) => {
|
use:enhance={async ({ cancel }) => {
|
||||||
if (!confirm('Are you sure?')) {
|
if ((await confirmModal.prompt('Are you sure?')) !== true) {
|
||||||
cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
return async ({ update }) => {
|
return async ({ update }) => {
|
||||||
update();
|
await update();
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -110,5 +115,56 @@
|
|||||||
<h3 style="text-align:center">Output</h3>
|
<h3 style="text-align:center">Output</h3>
|
||||||
<textarea use:stretchTextarea class="code mb-3 form-control" disabled>{data.output}</textarea>
|
<textarea use:stretchTextarea class="code mb-3 form-control" disabled>{data.output}</textarea>
|
||||||
<h3 style="text-align:center">Diff</h3>
|
<h3 style="text-align:center">Diff</h3>
|
||||||
<div id="diff" />
|
<div id="diff" class="dark-diff" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
:global(.dark-diff) {
|
||||||
|
:global(.d2h-code-side-linenumber),
|
||||||
|
:global(.d2h-info),
|
||||||
|
:global(.d2h-emptyplaceholder),
|
||||||
|
:global(.d2h-code-side-emptyplaceholder),
|
||||||
|
:global(.d2h-file-header),
|
||||||
|
:global(.d2h-tag) {
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
:global(span) {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.d2h-file-wrapper) {
|
||||||
|
border-color: var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.d2h-file-header) {
|
||||||
|
border-bottom-color: var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.d2h-info) {
|
||||||
|
border-color: var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.d2h-del) {
|
||||||
|
background-color: var(--bs-danger-border-subtle);
|
||||||
|
border-color: var(--bs-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(del) {
|
||||||
|
background-color: rgba(210, 85, 97, 0.5);
|
||||||
|
}
|
||||||
|
:global(.d2h-ins) {
|
||||||
|
background-color: var(--bs-success-border-subtle);
|
||||||
|
border-color: var(--bs-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.d2h-code-side-emptyplaceholder),
|
||||||
|
:global(.d2h-emptyplaceholder) {
|
||||||
|
border-color: var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(ins) {
|
||||||
|
background-color: rgba(13, 125, 75, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<title>Teams</title>
|
<title>Teams</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Teams</h1>
|
<h1 style="text-align:center" class="mb-1"><i class="bi bi-people"></i> Teams</h1>
|
||||||
|
|
||||||
{#if form && !form.success}
|
{#if form && !form.success}
|
||||||
<div class="alert alert-danger">Invalid action</div>
|
<div class="alert alert-danger">Invalid action</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import ConfirmModal from '$lib/ConfirmModal.svelte';
|
||||||
import { genPassword } from '../util';
|
import { genPassword } from '../util';
|
||||||
import type { Actions, PageData } from './$types';
|
import type { Actions, PageData } from './$types';
|
||||||
|
|
||||||
@ -16,12 +17,16 @@
|
|||||||
const passEntry = document.getElementById('pass_entry') as HTMLInputElement;
|
const passEntry = document.getElementById('pass_entry') as HTMLInputElement;
|
||||||
passEntry.value = genPassword();
|
passEntry.value = genPassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let confirmModal: ConfirmModal;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Team</title>
|
<title>Team</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<ConfirmModal bind:this={confirmModal} />
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">{data.team.name}</h1>
|
<h1 style="text-align:center" class="mb-4">{data.team.name}</h1>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -32,12 +37,12 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/delete"
|
action="?/delete"
|
||||||
use:enhance={({ cancel }) => {
|
use:enhance={async ({ cancel }) => {
|
||||||
if (!confirm('Are you sure?')) {
|
if ((await confirmModal.prompt('Are you sure?')) !== true) {
|
||||||
cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
return async ({ update }) => {
|
return async ({ update }) => {
|
||||||
update();
|
await update();
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,25 +1,21 @@
|
|||||||
import type { Actions } from '@sveltejs/kit';
|
import type { Actions } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/prisma';
|
import { attemptLogin } from '$lib/server/auth';
|
||||||
import * as UUID from 'uuid';
|
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
login: async ({ cookies, request }) => {
|
login: async ({ cookies, request }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const username = data.get('username')?.toString();
|
const formUsername = data.get('username');
|
||||||
const password = data.get('password')?.toString();
|
const formPassword = data.get('password');
|
||||||
if (!username || !password) {
|
if (formUsername === null || formPassword === null) {
|
||||||
return { success: false };
|
return { success: false, message: 'Incomplete form data' };
|
||||||
}
|
}
|
||||||
const user = await db.user.findUnique({ where: { username: username } });
|
if (
|
||||||
if (!user) {
|
(await attemptLogin(cookies, formUsername.toString().trim(), formPassword.toString())) !==
|
||||||
return { success: false };
|
true
|
||||||
}
|
) {
|
||||||
if (user.password === password) {
|
return { success: false, message: 'Invalid login' };
|
||||||
const uuid: string = UUID.v4();
|
} else {
|
||||||
await db.session.create({ data: { token: uuid, userId: user.id } });
|
|
||||||
cookies.set('token', uuid);
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
return { success: false };
|
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
@ -1,37 +1,73 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import type { ActionData } from './$types';
|
import type { Actions } from './$types';
|
||||||
export let form: ActionData;
|
import { slide, fly } from 'svelte/transition';
|
||||||
|
|
||||||
$: if (browser) {
|
export let form: Actions;
|
||||||
if (form && form.success) {
|
|
||||||
goto('/admin/reviews');
|
let dismissed = false;
|
||||||
|
|
||||||
|
$: if (form) {
|
||||||
|
if (form.success) {
|
||||||
|
goto('/admin');
|
||||||
}
|
}
|
||||||
|
dismissed = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mt-4 row justify-content-center">
|
<div transition:fly|global={{ y: -50 }} class="container login-modal bg-body-tertiary">
|
||||||
<div class="col-4">
|
<h1 class="mt-3 text-center">BW Contest Admin</h1>
|
||||||
<h1>Login</h1>
|
{#if form && !dismissed}
|
||||||
<form method="POST" action="?/login" use:enhance>
|
<div
|
||||||
<label for="username_field" class="form-label">Username</label>
|
transition:slide|global
|
||||||
<input type="text" name="username" class="form-control" id="username_field" />
|
class={`mt-4 alert alert-dismissible alert-${form.success ? 'success' : 'danger'}`}
|
||||||
|
>
|
||||||
<label for="password_field" class="form-label">Password</label>
|
{form.success ? 'Success' : form.message ?? 'Unknown Error'}
|
||||||
<input type="password" name="password" class="form-control" id="password_field" />
|
<button
|
||||||
|
on:click={() => {
|
||||||
<div class="mt-2">
|
dismissed = true;
|
||||||
<button type="submit" class="btn btn-primary">Login</button>
|
}}
|
||||||
</div>
|
type="button"
|
||||||
</form>
|
class="btn-close"
|
||||||
<div class="mt-2">
|
aria-label="Close"
|
||||||
{#if form?.success}
|
/>
|
||||||
<div class="alert alert-success" role="alert">Success!</div>
|
|
||||||
{:else if form && !form.success}
|
|
||||||
<div class="alert alert-danger" role="alert">Invalid login</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
<form class="mt-4" action="?/login" method="POST" use:enhance>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="usernameInput"
|
||||||
|
placeholder="Username"
|
||||||
|
/>
|
||||||
|
<label for="usernameInput">Username</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 form-floating">
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="passwordInput"
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
<label for="passwordInput">Password</label>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row mt-4 mb-4 justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.login-modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 300px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
|
||||||
import type { PageServerLoad } from './$types';
|
|
||||||
import { db } from '$lib/server/prisma';
|
|
||||||
|
|
||||||
export const load = (async ({ cookies }) => {
|
|
||||||
if (!cookies.get('token')) {
|
|
||||||
throw redirect(302, '/login');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await db.session.delete({ where: { token: cookies.get('token') } });
|
|
||||||
} catch {
|
|
||||||
throw redirect(302, '/login');
|
|
||||||
}
|
|
||||||
cookies.delete('token');
|
|
||||||
throw redirect(302, '/login');
|
|
||||||
}) satisfies PageServerLoad;
|
|
@ -1 +0,0 @@
|
|||||||
<h1>Logging Out</h1>
|
|
8
web/src/routes/logout/+server.ts
Normal file
8
web/src/routes/logout/+server.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { logout } from '$lib/server/auth';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
export const POST = (async ({ cookies }) => {
|
||||||
|
await logout(cookies);
|
||||||
|
return json({ success: true });
|
||||||
|
}) satisfies RequestHandler;
|
3
web/src/routes/stores.ts
Normal file
3
web/src/routes/stores.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export const theme = writable<'light' | 'dark'>('dark');
|
@ -7,6 +7,9 @@ const config = {
|
|||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter()
|
adapter: adapter()
|
||||||
|
},
|
||||||
|
vitePlugin: {
|
||||||
|
inspector: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user