From f0755615aee9da602774f9665f82bb4395b586c9 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Fri, 7 Oct 2022 17:09:13 -0500 Subject: [PATCH] Working on auth... --- .gitignore | 1 + db/migrations.ts | 50 +++++++++++++++++++++++++++++++++------------ mail/mod.ts | 39 +++++++++++++++++++++++++++++++++++ routes/login.tsx | 6 +++--- routes/register.tsx | 17 +++++++++++---- 5 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 .gitignore create mode 100644 mail/mod.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d3a24e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/config.json diff --git a/db/migrations.ts b/db/migrations.ts index 107d4e5..7a76bfb 100644 --- a/db/migrations.ts +++ b/db/migrations.ts @@ -9,10 +9,10 @@ interface TableSpec { prepStatements?: string[]; } -const timestamps = [ - "created_at timestamptz not null default now()", - "updated_at timestamptz not null default now()", -]; +const createdAtTimestamp = "created_at timestamptz not null default now()"; +const updatedAtTimestamp = "updated_at timestamptz not null default now()"; + +const timestamps = [createdAtTimestamp, updatedAtTimestamp]; const tables: Record = { "note": { @@ -26,15 +26,30 @@ const tables: Record = { columns: [ id, "username text not null unique", - "hashed_password text not null", + "password_digest text not null", "status user_status not null", "display_name text", ...timestamps, ], + additionalTableStatements: [ + "constraint valid_username check (username ~* '^[a-z\d\\-_]{2,38}$')", + ], }, "user_token": { + prepStatements: [ + "drop type if exists user_token_type", + "create type user_token_type as enum ('session', 'reset')", + ], columns: [ id, + "token_digest bytea not null unique", + "type user_token_type not null", + "sent_to text not null", + createdAtTimestamp, + ], + additionalStatements: [ + "create index team_user_type on user_token (type)", + "create index team_user_sent_to on user_token (sent_to)", ], }, "team": { @@ -94,8 +109,20 @@ create extension if not exists "uuid-ossp"; ${createTables} +commit; +`; + +const setupResult = await query(queryString); + +console.debug(setupResult); +console.log(queryString); + +const seedQuery = ` + +-- TODO: create reserved usernames? + with new_user as ( - insert into "user" (username, hashed_password, status) + insert into "user" (username, password_digest, status) values ('lytedev', '$2a$10$9fyDAOz6H4a393KHyjbvIe1WFxbhCJhq/CZmlXcEg4d1bE9Ey25WW', 'superadmin') returning id as user_id ), new_team as ( @@ -108,14 +135,11 @@ insert into "team_user" (user_id, team_id, status) (select user_id from new_user), (select team_id from new_team), 'owner' - ); + ) returning user_id; -commit; `; -console.log(queryString); +const seedResult = await query(seedQuery); -const result = await query(queryString); - -console.debug(result); -console.log(queryString); +console.debug(seedResult); +console.log(seedQuery); diff --git a/mail/mod.ts b/mail/mod.ts new file mode 100644 index 0000000..aacfa07 --- /dev/null +++ b/mail/mod.ts @@ -0,0 +1,39 @@ +import Mailgun from "https://deno.land/x/mailgun@v1.1.1/index.ts"; +import { type Message } from "https://deno.land/x/mailgun@v1.1.1/types.ts"; + +export type Email = Message; + +const MAILGUN_API_KEY = Deno.env.get("MAILGUN_API_KEY"); +const MAILGUN_DOMAIN = Deno.env.get("MAILGUN_DOMAIN"); + +if (!MAILGUN_API_KEY) { + console.error( + "MAILGUN_API_KEY not set. Emails will not be sent, only logged to the console.", + ); +} + +if (!MAILGUN_DOMAIN) { + console.error( + "MAILGUN_API_KEY not set. Emails will not be sent, only logged to the console.", + ); +} + +const mailgun = new Mailgun({ + key: "YOUR_API_KEY", + region: "us", + domain: "YOUR_DOMAIN", +}); + +export async function send(email: Email) { + console.debug("Email:", email); + if (MAILGUN_API_KEY && MAILGUN_DOMAIN) { + return await mailgun.send(email); + } else { + return new Response( + "Email client is not properly configured. Email not sent.", + { + status: 401, + }, + ); + } +} diff --git a/routes/login.tsx b/routes/login.tsx index ff71f0a..a397319 100644 --- a/routes/login.tsx +++ b/routes/login.tsx @@ -26,7 +26,7 @@ export const handler: Handlers = { } const result = await query< - { id: string; username: string; hashed_password: string } + { id: string; username: string; password_digest: string } >( `select * from "user" where username = $1`, [username], @@ -34,8 +34,8 @@ export const handler: Handlers = { if (result == null || result.rows.length < 1) { return await invalidLogin(context); } - const { rows: [{ id, hashed_password }] } = result; - if (await compare(password.toString(), hashed_password)) { + const { rows: [{ id, password_digest }] } = result; + if (await compare(password.toString(), password_digest)) { return await context.render(id); } else { return await invalidLogin(context); diff --git a/routes/register.tsx b/routes/register.tsx index 1bb88ee..a2966b2 100644 --- a/routes/register.tsx +++ b/routes/register.tsx @@ -21,13 +21,13 @@ export const handler: Handlers = { if (!password) { return await context.render({ message: "no password provided" }); } - const hashed_password = await hash(password.toString()); + const password_digest = await hash(password.toString()); try { const result = await query<{ user_id: string }>( ` with new_user as ( - insert into "user" (username, hashed_password, status) - values ($username, $hashed_password, 'unverified') + insert into "user" (username, password_digest, status) + values ($username, $password_digest, 'unverified') returning id as user_id ), new_team as ( insert into "team" (display_name) @@ -41,13 +41,14 @@ export const handler: Handlers = { 'owner' ) returning user_id `, - { username, hashed_password, team_name: `${username}'s First Team` }, + { username, password_digest, team_name: `${username}'s First Team` }, ); console.debug(result); if (!result) throw "insert failed"; const { rows: [{ user_id: id }] } = result; return await context.render(id); } catch (err) { + console.log("Error fields:", { ...err }); if ( err instanceof PostgresError && err.fields.code == "23505" && err.fields.constraint == "user_username_key" @@ -55,6 +56,14 @@ export const handler: Handlers = { return await context.render({ message: `A user with username '${username}' already exists`, }); + } else if ( + err instanceof PostgresError && err.fields.code == "23514" && + err.fields.constraint == "valid_username" + ) { + return await context.render({ + message: + `Username must ONLY be comprised of letters, number, dashes, and underscores`, + }); } throw err; }