Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"editor.defaultFormatter": "Prisma.prisma",
"editor.formatOnSave": true
},
"cSpell.words": ["bitnami", "zipcode"]
"cSpell.words": ["bitnami", "fkey", "pkey", "zipcode"]
}
47 changes: 47 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"engines": {
"node": "20"
},
"volta": {
"node": "20.19.4"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
Expand All @@ -27,6 +30,7 @@
"@commitlint/types": "^19.8.0",
"@faker-js/faker": "^9.8.0",
"@types/node": "^22.13.9",
"@types/nodemailer": "^6.4.17",
"conventional-changelog-atom": "^5.0.0",
"prisma": "^6.4.1",
"tsup": "^8.4.0",
Expand All @@ -39,8 +43,11 @@
"@fastify/jwt": "^9.1.0",
"@prisma/client": "^6.4.1",
"bcryptjs": "^3.0.2",
"dayjs": "^1.11.13",
"dotenv": "^16.5.0",
"fastify": "^5.3.2",
"nanoid": "^5.1.5",
"nodemailer": "^7.0.5",
"zod": "^3.24.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "auth_codes" (
"id" TEXT NOT NULL,
"code" INTEGER NOT NULL,
"user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "auth_codes_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "auth_codes_code_key" ON "auth_codes"("code");

-- AddForeignKey
ALTER TABLE "auth_codes" ADD CONSTRAINT "auth_codes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
19 changes: 16 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ datasource db {
url = env("DATABASE_URL")
}

model AuthCodes {
id String @id @default(cuid())
code Int @unique

user User @relation(fields: [userId], references: [id])
userId String @map("user_id")

createdAt DateTime @default(now()) @map("created_at")

@@map("auth_codes")
}

model Address {
id String @id @default(cuid())
number Int
Expand Down Expand Up @@ -45,9 +57,10 @@ model User {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

Address Address?
Order Order[]
Cart Cart?
AuthCodes AuthCodes[]
Address Address?
Order Order[]
Cart Cart?

@@map("users")
}
Expand Down
3 changes: 3 additions & 0 deletions src/env/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import 'dotenv/config'
import { z } from 'zod'

const envSchema = z.object({
DATABASE_URL: z.string(),
PORT: z.coerce.number().min(1000).max(9999).default(3333),
NODE_ENV: z.enum(['dev', 'test', 'prod']).default('dev'),
JWT_SECRET: z.string(),
GMAIL_USER: z.string().email(),
GMAIL_PASS: z.string(),
})

const _env = envSchema.safeParse(process.env)
Expand Down
32 changes: 32 additions & 0 deletions src/http/controllers/users/resetPassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { FastifyReply, FastifyRequest } from 'fastify'
import { z } from 'zod'

import { InvalidCredentialsError } from '@/services/errors/invalid-credentials-error'
import { makeResetPasswordService } from '@/services/factories/users/make-reset-password-service'

export async function resetPassword(
request: FastifyRequest,
reply: FastifyReply,
) {
const resetPasswordBodySchema = z.object({
code: z.number().int().min(100000).max(999999),
password: z.string().min(6),
userId: z.string().cuid(),
})

const { code, password, userId } = resetPasswordBodySchema.parse(request.body)

try {
const resetPasswordService = makeResetPasswordService()

await resetPasswordService.execute({ code, password, userId })
} catch (err) {
if (err instanceof InvalidCredentialsError) {
return reply.status(404).send({ message: err.message })
}

throw err
}

return reply.status(204).send()
}
4 changes: 4 additions & 0 deletions src/http/controllers/users/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { logout } from './logout'
import { refresh } from './refresh'
import { register } from './register'
import { remove } from './remove'
import { resetPassword } from './resetPassword'
import { sendCode } from './sendCode'
import { update } from './update'

export async function usersRoutes(app: FastifyInstance) {
Expand All @@ -22,6 +24,8 @@ export async function usersRoutes(app: FastifyInstance) {
app.post('/auth/login', authenticate)
app.patch('/auth/refresh', refresh)
app.delete('/auth/logout', { onRequest: [verifyJWT] }, logout)
app.post('/auth/send-code', sendCode)
app.post('/auth/reset-password', resetPassword)

app.get('/users', { onRequest: [verifyUserRole('ADMIN')] }, list)
}
36 changes: 36 additions & 0 deletions src/http/controllers/users/sendCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { FastifyReply, FastifyRequest } from 'fastify'
import { z } from 'zod'

import { CodeGeneratedRecentlyError } from '@/services/errors/code-generated-recently-error'
import { UnableToSendEmailError } from '@/services/errors/unable-to-send-email-error'
import { UserNotExistsError } from '@/services/errors/user-not-exists-error'
import { makeSendCodeService } from '@/services/factories/users/make-send-code-service'

export async function sendCode(request: FastifyRequest, reply: FastifyReply) {
const sendCodeBodySchema = z.object({
email: z.string().email(),
})

const { email } = sendCodeBodySchema.parse(request.body)

try {
const sendCodeService = makeSendCodeService()
const emailSent = await sendCodeService.execute({ email })

return reply.status(200).send(emailSent)
} catch (err) {
if (err instanceof UserNotExistsError) {
return reply.status(404).send({ message: err.message })
}

if (err instanceof CodeGeneratedRecentlyError) {
return reply.status(429).send({ message: err.message })
}

if (err instanceof UnableToSendEmailError) {
return reply.status(503).send({ message: err.message })
}

throw err
}
}
17 changes: 17 additions & 0 deletions src/lib/nanoid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { customAlphabet } from 'nanoid'

const defaultAlphabet = '123456789'
const defaultLength = 6

interface generateAuthCodeParams {
alphabet?: string
length?: number
}

export function generateAuthCode({
alphabet = defaultAlphabet,
length = defaultLength,
}: generateAuthCodeParams) {
const code = customAlphabet(alphabet, length)()
return Number.parseInt(code)
}
28 changes: 28 additions & 0 deletions src/lib/nodemailer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { env } from '@/env'
import { type SendMailOptions, createTransport } from 'nodemailer'

const transporter = createTransport({
service: 'gmail',
port: 587,
secure: false,
auth: {
user: env.GMAIL_USER,
pass: env.GMAIL_PASS,
},
})

transporter.verify()

export async function sendMail(options: SendMailOptions) {
const mailOptions = {
from: `Pizza Stars <${env.GMAIL_USER}>`,
to: options.to,
subject: options.subject,
text: options.text,
html: options.html,
...options,
}

const email = await transporter.sendMail(mailOptions)
return email
}
9 changes: 9 additions & 0 deletions src/repositories/auth-codes-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { AuthCodes, Prisma } from '@prisma/client'

export interface AuthCodesRepository {
create(data: Prisma.AuthCodesCreateInput): Promise<AuthCodes>
list(): Promise<AuthCodes[]>
getByCodeAndUser(userId: string, code: number): Promise<AuthCodes | null>
getLastCodeByUser(userId: string): Promise<AuthCodes | null>
deleteByCode(code: number): Promise<AuthCodes>
}
58 changes: 58 additions & 0 deletions src/repositories/prisma/prisma-auth-codes-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { prisma } from '@/lib/prisma'
import type { Prisma } from '@prisma/client'

export class PrismaAuthCodesRepository {
async create(data: Prisma.AuthCodesCreateInput) {
const authCode = await prisma.authCodes.create({
data,
})

return authCode
}

async list() {
const authCodes = await prisma.authCodes.findMany({
orderBy: {
createdAt: 'desc',
},
})

return authCodes
}

async getByCodeAndUser(userId: string, code: number) {
const authCode = await prisma.authCodes.findFirst({
where: {
userId,
AND: {
code,
},
},
})

return authCode
}

async getLastCodeByUser(userId: string) {
const authCode = await prisma.authCodes.findFirst({
where: {
userId,
},
orderBy: {
createdAt: 'desc',
},
})

return authCode
}

async deleteByCode(code: number) {
const authCode = await prisma.authCodes.delete({
where: {
code,
},
})

return authCode
}
}
5 changes: 5 additions & 0 deletions src/services/errors/code-generated-recently-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class CodeGeneratedRecentlyError extends Error {
constructor() {
super('Code generated recently.')
}
}
5 changes: 5 additions & 0 deletions src/services/errors/unable-to-send-email-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class UnableToSendEmailError extends Error {
constructor() {
super('Unable to send the email')
}
}
Loading