start dev
This commit is contained in:
+315
@@ -1,2 +1,317 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/forms';
|
||||
|
||||
/* defaults */
|
||||
:root {
|
||||
--text-color: #000000;
|
||||
--text-box-bg: #00000010;
|
||||
--bg-color: #e9e9e9;
|
||||
--hover-bg-color: #d0d0d0;
|
||||
--separator-line-color: #a0a0a0;
|
||||
--low-emphasis-text-color: #6b6b6b;
|
||||
--primary-color: #1f96f3;
|
||||
--dull-primary-color: #51aaf0;
|
||||
--elevated-bg-color: #ffffff;
|
||||
--bg-accent-color: #f4f4f4;
|
||||
--danger-color: #ff2d2f;
|
||||
--hyperlink-color: #3b82f6;
|
||||
--nav-bg-color: #f0f0f0;
|
||||
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.bg-color {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.hover-bg-color:hover {
|
||||
background-color: var(--hover-bg-color);
|
||||
}
|
||||
|
||||
.low-emphasis-text {
|
||||
color: var(--low-emphasis-text-color);
|
||||
}
|
||||
|
||||
.low-emphasis-text-button {
|
||||
color: var(--low-emphasis-text-color);
|
||||
}
|
||||
|
||||
.low-emphasis-text-button:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.primary-underline {
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.top-border {
|
||||
border-top: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.bottom-border {
|
||||
border-bottom: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.left-border {
|
||||
border-left: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.right-border {
|
||||
border-right: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
input[type='search'],
|
||||
input[type='text'],
|
||||
input[type='password'],
|
||||
input[type='email'],
|
||||
input[type='tel'],
|
||||
input[type='number'],
|
||||
textarea,
|
||||
select {
|
||||
@apply rounded-t;
|
||||
background-color: var(--text-box-bg);
|
||||
color: var(--text-color);
|
||||
border: 0;
|
||||
border-bottom: 2px solid var(--separator-line-color);
|
||||
caret-color: var(--text-color);
|
||||
}
|
||||
|
||||
input[type='search']:focus,
|
||||
input[type='text']:focus,
|
||||
input[type='password']:focus,
|
||||
input[type='email']:focus,
|
||||
input[type='tel']:focus,
|
||||
input[type='number']:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: 0 solid var(--text-color);
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
box-shadow: 0 0 0 0 var(--primary-color);
|
||||
caret-color: var(--text-color);
|
||||
}
|
||||
|
||||
input[type='search']:-webkit-autofill,
|
||||
input[type='text']:-webkit-autofill,
|
||||
input[type='password']:-webkit-autofill,
|
||||
input[type='email']:-webkit-autofill,
|
||||
input[type='tel']:-webkit-autofill,
|
||||
input[type='number']:-webkit-autofill,
|
||||
textarea:-webkit-autofill,
|
||||
select:-webkit-autofill {
|
||||
-webkit-text-fill-color: var(--text-color) !important;
|
||||
background-clip: text !important;
|
||||
caret-color: var(--text-color);
|
||||
}
|
||||
|
||||
input[type='search'].outlined,
|
||||
input[type='text'].outlined,
|
||||
input[type='password'].outlined,
|
||||
input[type='email'].outlined,
|
||||
input[type='tel'].outlined,
|
||||
input[type='number'].outlined,
|
||||
textarea.outlined,
|
||||
select.outlined {
|
||||
@apply rounded;
|
||||
border: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
input[type='search'].outlined:focus,
|
||||
input[type='text'].outlined:focus,
|
||||
input[type='password'].outlined:focus,
|
||||
input[type='email'].outlined:focus,
|
||||
input[type='tel'].outlined:focus,
|
||||
input[type='number'].outlined:focus,
|
||||
textarea.outlined:focus,
|
||||
select.outlined:focus {
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
background-color: var(--text-box-bg);
|
||||
border: 1px solid var(--separator-line-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
input[type='checkbox']:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.text-box-bg {
|
||||
background-color: var(--text-box-bg) !important;
|
||||
}
|
||||
|
||||
.separator-borders {
|
||||
border: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.elevated {
|
||||
background-color: var(--elevated-bg-color);
|
||||
@apply shadow-lg;
|
||||
}
|
||||
|
||||
.elevated-bg {
|
||||
background-color: var(--elevated-bg-color);
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply rounded-lg p-4 block shadow-lg;
|
||||
background-color: var(--elevated-bg-color);
|
||||
}
|
||||
|
||||
.nav-bg {
|
||||
background-color: var(--nav-bg-color);
|
||||
}
|
||||
|
||||
.primary-bg-color {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.accent-bg-color {
|
||||
background-color: var(--bg-accent-color);
|
||||
}
|
||||
|
||||
.dull-primary-bg-color {
|
||||
background-color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
.dull-primary-text-color {
|
||||
color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
.dull-primary-border-color {
|
||||
border-color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
.primary-text-color {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.danger-bg-color {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.danger-color {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.danger-border-color {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.hover-hyperlink:hover {
|
||||
color: var(--hyperlink-color);
|
||||
text-decoration: underline var(--hyperlink-color);
|
||||
}
|
||||
|
||||
.hyperlink-color {
|
||||
color: var(--hyperlink-color);
|
||||
}
|
||||
|
||||
.hyperlink-underline {
|
||||
text-decoration: underline var(--hyperlink-color);
|
||||
}
|
||||
|
||||
.center {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.normal, a.normal {
|
||||
@apply px-2 py-1 leading-tight rounded-lg;
|
||||
border-width: 1px;
|
||||
border-color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
button.large, a.large {
|
||||
@apply px-4 py-3 leading-tight rounded-xl;
|
||||
border-width: 1px;
|
||||
border-color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
button.wide {
|
||||
@apply px-4 py-2 leading-tight rounded w-full;
|
||||
}
|
||||
|
||||
button.text {
|
||||
color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
button.filled, a.filled {
|
||||
background-color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background-color: var(--danger-color);
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
button.outlined-danger {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
button.text-danger {
|
||||
color: var(--danger-color);
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.switch {
|
||||
display: flex;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.switch button {
|
||||
flex: 1;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
background: #f0f0f0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.switch button.selected {
|
||||
background: #007acc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgb(0, 0, 0); /* Fallback color */
|
||||
background-color: rgba(0, 0, 0, 0.6); /* Darken background */
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--bg-color);
|
||||
margin: 10% auto;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--separator-line-color);
|
||||
max-width: 420px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: rgb(from var(--primary-color) r g b / 10%);
|
||||
}
|
||||
|
||||
+11
-8
@@ -1,11 +1,14 @@
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>FBLA26</title>
|
||||
<link href="%sveltekit.assets%/favicon.png" rel="icon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { User } from '$lib/types';
|
||||
import bcrypt from 'bcrypt';
|
||||
import sql from '$lib/db/db.server';
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export function setJWT(cookies: Cookies, user: User) {
|
||||
const payload = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name
|
||||
};
|
||||
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET not defined');
|
||||
}
|
||||
if (process.env.BASE_URL === undefined) {
|
||||
throw new Error('BASE_URL not defined');
|
||||
}
|
||||
|
||||
const secure: boolean = process.env.BASE_URL?.includes('https://');
|
||||
|
||||
const maxAge = 60 * 60 * 24 * 30; // 30 days
|
||||
const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' });
|
||||
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false, secure });
|
||||
}
|
||||
|
||||
// export function checkPerms(cookies: Cookies, perms: number): void {
|
||||
// const JWT = cookies.get('jwt');
|
||||
// if (!JWT) throw error(403, 'Unauthorized');
|
||||
// if (process.env.JWT_SECRET === undefined) {
|
||||
// throw new Error('JWT_SECRET not defined');
|
||||
// }
|
||||
// const user = jwt.verify(JWT, process.env.JWT_SECRET) as User;
|
||||
// if ((user.perms & perms) !== perms) {
|
||||
// throw error(403, 'Unauthorized');
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// export function hasPerms(cookies: Cookies, perms: number): boolean {
|
||||
// const JWT = cookies.get('jwt');
|
||||
// if (!JWT) return false;
|
||||
// if (process.env.JWT_SECRET === undefined) {
|
||||
// throw new Error('JWT_SECRET not defined');
|
||||
// }
|
||||
// const user = jwt.verify(JWT, process.env.JWT_SECRET) as User;
|
||||
// return (user.perms & perms) === perms;
|
||||
// }
|
||||
|
||||
export async function login(email: string, password: string): Promise<User> {
|
||||
try {
|
||||
const [user] = await sql`
|
||||
SELECT id, email, password_hash, perms, name
|
||||
WHERE email = ${email};
|
||||
`;
|
||||
|
||||
if (await bcrypt.compare(password, user.password_hash)) {
|
||||
delete user.password_hash;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
sql`
|
||||
UPDATE users
|
||||
SET last_signin = NOW()
|
||||
WHERE id = ${user.id};
|
||||
`;
|
||||
|
||||
return <User>user;
|
||||
}
|
||||
} catch {
|
||||
throw Error('Error signing in ');
|
||||
}
|
||||
throw Error('Invalid email or password');
|
||||
}
|
||||
|
||||
// await createUser(<User>{
|
||||
// email: 'drake@marinodev.com',
|
||||
// password: 'password',
|
||||
// perms: 255,
|
||||
// name: 'Drake'
|
||||
// });
|
||||
@@ -0,0 +1,9 @@
|
||||
export const PERMISSIONS = {
|
||||
VIEW: 0b00000001,
|
||||
DO_TASKS: 0b00000010,
|
||||
MANAGE_USERS: 0b00000100,
|
||||
MANAGE_ITEMS: 0b00001000,
|
||||
MANAGE_TASKS: 0b00010000
|
||||
};
|
||||
|
||||
export const EXPIRE_REMINDER_DAYS = 30;
|
||||
@@ -0,0 +1,14 @@
|
||||
import postgres from 'postgres';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({ path: '.env' });
|
||||
|
||||
const sql = postgres({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT!),
|
||||
database: process.env.POSTGRES_DB,
|
||||
username: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD
|
||||
});
|
||||
|
||||
export default sql;
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { User } from '$lib/types';
|
||||
import sql from '$lib/db/db.server';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
// should require MANAGE_USERS permission
|
||||
|
||||
export async function getUsers(searchQuery: string | null = null): Promise<User[]> {
|
||||
return sql`
|
||||
SELECT id,
|
||||
email,
|
||||
perms,
|
||||
name,
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn"
|
||||
FROM users
|
||||
WHERE (${!searchQuery ? sql`TRUE` : sql`email ILIKE ${'%' + searchQuery + '%'} OR name ILIKE ${'%' + searchQuery + '%'}`});
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getUser(id: number): Promise<User> {
|
||||
return <User>(
|
||||
await sql`
|
||||
SELECT id,
|
||||
email,
|
||||
perms,
|
||||
name,
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn"
|
||||
FROM users
|
||||
WHERE (id = ${id}) LIMIT 1;
|
||||
`
|
||||
)[0];
|
||||
}
|
||||
|
||||
export async function createUser(user: User) {
|
||||
const passwordHash = await bcrypt.hash(user.password!, 12);
|
||||
await sql`
|
||||
INSERT INTO users (email, password_hash, perms, name, created_at, last_signin)
|
||||
VALUES (${user.email}, ${passwordHash}, ${user.perms}, ${user.name}, NOW(), NOW())
|
||||
RETURNING id;
|
||||
`;
|
||||
}
|
||||
|
||||
export async function editUser(user: User) {
|
||||
let passwordHash: string | undefined;
|
||||
if (user.password) {
|
||||
passwordHash = await bcrypt.hash(user.password, 12);
|
||||
}
|
||||
|
||||
await sql`
|
||||
UPDATE users
|
||||
SET
|
||||
email = ${user.email},
|
||||
${passwordHash ? sql`password_hash = ${passwordHash},` : sql``}
|
||||
perms = ${user.perms},
|
||||
name = ${user.name}
|
||||
WHERE id = ${user.id!}
|
||||
RETURNING id;
|
||||
`;
|
||||
}
|
||||
|
||||
export async function deleteUser(id: number) {
|
||||
await sql`
|
||||
DELETE FROM users
|
||||
WHERE id = ${id}
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
phone: string;
|
||||
password?: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
lastSignIn: Date;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: number;
|
||||
ownerEmail: string;
|
||||
ownerPhone: string;
|
||||
foundDate: Date;
|
||||
approvedDate?: Date;
|
||||
claimedDate?: Date;
|
||||
title: string;
|
||||
description: string;
|
||||
transferred: boolean; // to L&F location
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
export const getCookieValue = (name: string): string =>
|
||||
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
|
||||
|
||||
export const getUserFromJWT = (jwt: string): User => JSON.parse(atob(jwt.split('.')[1]));
|
||||
|
||||
// export const userData = () => {
|
||||
// let userData: User | null = null;
|
||||
// const cookieValue = getCookieValue('jwt');
|
||||
// if (cookieValue) {
|
||||
// userData = getUserFromJWT(cookieValue);
|
||||
// }
|
||||
// return userData;
|
||||
// };
|
||||
|
||||
export function getFormString(data: FormData, key: string): string | undefined {
|
||||
const value = data.get(key);
|
||||
if (value === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
throw Error(`Incorrect input in field ${key}.`);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
export function getRequiredFormString(data: FormData, key: string) {
|
||||
const value = data.get(key);
|
||||
if (typeof value !== 'string') {
|
||||
throw Error(`Missing required field ${key}.`);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
export const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { getUserFromJWT } from '$lib/utils';
|
||||
|
||||
export const load: LayoutServerLoad = ({ cookies }) => {
|
||||
const jwt = cookies.get('jwt');
|
||||
if (jwt) {
|
||||
return { selfUser: getUserFromJWT(jwt) };
|
||||
}
|
||||
return { selfUser: null };
|
||||
};
|
||||
@@ -1,12 +1,29 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..40,400,0,0&display=block&icon_names=account_circle,arrow_drop_down,arrow_drop_up,calendar_today,call,check,close,cloud_upload,dark_mode,delete,description,edit,group,home,info,light_mode,login,mail,menu,open_in_new,person,search,sell,store,upload,visibility,visibility_off,work"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
{@render children?.()}
|
||||
<!--<header class="bottom-border flex justify-between">-->
|
||||
<!-- <a class="material-symbols-outlined p-2 !text-3xl leading-none" href="/">home</a>-->
|
||||
<!-- {#if data.selfUser}-->
|
||||
<!-- <a class="material-symbols-outlined p-2 !text-3xl leading-none" href="/account"-->
|
||||
<!-- >account_circle</a-->
|
||||
<!-- >-->
|
||||
<!-- {:else}-->
|
||||
<!-- <a class="material-symbols-outlined p-2 !text-3xl leading-none" href="/signin">login</a>-->
|
||||
<!-- {/if}-->
|
||||
<!--</header>-->
|
||||
|
||||
<div style="max-width: 800px;" class="center">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
|
||||
</footer>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user