Skip to content

Commit 841ea5a

Browse files
feat(auth): implement complete authentication system
1 parent 6bfbe60 commit 841ea5a

34 files changed

Lines changed: 1769 additions & 168 deletions

apps/api/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,22 @@
2929
},
3030
"dependencies": {
3131
"@nestjs/common": "^10.0.0",
32+
"@nestjs/config": "^4.0.4",
3233
"@nestjs/core": "^10.0.0",
34+
"@nestjs/jwt": "^11.0.2",
35+
"@nestjs/passport": "^11.0.5",
3336
"@nestjs/platform-express": "^10.0.0",
3437
"@nestjs/swagger": "^7.0.0",
35-
"@prisma/client": "^5.22.0",
38+
"@nestjs/throttler": "^6.5.0",
39+
"@prisma/client": "^7.8.0",
3640
"@renewly/constants": "workspace:*",
3741
"@renewly/types": "workspace:*",
3842
"@renewly/validation": "workspace:*",
43+
"argon2": "^0.44.0",
44+
"cookie-parser": "^1.4.7",
45+
"helmet": "^8.2.0",
46+
"passport": "^0.7.0",
47+
"passport-jwt": "^4.0.1",
3948
"reflect-metadata": "^0.2.0",
4049
"rxjs": "^7.8.1",
4150
"zod": "^3.22.4"
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
-- CreateEnum
2+
CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN');
3+
4+
-- CreateEnum
5+
CREATE TYPE "Currency" AS ENUM ('USD', 'EUR', 'GBP', 'INR', 'CAD', 'AUD');
6+
7+
-- CreateEnum
8+
CREATE TYPE "BillingCycle" AS ENUM ('MONTHLY', 'YEARLY', 'QUARTERLY', 'WEEKLY', 'LIFETIME');
9+
10+
-- CreateEnum
11+
CREATE TYPE "SubscriptionStatus" AS ENUM ('ACTIVE', 'CANCELED', 'PAUSED', 'PAST_DUE');
12+
13+
-- CreateEnum
14+
CREATE TYPE "ReminderType" AS ENUM ('EMAIL', 'PUSH', 'SMS', 'IN_APP');
15+
16+
-- CreateEnum
17+
CREATE TYPE "PaymentMethodType" AS ENUM ('CREDIT_CARD', 'PAYPAL', 'BANK_TRANSFER', 'CRYPTO');
18+
19+
-- CreateEnum
20+
CREATE TYPE "ProviderType" AS ENUM ('ENTERTAINMENT', 'SOFTWARE', 'UTILITY', 'GAMING', 'OTHER');
21+
22+
-- CreateTable
23+
CREATE TABLE "User" (
24+
"id" TEXT NOT NULL,
25+
"email" TEXT NOT NULL,
26+
"name" TEXT,
27+
"role" "UserRole" NOT NULL DEFAULT 'USER',
28+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
29+
"updatedAt" TIMESTAMP(3) NOT NULL,
30+
"deletedAt" TIMESTAMP(3),
31+
32+
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
33+
);
34+
35+
-- CreateTable
36+
CREATE TABLE "Category" (
37+
"id" TEXT NOT NULL,
38+
"name" TEXT NOT NULL,
39+
"slug" TEXT NOT NULL,
40+
"description" TEXT,
41+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
42+
"updatedAt" TIMESTAMP(3) NOT NULL,
43+
44+
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
45+
);
46+
47+
-- CreateTable
48+
CREATE TABLE "Provider" (
49+
"id" TEXT NOT NULL,
50+
"name" TEXT NOT NULL,
51+
"slug" TEXT NOT NULL,
52+
"type" "ProviderType" NOT NULL DEFAULT 'OTHER',
53+
"categoryId" TEXT NOT NULL,
54+
"iconUrl" TEXT,
55+
"websiteUrl" TEXT,
56+
"billingUrl" TEXT,
57+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
58+
"updatedAt" TIMESTAMP(3) NOT NULL,
59+
60+
CONSTRAINT "Provider_pkey" PRIMARY KEY ("id")
61+
);
62+
63+
-- CreateTable
64+
CREATE TABLE "PaymentMethod" (
65+
"id" TEXT NOT NULL,
66+
"userId" TEXT NOT NULL,
67+
"type" "PaymentMethodType" NOT NULL,
68+
"provider" TEXT,
69+
"lastFour" TEXT,
70+
"expiryMonth" INTEGER,
71+
"expiryYear" INTEGER,
72+
"isDefault" BOOLEAN NOT NULL DEFAULT false,
73+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
74+
"updatedAt" TIMESTAMP(3) NOT NULL,
75+
"deletedAt" TIMESTAMP(3),
76+
77+
CONSTRAINT "PaymentMethod_pkey" PRIMARY KEY ("id")
78+
);
79+
80+
-- CreateTable
81+
CREATE TABLE "Subscription" (
82+
"id" TEXT NOT NULL,
83+
"userId" TEXT NOT NULL,
84+
"providerId" TEXT NOT NULL,
85+
"categoryId" TEXT,
86+
"paymentMethodId" TEXT,
87+
"name" TEXT NOT NULL,
88+
"amount" DECIMAL(10,2) NOT NULL,
89+
"currency" "Currency" NOT NULL DEFAULT 'USD',
90+
"cycle" "BillingCycle" NOT NULL DEFAULT 'MONTHLY',
91+
"status" "SubscriptionStatus" NOT NULL DEFAULT 'ACTIVE',
92+
"startDate" TIMESTAMP(3) NOT NULL,
93+
"nextBillingDate" TIMESTAMP(3),
94+
"cancelDate" TIMESTAMP(3),
95+
"notes" TEXT,
96+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
97+
"updatedAt" TIMESTAMP(3) NOT NULL,
98+
"deletedAt" TIMESTAMP(3),
99+
100+
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
101+
);
102+
103+
-- CreateTable
104+
CREATE TABLE "Purchase" (
105+
"id" TEXT NOT NULL,
106+
"userId" TEXT NOT NULL,
107+
"providerId" TEXT NOT NULL,
108+
"categoryId" TEXT,
109+
"name" TEXT NOT NULL,
110+
"amount" DECIMAL(10,2) NOT NULL,
111+
"currency" "Currency" NOT NULL DEFAULT 'USD',
112+
"purchaseDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
113+
"notes" TEXT,
114+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
115+
"updatedAt" TIMESTAMP(3) NOT NULL,
116+
"deletedAt" TIMESTAMP(3),
117+
118+
CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id")
119+
);
120+
121+
-- CreateTable
122+
CREATE TABLE "Reminder" (
123+
"id" TEXT NOT NULL,
124+
"userId" TEXT NOT NULL,
125+
"subscriptionId" TEXT NOT NULL,
126+
"type" "ReminderType" NOT NULL DEFAULT 'EMAIL',
127+
"daysBefore" INTEGER NOT NULL DEFAULT 3,
128+
"isActive" BOOLEAN NOT NULL DEFAULT true,
129+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
130+
"updatedAt" TIMESTAMP(3) NOT NULL,
131+
132+
CONSTRAINT "Reminder_pkey" PRIMARY KEY ("id")
133+
);
134+
135+
-- CreateIndex
136+
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
137+
138+
-- CreateIndex
139+
CREATE INDEX "User_deletedAt_idx" ON "User"("deletedAt");
140+
141+
-- CreateIndex
142+
CREATE INDEX "User_createdAt_idx" ON "User"("createdAt");
143+
144+
-- CreateIndex
145+
CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
146+
147+
-- CreateIndex
148+
CREATE UNIQUE INDEX "Category_slug_key" ON "Category"("slug");
149+
150+
-- CreateIndex
151+
CREATE UNIQUE INDEX "Provider_slug_key" ON "Provider"("slug");
152+
153+
-- CreateIndex
154+
CREATE INDEX "Provider_categoryId_idx" ON "Provider"("categoryId");
155+
156+
-- CreateIndex
157+
CREATE INDEX "PaymentMethod_userId_idx" ON "PaymentMethod"("userId");
158+
159+
-- CreateIndex
160+
CREATE INDEX "PaymentMethod_deletedAt_idx" ON "PaymentMethod"("deletedAt");
161+
162+
-- CreateIndex
163+
CREATE INDEX "Subscription_userId_idx" ON "Subscription"("userId");
164+
165+
-- CreateIndex
166+
CREATE INDEX "Subscription_providerId_idx" ON "Subscription"("providerId");
167+
168+
-- CreateIndex
169+
CREATE INDEX "Subscription_categoryId_idx" ON "Subscription"("categoryId");
170+
171+
-- CreateIndex
172+
CREATE INDEX "Subscription_nextBillingDate_idx" ON "Subscription"("nextBillingDate");
173+
174+
-- CreateIndex
175+
CREATE INDEX "Subscription_status_idx" ON "Subscription"("status");
176+
177+
-- CreateIndex
178+
CREATE INDEX "Subscription_deletedAt_idx" ON "Subscription"("deletedAt");
179+
180+
-- CreateIndex
181+
CREATE INDEX "Purchase_userId_idx" ON "Purchase"("userId");
182+
183+
-- CreateIndex
184+
CREATE INDEX "Purchase_providerId_idx" ON "Purchase"("providerId");
185+
186+
-- CreateIndex
187+
CREATE INDEX "Purchase_categoryId_idx" ON "Purchase"("categoryId");
188+
189+
-- CreateIndex
190+
CREATE INDEX "Purchase_deletedAt_idx" ON "Purchase"("deletedAt");
191+
192+
-- CreateIndex
193+
CREATE INDEX "Reminder_userId_idx" ON "Reminder"("userId");
194+
195+
-- CreateIndex
196+
CREATE INDEX "Reminder_subscriptionId_idx" ON "Reminder"("subscriptionId");
197+
198+
-- AddForeignKey
199+
ALTER TABLE "Provider" ADD CONSTRAINT "Provider_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
200+
201+
-- AddForeignKey
202+
ALTER TABLE "PaymentMethod" ADD CONSTRAINT "PaymentMethod_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
203+
204+
-- AddForeignKey
205+
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
206+
207+
-- AddForeignKey
208+
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
209+
210+
-- AddForeignKey
211+
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
212+
213+
-- AddForeignKey
214+
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_paymentMethodId_fkey" FOREIGN KEY ("paymentMethodId") REFERENCES "PaymentMethod"("id") ON DELETE SET NULL ON UPDATE CASCADE;
215+
216+
-- AddForeignKey
217+
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
218+
219+
-- AddForeignKey
220+
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
221+
222+
-- AddForeignKey
223+
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
224+
225+
-- AddForeignKey
226+
ALTER TABLE "Reminder" ADD CONSTRAINT "Reminder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
227+
228+
-- AddForeignKey
229+
ALTER TABLE "Reminder" ADD CONSTRAINT "Reminder_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "Subscription"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Please do not edit this file manually
2+
# It should be added in your version-control system (i.e. Git)
3+
provider = "postgresql"

apps/api/prisma/schema.prisma

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,19 @@ enum ProviderType {
5959
}
6060

6161
model User {
62-
id String @id @default(uuid())
63-
email String @unique
64-
name String?
65-
role UserRole @default(USER)
66-
createdAt DateTime @default(now())
67-
updatedAt DateTime @updatedAt
68-
deletedAt DateTime?
62+
id String @id @default(uuid())
63+
email String @unique
64+
name String?
65+
passwordHash String
66+
refreshTokenHash String?
67+
emailVerified Boolean @default(false)
68+
emailVerificationToken String? @unique
69+
passwordResetToken String? @unique
70+
passwordResetExpires DateTime?
71+
role UserRole @default(USER)
72+
createdAt DateTime @default(now())
73+
updatedAt DateTime @updatedAt
74+
deletedAt DateTime?
6975
7076
subscriptions Subscription[]
7177
purchases Purchase[]

apps/api/src/app.module.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
11
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
3+
import { APP_GUARD } from '@nestjs/core';
4+
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
25
import { AppController } from './app.controller';
36
import { AppService } from './app.service';
7+
import { PrismaModule } from './prisma/prisma.module';
8+
import { UsersModule } from './modules/users/users.module';
9+
import { AuthModule } from './modules/auth/auth.module';
10+
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
411

512
@Module({
6-
imports: [],
13+
imports: [
14+
ConfigModule.forRoot({ isGlobal: true }),
15+
ThrottlerModule.forRoot([{
16+
ttl: 60000,
17+
limit: 100,
18+
}]),
19+
PrismaModule,
20+
UsersModule,
21+
AuthModule,
22+
],
723
controllers: [AppController],
8-
providers: [AppService],
24+
providers: [
25+
AppService,
26+
{
27+
provide: APP_GUARD,
28+
useClass: JwtAuthGuard,
29+
},
30+
{
31+
provide: APP_GUARD,
32+
useClass: ThrottlerGuard,
33+
},
34+
],
935
})
1036
export class AppModule {}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { PipeTransform, ArgumentMetadata, BadRequestException, Injectable } from '@nestjs/common';
2+
import { ZodSchema } from 'zod';
3+
4+
@Injectable()
5+
export class ZodValidationPipe implements PipeTransform {
6+
constructor(private schema: ZodSchema<any>) {}
7+
8+
transform(value: any, metadata: ArgumentMetadata) {
9+
if (metadata.type !== 'body' && metadata.type !== 'query' && metadata.type !== 'param') {
10+
return value;
11+
}
12+
13+
try {
14+
const parsedValue = this.schema.parse(value);
15+
return parsedValue;
16+
} catch (error: any) {
17+
throw new BadRequestException({
18+
message: 'Validation failed',
19+
errors: error.errors,
20+
});
21+
}
22+
}
23+
}

apps/api/src/main.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
import { NestFactory } from '@nestjs/core';
22
import { AppModule } from './app.module';
3+
import helmet from 'helmet';
4+
import * as cookieParser from 'cookie-parser';
35

46
async function bootstrap() {
57
const app = await NestFactory.create(AppModule);
8+
9+
app.use(helmet());
10+
app.use(cookieParser());
11+
12+
app.enableCors({
13+
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
14+
credentials: true,
15+
});
16+
617
await app.listen(process.env.PORT ?? 3000);
718
}
819
bootstrap();

0 commit comments

Comments
 (0)