Login and default team

This commit is contained in:
Daniel Flanagan 2022-10-05 17:02:21 -05:00
parent ec674a073e
commit 52a1eefb24
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
6 changed files with 154 additions and 24 deletions

View file

@ -4,6 +4,7 @@ const id = "id uuid primary key default uuid_generate_v4()";
interface TableSpec { interface TableSpec {
columns: string[]; columns: string[];
additionalTableStatements?: string[];
additionalStatements?: string[]; additionalStatements?: string[];
prepStatements?: string[]; prepStatements?: string[];
} }
@ -18,22 +19,32 @@ const tables: Record<string, TableSpec> = {
columns: [id, "content text not null", ...timestamps], columns: [id, "content text not null", ...timestamps],
}, },
"user": { "user": {
prepStatements: [
"drop type if exists user_status",
"create type user_status as enum ('unverified', 'verified', 'owner', 'superadmin')",
],
columns: [ columns: [
id, id,
"username text not null unique", "username text not null unique",
"hashed_password text not null", "hashed_password text not null",
"name text", "status user_status not null",
"display_name text",
...timestamps, ...timestamps,
], ],
}, },
"user_token": {
columns: [
id,
],
},
"team": { "team": {
columns: [ columns: [
id, id,
"name text not null", "display_name text not null",
...timestamps, ...timestamps,
], ],
additionalStatements: [ additionalStatements: [
'create index name_idx on team ("name")', 'create index display_name_idx on team ("display_name")',
], ],
}, },
"team_user": { "team_user": {
@ -44,9 +55,13 @@ const tables: Record<string, TableSpec> = {
columns: [ columns: [
"team_id uuid", "team_id uuid",
"user_id uuid", "user_id uuid",
"status team_user_status", "status team_user_status not null",
...timestamps, ...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: [ additionalStatements: [
"create index team_user_idx on team_user (team_id) include (user_id)", "create index team_user_idx on team_user (team_id) include (user_id)",
"create index team_idx on team_user (team_id)", "create index team_idx on team_user (team_id)",
@ -56,19 +71,51 @@ const tables: Record<string, TableSpec> = {
}, },
}; };
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")} ${(meta.prepStatements || []).map((s) => `${s};`).join("\n")}
-- TABLE ${name}
drop table if exists "${name}";
create table "${name}" ( create table "${name}" (
${meta.columns.join(",\n ")} ${meta.columns.concat(meta.additionalTableStatements || []).join(",\n ")}
); );
${(meta.additionalStatements || []).map((s) => `${s};`).join("\n")} ${(meta.additionalStatements || []).map((s) => `${s};`).join("\n")}
`); `).map((s) => s.trim()).join("\n\n");
const queryString = ` const queryString = `
begin;
${dropTables}
create extension if not exists "uuid-ossp"; 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); console.log(queryString);
await query(queryString);

2
db.ts
View file

@ -11,7 +11,7 @@ export { PostgresError };
export { type QueryObjectResult }; export { type QueryObjectResult };
const databaseUrl = Deno.env.get("DATABASE_URL") || 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); const pool = new Pool(databaseUrl, 3, true);
export async function query<T>( export async function query<T>(

View file

@ -1,6 +1,6 @@
{ {
"tasks": { "tasks": {
"start": "deno run -A --watch=static/,routes/ dev.ts" "start": "deno run -A --watch=. dev.ts"
}, },
"importMap": "./import_map.json", "importMap": "./import_map.json",
"compilerOptions": { "compilerOptions": {

View file

@ -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<LoginError>(context: HandlerContext<LoginError>) {
return await context.render({ message: "Invalid login" } as LoginError);
}
export const handler: Handlers<UserID | LoginError | null> = {
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 ( return (
<Page> <Page>
<p>
You are now logged in. Let's go to your{" "}
<a href="/dashboard">dashboard</a>!
</p>
</Page>
);
}
function LoginForm(props?: LoginError | null) {
return (
<Page>
{props != null &&
(
<p class="text-red-500">
<strong>Error</strong>: {props.message}
</p>
)}
<h1 class="text-4xl mb-4 outline-white"> <h1 class="text-4xl mb-4 outline-white">
Log in to your account Log in to your account
</h1> </h1>

View file

@ -1,6 +1,6 @@
import { Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
import { query } from "../db.ts"; import { query } from "@/db.ts";
import { Page } from "../components/Page.tsx"; import { Page } from "@/components/Page.tsx";
interface Note { interface Note {
id: string; id: string;
@ -9,12 +9,10 @@ interface Note {
} }
export const handler: Handlers<Note[]> = { export const handler: Handlers<Note[]> = {
async GET(request, context) { async GET(_request, context) {
console.debug({ request, context });
const result = await query("select * from note order by created_at desc"); const result = await query("select * from note order by created_at desc");
if (result == null) throw "unable to fetch from database"; if (result == null) throw "unable to fetch from database";
const notes = result.rows as Note[]; const notes = result.rows as Note[];
console.debug(notes);
return await context.render(notes); return await context.render(notes);
}, },
}; };

View file

@ -14,6 +14,7 @@ export const handler: Handlers<UserID | RegistrationError | null> = {
const formData = (await request.formData()); const formData = (await request.formData());
const username = formData.get("username"); const username = formData.get("username");
const password = formData.get("password"); const password = formData.get("password");
// TODO: verify that username conforms to some regex? no spaces?
if (!username) { if (!username) {
return await context.render({ message: "no username provided" }); return await context.render({ message: "no username provided" });
} }
@ -22,13 +23,29 @@ export const handler: Handlers<UserID | RegistrationError | null> = {
} }
const hashed_password = await hash(password.toString()); const hashed_password = await hash(password.toString());
try { try {
const result = await query<{ id: string }>( const result = await query<{ user_id: string }>(
`insert into "user" (username, hashed_password) values ($1, $2) returning id`, `
[username, hashed_password], 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); console.debug(result);
if (!result) throw "insert failed"; if (!result) throw "insert failed";
const { rows: [{ id }] } = result; const { rows: [{ user_id: id }] } = result;
return await context.render(id); return await context.render(id);
} catch (err) { } catch (err) {
if ( if (