Login and default team
This commit is contained in:
parent
ec674a073e
commit
52a1eefb24
|
@ -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
2
db.ts
|
@ -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>(
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
Loading…
Reference in a new issue