Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TAS-119/rewrite-auth #68

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,17 @@
"dev:db": "bun -b kysely",
"dev": "bun run --watch src/index.ts",
"dev:email": "email dev",
"dev:auth:generate": "bunx -b @better-auth/cli generate",
"dev:auth:migrate": "bunx -b @better-auth/cli migrate",
"lint": "eslint src/** && tsc",
"lint:fix": "eslint src/** --fix && tsc"
},
"dependencies": {
"@better-auth/cli": "^0.8.7-beta.3",
"@faker-js/faker": "^9.0.3",
"@grpc/grpc-js": "^1.12.2",
"@grpc/proto-loader": "^0.7.13",
"@hono/zod-validator": "^0.4.1",
"@react-email/components": "0.0.25",
"better-auth": "^1.1.10",
"@types/bcrypt": "^5.0.2",
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"hono": "^4.6.3",
"kysely": "^0.27.4",
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const apiConfigSchema = z.object({
}),
services: z.object({
auth: z.object({
jwtSecret: z.string(),
totp: z.object({
issuer: z.string(),
}),
Expand Down Expand Up @@ -39,6 +40,7 @@ const data = {
},
services: {
auth: {
jwtSecret: "its-a-secret",
totp: {
issuer: "a-little-byte",
},
Expand Down
21 changes: 21 additions & 0 deletions apps/api/src/database/migrations/1737488390410_auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Database } from "@alittlebyte/api/database/types"
import { baseTable } from "@alittlebyte/api/database/utils/baseTable"
import { notNullColumn } from "@alittlebyte/api/database/utils/notNullColumn"
import type { Kysely } from "kysely"

export const up = async (db: Kysely<Database>): Promise<void> => {
await db.schema
.createTable("users")
.$call(baseTable)
.$call(notNullColumn("firstName"))
.$call(notNullColumn("lastName"))
.addColumn("email", "text", (col) => col.notNull().unique())
.$call(notNullColumn("passwordHash"))
.$call(notNullColumn("passwordSalt"))
.addColumn("emailVerifiedAt", "timestamptz")
.execute()
}

export const down = async (db: Kysely<Database>): Promise<void> => {
await db.schema.dropTable("users").execute()
}
26 changes: 24 additions & 2 deletions apps/api/src/database/repositories/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { db } from "@alittlebyte/api/database"
import { NewUser, User, UserUpdate } from "@alittlebyte/api/database/types"
import { UUID } from "node:crypto"

const findAll = async (criteria: Partial<User>) => {
const findAll = (criteria: Partial<User>) => {
let query = db.selectFrom("users")

if (criteria.id) {
Expand All @@ -21,7 +21,28 @@ const findAll = async (criteria: Partial<User>) => {
query = query.where("createdAt", "=", criteria.createdAt)
}

return await query.selectAll().execute()
return query.selectAll().execute()
}
const findOne = (criteria: Partial<User>) => {
let query = db.selectFrom("users")

if (criteria.id) {
query = query.where("id", "=", criteria.id)
}

if (criteria.firstName) {
query = query.where("firstName", "=", criteria.firstName)
}

if (criteria.lastName) {
query = query.where("lastName", "=", criteria.lastName)
}

if (criteria.createdAt) {
query = query.where("createdAt", "=", criteria.createdAt)
}

return query.selectAll().executeTakeFirst()
}
const findById = (id: UUID) =>
db.selectFrom("users").where("id", "=", id).selectAll().executeTakeFirst()
Expand All @@ -47,4 +68,5 @@ export const usersRepository = {
update,
updateReturn,
delete: deleteUser,
findOne,
} as const
17 changes: 2 additions & 15 deletions apps/api/src/database/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,10 @@ import type { UUID } from "node:crypto"

export type Database = {
accounts: AccountTable
"accounts.user": UserTable
creditCards: CreditCardTable
"creditCards.user": UserTable
services: ServiceTable
translations: TranslationTable
sessions: SessionTable
twoFactors: TwoFactorTable
"twoFactors.user": UserTable
users: UserTable
"users.accounts": AccountTable
"users.creditCards": CreditCardTable
"users.sessions": SessionTable
"users.twoFactors": TwoFactorTable
verifications: VerificationTable
}

export type ServiceTable = {
Expand Down Expand Up @@ -74,14 +64,11 @@ export type CreditCardUpdate = Updateable<CreditCardTable>

export type UserTable = {
id: Generated<UUID>
name: string
firstName: string
lastName: string
email: string
emailVerified: boolean
image?: string
twoFactorEnabled?: boolean
creditCards: CreditCard[]
passwordHash: string
passwordSalt: string
createdAt: ColumnType<Date, string | undefined, never>
updatedAt: ColumnType<Date, never, string | Date>
}
Expand Down
9 changes: 7 additions & 2 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { apiConfig } from "@alittlebyte/api/config"
import { authMiddleware } from "@alittlebyte/api/middlewares/auth"
import { corsMiddleware } from "@alittlebyte/api/middlewares/cors"
import { dbMiddleware } from "@alittlebyte/api/middlewares/db"
import { authRouter } from "@alittlebyte/api/routes/auth"
Expand All @@ -10,6 +9,7 @@ import { usersRouter } from "@alittlebyte/api/routes/users"
import type { PublicContextVariables } from "@alittlebyte/api/utils/types"
import { HTTP_STATUS_CODES } from "@alittlebyte/common/constants"
import { Hono } from "hono"
import { jwt } from "hono/jwt"

const { port } = apiConfig.server
const app = new Hono<{ Variables: PublicContextVariables }>()
Expand All @@ -26,7 +26,12 @@ const router = app
.route("/auth", authRouter)
.route("/services", servicesRouter)
.route("/example", backofficeExample)
.use(authMiddleware)
.use(
jwt({
secret: apiConfig.services.auth.jwtSecret,
alg: "RS512",
}),
)
.route("/users", usersRouter)
.route("/checkout", checkoutRouter)

Expand Down
84 changes: 0 additions & 84 deletions apps/api/src/lib/auth.ts

This file was deleted.

19 changes: 0 additions & 19 deletions apps/api/src/middlewares/auth.ts

This file was deleted.

93 changes: 89 additions & 4 deletions apps/api/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,91 @@
import { auth } from "@alittlebyte/api/lib/auth"
import { apiConfig } from "@alittlebyte/api/config"
import {
hashPassword,
validatePassword,
} from "@alittlebyte/api/utils/hashPassword"
import { PrivateContextVariables } from "@alittlebyte/api/utils/types"
import {
emailValidator,
firstNameValidator,
lastNameValidator,
passwordValidator,
} from "@alittlebyte/common/validators"
import { zValidator } from "@hono/zod-validator"
import { Hono } from "hono"
import { sign } from "hono/jwt"
import { UUID } from "node:crypto"
import { z } from "zod"

export const authRouter = new Hono().on(["POST", "GET"], "/**", (c) =>
auth.handler(c.req.raw),
)
const createToken = (id: UUID, email: string) =>
sign({ id, email }, apiConfig.services.auth.jwtSecret)

export const authRouter = new Hono<{ Variables: PrivateContextVariables }>()
.post(
"/sign-in",
zValidator(
"json",
z.object({
email: emailValidator,
password: passwordValidator,
}),
),
async ({ req, var: { repositories }, json }) => {
const { email, password } = req.valid("json")
const user = await repositories.users.findOne({ email })

if (!user) {
return json({}, 404)
}

const matches = await validatePassword(password, user.passwordHash)

if (!matches) {
throw new Error("Invalid credentials")
}

const token = await createToken(user.id, user.email)

return json({ token }, 201)
},
)
.post(
"/sign-up",
zValidator(
"json",
z.object({
email: emailValidator,
password: passwordValidator,
firstName: firstNameValidator,
lastName: lastNameValidator,
}),
),
async ({ req, var: { repositories }, json }) => {
const { password, email, ...body } = req.valid("json")

try {
const usersExists = await repositories.users.findOne({ email })

if (usersExists) {
throw new Error("An error occurred during signup")
}

const hashedPassword = await hashPassword(password)
const user = await repositories.users.create({
email,
...body,
...hashedPassword,
})

return json(user, 201)
} catch (error) {
return json(
{
message: error,
},
401,
)
}
},
)
.post("/forgot-password")
.post("/reset-password")
7 changes: 7 additions & 0 deletions apps/api/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const AUTH_CONSTANTS = {
SALT_ROUNDS: 12,
TOKEN_EXPIRY: "30 days",
MIN_PASSWORD_LENGTH: 8,
MAX_USERNAME_LENGTH: 30,
DEFAULT_SKIN_NAME: "default",
} as const
15 changes: 15 additions & 0 deletions apps/api/src/utils/hashPassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import bcrypt from "bcrypt"

export const hashPassword = async (password: string) => {
const salt = await bcrypt.genSalt(12)

return {
passwordSalt: salt,
passwordHash: await bcrypt.hash(password, salt),
}
}

export const validatePassword = (
plainPassword: string,
hashedPassword: string,
): Promise<boolean> => bcrypt.compare(plainPassword, hashedPassword)
Loading