From 52a1eefb2415db46a56d9df6f3701552c9e4d9e5 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Wed, 5 Oct 2022 17:02:21 -0500 Subject: [PATCH] Login and default team --- db-migrations.ts | 69 ++++++++++++++++++++++++++++++++++++------- db.ts | 2 +- deno.json | 2 +- routes/login.tsx | 72 +++++++++++++++++++++++++++++++++++++++++++-- routes/note.tsx | 8 ++--- routes/register.tsx | 25 +++++++++++++--- 6 files changed, 154 insertions(+), 24 deletions(-) diff --git a/db-migrations.ts b/db-migrations.ts index 09f5cc9..10a2e18 100644 --- a/db-migrations.ts +++ b/db-migrations.ts @@ -4,6 +4,7 @@ const id = "id uuid primary key default uuid_generate_v4()"; interface TableSpec { columns: string[]; + additionalTableStatements?: string[]; additionalStatements?: string[]; prepStatements?: string[]; } @@ -18,22 +19,32 @@ const tables: Record = { columns: [id, "content text not null", ...timestamps], }, "user": { + prepStatements: [ + "drop type if exists user_status", + "create type user_status as enum ('unverified', 'verified', 'owner', 'superadmin')", + ], columns: [ id, "username text not null unique", "hashed_password text not null", - "name text", + "status user_status not null", + "display_name text", ...timestamps, ], }, + "user_token": { + columns: [ + id, + ], + }, "team": { columns: [ id, - "name text not null", + "display_name text not null", ...timestamps, ], additionalStatements: [ - 'create index name_idx on team ("name")', + 'create index display_name_idx on team ("display_name")', ], }, "team_user": { @@ -44,9 +55,13 @@ const tables: Record = { columns: [ "team_id uuid", "user_id uuid", - "status team_user_status", + "status team_user_status not null", ...timestamps, ], + additionalTableStatements: [ + 'constraint fk_team foreign key(team_id) references "team"(id) on delete cascade', + 'constraint fk_user foreign key(user_id) references "user"(id) on delete cascade', + ], additionalStatements: [ "create index team_user_idx on team_user (team_id) include (user_id)", "create index team_idx on team_user (team_id)", @@ -56,19 +71,51 @@ const tables: Record = { }, }; -const tableStatements = Object.entries(tables).map(([name, meta]) => ` +const dropTables = Object.entries(tables).reverse().map(([name, _meta]) => + `drop table if exists "${name}";` +).join("\n"); + +const createTables = Object.entries(tables).map(([name, meta]) => ` + +-- CREATE TABLE ${name} ${(meta.prepStatements || []).map((s) => `${s};`).join("\n")} --- TABLE ${name} -drop table if exists "${name}"; create table "${name}" ( - ${meta.columns.join(",\n ")} + ${meta.columns.concat(meta.additionalTableStatements || []).join(",\n ")} ); ${(meta.additionalStatements || []).map((s) => `${s};`).join("\n")} -`); +`).map((s) => s.trim()).join("\n\n"); const queryString = ` +begin; + +${dropTables} + create extension if not exists "uuid-ossp"; -${tableStatements.map((s) => s.trim()).join("\n\n")} + +${createTables} + +with new_user as ( + insert into "user" (username, hashed_password, status) + values ('lytedev', '$2a$10$9fyDAOz6H4a393KHyjbvIe1WFxbhCJhq/CZmlXcEg4d1bE9Ey25WW', 'superadmin') + returning id as user_id +), new_team as ( + insert into "team" (display_name) + values ('superadmins') + returning id as team_id +) +insert into "team_user" (user_id, team_id, status) + values ( + (select user_id from new_user), + (select team_id from new_team), + 'owner' + ); + +commit; `; + +console.log(queryString); + +const result = await query(queryString); + +console.debug(result); console.log(queryString); -await query(queryString); diff --git a/db.ts b/db.ts index 349ec70..96cbbd9 100644 --- a/db.ts +++ b/db.ts @@ -11,7 +11,7 @@ export { PostgresError }; export { type QueryObjectResult }; const databaseUrl = Deno.env.get("DATABASE_URL") || - "postgresql://danielflanagan:@127.0.0.1:5432/lyricscreen"; + "postgresql://postgres:@127.0.0.1:5432/lyricscreen"; const pool = new Pool(databaseUrl, 3, true); export async function query( diff --git a/deno.json b/deno.json index 8d9ac33..b3fe28b 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "start": "deno run -A --watch=static/,routes/ dev.ts" + "start": "deno run -A --watch=. dev.ts" }, "importMap": "./import_map.json", "compilerOptions": { diff --git a/routes/login.tsx b/routes/login.tsx index 8053eaa..742603a 100644 --- a/routes/login.tsx +++ b/routes/login.tsx @@ -1,8 +1,76 @@ -import { Page } from "../components/Page.tsx"; +import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts"; +import { compare } from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; +import { Page } from "@/components/Page.tsx"; +import { query } from "../db.ts"; -export default function Login() { +type UserID = string; + +interface LoginError { + message: string; +} + +async function invalidLogin(context: HandlerContext) { + return await context.render({ message: "Invalid login" } as LoginError); +} + +export const handler: Handlers = { + async POST(request: Request, context) { + const formData = (await request.formData()); + const username = formData.get("username"); + const password = formData.get("password"); + if (!username) { + return await context.render({ message: "no username provided" }); + } + if (!password) { + return await context.render({ message: "no password provided" }); + } + + const result = await query< + { id: string; username: string; hashed_password: string } + >( + `select * from "user" where username = $1`, + [username], + ); + if (result == null || result.rows.length < 1) { + return await invalidLogin(context); + } + const { rows: [{ id, hashed_password }] } = result; + if (await compare(password.toString(), hashed_password)) { + return await context.render(id); + } else { + return await invalidLogin(context); + } + }, +}; + +export default function Login({ data }: PageProps) { + if (typeof data == "string") { + return LoginSuccessful(data); + } else { + return LoginForm(data); + } +} + +function LoginSuccessful(_userId: UserID) { return ( +

+ You are now logged in. Let's go to your{" "} + dashboard! +

+
+ ); +} + +function LoginForm(props?: LoginError | null) { + return ( + + {props != null && + ( +

+ Error: {props.message} +

+ )}

Log in to your account

diff --git a/routes/note.tsx b/routes/note.tsx index d5f43dc..43e9b76 100644 --- a/routes/note.tsx +++ b/routes/note.tsx @@ -1,6 +1,6 @@ import { Handlers, PageProps } from "$fresh/server.ts"; -import { query } from "../db.ts"; -import { Page } from "../components/Page.tsx"; +import { query } from "@/db.ts"; +import { Page } from "@/components/Page.tsx"; interface Note { id: string; @@ -9,12 +9,10 @@ interface Note { } export const handler: Handlers = { - async GET(request, context) { - console.debug({ request, context }); + async GET(_request, context) { const result = await query("select * from note order by created_at desc"); if (result == null) throw "unable to fetch from database"; const notes = result.rows as Note[]; - console.debug(notes); return await context.render(notes); }, }; diff --git a/routes/register.tsx b/routes/register.tsx index e25ceac..688a725 100644 --- a/routes/register.tsx +++ b/routes/register.tsx @@ -14,6 +14,7 @@ export const handler: Handlers = { const formData = (await request.formData()); const username = formData.get("username"); const password = formData.get("password"); + // TODO: verify that username conforms to some regex? no spaces? if (!username) { return await context.render({ message: "no username provided" }); } @@ -22,13 +23,29 @@ export const handler: Handlers = { } const hashed_password = await hash(password.toString()); try { - const result = await query<{ id: string }>( - `insert into "user" (username, hashed_password) values ($1, $2) returning id`, - [username, hashed_password], + const result = await query<{ user_id: string }>( + ` + with new_user as ( + insert into "user" (username, hashed_password, status) + values ($username, $hashed_password, 'unverified') + returning id as user_id + ), new_team as ( + insert into "team" (display_name) + values ($team_name) + returning id as team_id + ) + insert into "team_user" (user_id, team_id, status) + values ( + (select user_id from new_user), + (select team_id from new_team), + 'owner' + ) returning user_id + `, + { username, hashed_password, team_name: `${username}'s First Team` }, ); console.debug(result); if (!result) throw "insert failed"; - const { rows: [{ id }] } = result; + const { rows: [{ user_id: id }] } = result; return await context.render(id); } catch (err) { if (