From bfeba9b231a362e54e5de844ed6edfe957d65b67 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sun, 6 Oct 2024 22:50:23 -0400 Subject: [PATCH 1/2] added bruno endpoints for auth --- bruno/Auth/2fa-disable.bru | 18 ++++++++++++++++++ bruno/Auth/2fa-enable.bru | 17 +++++++++++++++++ bruno/Auth/2fa-request.bru | 17 +++++++++++++++++ bruno/Auth/change-password.bru | 18 ++++++++++++++++++ bruno/Auth/login.bru | 13 ++++++++++--- bruno/Auth/logout.bru | 11 +++++++++++ bruno/Auth/reset-password-request.bru | 17 +++++++++++++++++ bruno/Auth/reset-password.bru | 19 +++++++++++++++++++ bruno/Auth/signup.bru | 13 ++++++++++--- bruno/Auth/verify-email-request.bru | 11 +++++++++++ bruno/Auth/verify-email.bru | 9 ++++----- bruno/bruno.json | 18 +++++++++++------- 12 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 bruno/Auth/2fa-disable.bru create mode 100644 bruno/Auth/2fa-enable.bru create mode 100644 bruno/Auth/2fa-request.bru create mode 100644 bruno/Auth/change-password.bru create mode 100644 bruno/Auth/logout.bru create mode 100644 bruno/Auth/reset-password-request.bru create mode 100644 bruno/Auth/reset-password.bru create mode 100644 bruno/Auth/verify-email-request.bru diff --git a/bruno/Auth/2fa-disable.bru b/bruno/Auth/2fa-disable.bru new file mode 100644 index 0000000..c98539c --- /dev/null +++ b/bruno/Auth/2fa-disable.bru @@ -0,0 +1,18 @@ +meta { + name: 2fa-disable + type: http + seq: 6 +} + +post { + url: http://localhost:3000/api/v1/auth/2fa/disable + body: json + auth: none +} + +body:json { + { + "password": "aaaaa-1A", + "code": "377289" + } +} diff --git a/bruno/Auth/2fa-enable.bru b/bruno/Auth/2fa-enable.bru new file mode 100644 index 0000000..a3a01d1 --- /dev/null +++ b/bruno/Auth/2fa-enable.bru @@ -0,0 +1,17 @@ +meta { + name: 2fa-enable + type: http + seq: 4 +} + +post { + url: http://localhost:3000/api/v1/auth/2fa/enable + body: json + auth: none +} + +body:json { + { + "code": "374138" + } +} diff --git a/bruno/Auth/2fa-request.bru b/bruno/Auth/2fa-request.bru new file mode 100644 index 0000000..fcf0c98 --- /dev/null +++ b/bruno/Auth/2fa-request.bru @@ -0,0 +1,17 @@ +meta { + name: 2fa-request + type: http + seq: 5 +} + +post { + url: http://localhost:3000/api/v1/auth/2fa/request + body: json + auth: none +} + +body:json { + { + "password": "aaaaa-1A" + } +} diff --git a/bruno/Auth/change-password.bru b/bruno/Auth/change-password.bru new file mode 100644 index 0000000..7d1c707 --- /dev/null +++ b/bruno/Auth/change-password.bru @@ -0,0 +1,18 @@ +meta { + name: change-password + type: http + seq: 9 +} + +post { + url: http://localhost:3000/api/v1/auth/change-password + body: json + auth: none +} + +body:json { + { + "oldPassword": "", + "newPassword": "" + } +} diff --git a/bruno/Auth/login.bru b/bruno/Auth/login.bru index 25e6406..ac90083 100644 --- a/bruno/Auth/login.bru +++ b/bruno/Auth/login.bru @@ -4,8 +4,15 @@ meta { seq: 1 } -get { - url: - body: none +post { + url: http://localhost:3000/api/v1/auth/login + body: json auth: none } + +body:json { + { + "email": "milo@fossorial.io", + "password": "Password123!" + } +} diff --git a/bruno/Auth/logout.bru b/bruno/Auth/logout.bru new file mode 100644 index 0000000..7dd134c --- /dev/null +++ b/bruno/Auth/logout.bru @@ -0,0 +1,11 @@ +meta { + name: logout + type: http + seq: 3 +} + +post { + url: http://localhost:3000/api/v1/auth/logout + body: none + auth: none +} diff --git a/bruno/Auth/reset-password-request.bru b/bruno/Auth/reset-password-request.bru new file mode 100644 index 0000000..9d4bfc7 --- /dev/null +++ b/bruno/Auth/reset-password-request.bru @@ -0,0 +1,17 @@ +meta { + name: reset-password-request + type: http + seq: 10 +} + +post { + url: http://localhost:3000/api/v1/auth/reset-password/request + body: json + auth: none +} + +body:json { + { + "email": "milo@fossorial.io" + } +} diff --git a/bruno/Auth/reset-password.bru b/bruno/Auth/reset-password.bru new file mode 100644 index 0000000..8d567b1 --- /dev/null +++ b/bruno/Auth/reset-password.bru @@ -0,0 +1,19 @@ +meta { + name: reset-password + type: http + seq: 11 +} + +post { + url: http://localhost:3000/api/v1/auth/reset-password + body: json + auth: none +} + +body:json { + { + "token": "3uhsbom72dwdhboctwrtntyd6jrlg4jtf5oaxy4k", + "newPassword": "aaaaa-1A", + "code": "6irqCGR3" + } +} diff --git a/bruno/Auth/signup.bru b/bruno/Auth/signup.bru index 7937fad..84592ee 100644 --- a/bruno/Auth/signup.bru +++ b/bruno/Auth/signup.bru @@ -4,8 +4,15 @@ meta { seq: 2 } -get { - url: - body: none +put { + url: http://localhost:3000/api/v1/auth/signup + body: json auth: none } + +body:json { + { + "email": "milo@fossorial.io", + "password": "Password123!" + } +} diff --git a/bruno/Auth/verify-email-request.bru b/bruno/Auth/verify-email-request.bru new file mode 100644 index 0000000..72189d1 --- /dev/null +++ b/bruno/Auth/verify-email-request.bru @@ -0,0 +1,11 @@ +meta { + name: verify-email-request + type: http + seq: 8 +} + +post { + url: http://localhost:3000/api/v1/auth/verify-email/request + body: none + auth: none +} diff --git a/bruno/Auth/verify-email.bru b/bruno/Auth/verify-email.bru index 5a6163b..a06a710 100644 --- a/bruno/Auth/verify-email.bru +++ b/bruno/Auth/verify-email.bru @@ -1,18 +1,17 @@ meta { name: verify-email type: http - seq: 3 + seq: 7 } -put { - url: http://localhost:3000/api/v1/auth/signup +post { + url: http://localhost:3000/api/v1/auth/verify-email body: json auth: none } body:json { { - "email": "owen@fossorial.io", - "password": "Password123!" + "code": "50317187" } } diff --git a/bruno/bruno.json b/bruno/bruno.json index ba37321..f19d936 100644 --- a/bruno/bruno.json +++ b/bruno/bruno.json @@ -1,9 +1,13 @@ { - "version": "1", - "name": "Pangolin", - "type": "collection", - "ignore": [ - "node_modules", - ".git" - ] + "version": "1", + "name": "Pangolin", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ], + "presets": { + "requestType": "http", + "requestUrl": "http://localhost:3000/api/v1" + } } \ No newline at end of file From 6fb569e2cde2a41f7c3913d4a712d18e93eb235c Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Mon, 7 Oct 2024 23:31:23 -0400 Subject: [PATCH 2/2] check for stale users on signup --- package.json | 3 ++- server/auth/index.ts | 5 +++-- server/db/schema.ts | 15 ++++++++++---- server/routers/auth/signup.ts | 39 +++++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a06f640..c4e8c65 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "http-errors": "2.0.0", "lucia": "3.2.0", "lucide-react": "0.447.0", + "moment": "2.30.1", "next": "14.2.13", "node-fetch": "3.3.2", "nodemailer": "6.9.15", @@ -76,4 +77,4 @@ "tsx": "4.19.1", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/server/auth/index.ts b/server/auth/index.ts index 244ebee..3fa0c7f 100644 --- a/server/auth/index.ts +++ b/server/auth/index.ts @@ -5,7 +5,6 @@ import { Lucia, TimeSpan } from "lucia"; import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle"; import db from "@server/db"; import { sessions, users } from "@server/db/schema"; -import environment from "@server/environment"; const adapter = new DrizzleSQLiteAdapter(db, sessions, users); @@ -16,6 +15,7 @@ export const lucia = new Lucia(adapter, { twoFactorEnabled: attributes.twoFactorEnabled, twoFactorSecret: attributes.twoFactorSecret, emailVerified: attributes.emailVerified, + dateCreated: attributes.dateCreated, }; }, // getSessionAttributes: (attributes) => { @@ -30,7 +30,7 @@ export const lucia = new Lucia(adapter, { // secure: environment.ENVIRONMENT === "prod", // sameSite: "strict", secure: false, - domain: ".testing123.io" + domain: ".testing123.io", }, }, sessionExpiresIn: new TimeSpan(2, "w"), @@ -52,6 +52,7 @@ interface DatabaseUserAttributes { twoFactorEnabled: boolean; twoFactorSecret?: string; emailVerified: boolean; + dateCreated: string; } interface DatabaseSessionAttributes { diff --git a/server/db/schema.ts b/server/db/schema.ts index 440832f..c2ca3d8 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -81,6 +81,7 @@ export const users = sqliteTable("user", { emailVerified: integer("emailVerified", { mode: "boolean" }) .notNull() .default(false), + dateCreated: text("dateCreated").notNull(), }); export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", { @@ -107,7 +108,9 @@ export const userOrgs = sqliteTable("userOrgs", { orgId: integer("orgId") .notNull() .references(() => orgs.orgId), - roleId: integer("roleId").notNull().references(() => roles.roleId), + roleId: integer("roleId") + .notNull() + .references(() => roles.roleId), }); export const emailVerificationCodes = sqliteTable("emailVerificationCodes", { @@ -137,7 +140,9 @@ export const actions = sqliteTable("actions", { export const roles = sqliteTable("roles", { roleId: integer("roleId").primaryKey({ autoIncrement: true }), - orgId: integer("orgId").references(() => orgs.orgId, { onDelete: "cascade" }), + orgId: integer("orgId").references(() => orgs.orgId, { + onDelete: "cascade", + }), name: text("name").notNull(), description: text("description"), }); @@ -204,7 +209,9 @@ export const userResources = sqliteTable("userResources", { export const limitsTable = sqliteTable("limits", { limitId: integer("limitId").primaryKey({ autoIncrement: true }), - orgId: integer("orgId").references(() => orgs.orgId, { onDelete: "cascade" }), + orgId: integer("orgId").references(() => orgs.orgId, { + onDelete: "cascade", + }), name: text("name").notNull(), value: integer("value").notNull(), description: text("description"), @@ -232,4 +239,4 @@ export type RoleSite = InferSelectModel; export type UserSite = InferSelectModel; export type RoleResource = InferSelectModel; export type UserResource = InferSelectModel; -export type Limit = InferSelectModel; \ No newline at end of file +export type Limit = InferSelectModel; diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index dfd5abf..4344a16 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -12,6 +12,8 @@ import response from "@server/utils/response"; import { SqliteError } from "better-sqlite3"; import { sendEmailVerificationCode } from "./sendEmailVerificationCode"; import { passwordSchema } from "@server/auth/passwordSchema"; +import { eq } from "drizzle-orm"; +import moment from "moment"; export const signupBodySchema = z.object({ email: z.string().email(), @@ -51,10 +53,47 @@ export async function signup( const userId = generateId(15); try { + const existing = await db + .select() + .from(users) + .where(eq(users.email, email)); + + if (existing && existing.length > 0) { + const user = existing[0]; + + // If the user is already verified, we don't want to create a new user + if (user.emailVerified) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "A user with that email address already exists", + ), + ); + } + + const dateCreated = moment(user.dateCreated); + const now = moment(); + const diff = now.diff(dateCreated, "hours"); + + if (diff < 2) { + // If the user was created less than 2 hours ago, we don't want to create a new user + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "A verification email was already sent to this email address. Please check your email for the verification code.", + ), + ); + } else { + // If the user was created more than 2 hours ago, we want to delete the old user and create a new one + await db.delete(users).where(eq(users.id, user.id)); + } + } + await db.insert(users).values({ id: userId, email: email, passwordHash, + dateCreated: moment().toISOString(), }); const session = await lucia.createSession(userId, {});