Commit a1e1b9d4 authored by ThinhNC's avatar ThinhNC

feat: get ip & userAgent + validation

parent 8d3bead5
NODE_ENV=development NODE_ENV=development
PORT=3000 PORT=3000
DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-1-[region].pooler.supabase.com:6543/postgres DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-1-[region].pooler.supabase.com:6543/postgres?pgbouncer=true
DIRECT_URL=postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres DIRECT_URL=postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres
DB_HOST=aws-1-[region].pooler.supabase.com DB_HOST=aws-1-[region].pooler.supabase.com
DB_PORT=6543 DB_PORT=6543
......
-- AlterTable
ALTER TABLE "refresh_tokens" ADD COLUMN "ip_address" TEXT,
ADD COLUMN "user_agent" TEXT;
-- CreateIndex
CREATE INDEX "applications_approved_by_idx" ON "applications"("approved_by");
-- CreateIndex
CREATE INDEX "daily_reports_intern_id_idx" ON "daily_reports"("intern_id");
-- CreateIndex
CREATE INDEX "intern_profiles_leader_id_idx" ON "intern_profiles"("leader_id");
-- CreateIndex
CREATE INDEX "notification_logs_notification_id_idx" ON "notification_logs"("notification_id");
-- CreateIndex
CREATE INDEX "notifications_user_id_idx" ON "notifications"("user_id");
-- CreateIndex
CREATE INDEX "refresh_tokens_user_id_idx" ON "refresh_tokens"("user_id");
-- CreateIndex
CREATE INDEX "task_assignments_intern_id_idx" ON "task_assignments"("intern_id");
-- CreateIndex
CREATE INDEX "task_assignments_assigned_by_idx" ON "task_assignments"("assigned_by");
-- CreateIndex
CREATE INDEX "task_submissions_assignment_id_idx" ON "task_submissions"("assignment_id");
-- CreateIndex
CREATE INDEX "task_submissions_reviewed_by_idx" ON "task_submissions"("reviewed_by");
-- CreateIndex
CREATE INDEX "tasks_created_by_idx" ON "tasks"("created_by");
-- CreateIndex
CREATE INDEX "users_role_id_idx" ON "users"("role_id");
-- CreateIndex
CREATE INDEX "weekly_evaluations_leader_id_idx" ON "weekly_evaluations"("leader_id");
...@@ -47,6 +47,7 @@ model User { ...@@ -47,6 +47,7 @@ model User {
notifications Notification[] notifications Notification[]
refreshTokens RefreshToken[] refreshTokens RefreshToken[]
@@index([roleId])
@@map("users") @@map("users")
} }
...@@ -74,6 +75,7 @@ model Application { ...@@ -74,6 +75,7 @@ model Application {
approver User? @relation("ApprovedApplications", fields: [approvedBy], references: [id], onDelete: SetNull) approver User? @relation("ApprovedApplications", fields: [approvedBy], references: [id], onDelete: SetNull)
@@index([approvedBy])
@@map("applications") @@map("applications")
} }
...@@ -108,6 +110,7 @@ model InternProfile { ...@@ -108,6 +110,7 @@ model InternProfile {
dailyReports DailyReport[] dailyReports DailyReport[]
weeklyEvaluations WeeklyEvaluation[] weeklyEvaluations WeeklyEvaluation[]
@@index([leaderId])
@@map("intern_profiles") @@map("intern_profiles")
} }
...@@ -146,6 +149,7 @@ model Task { ...@@ -146,6 +149,7 @@ model Task {
creator User @relation("CreatedTasks", fields: [createdBy], references: [id], onDelete: Cascade) creator User @relation("CreatedTasks", fields: [createdBy], references: [id], onDelete: Cascade)
assignment TaskAssignment? assignment TaskAssignment?
@@index([createdBy])
@@map("tasks") @@map("tasks")
} }
...@@ -171,6 +175,8 @@ model TaskAssignment { ...@@ -171,6 +175,8 @@ model TaskAssignment {
assigner User @relation("AssignedTasks", fields: [assignedBy], references: [id], onDelete: Cascade) assigner User @relation("AssignedTasks", fields: [assignedBy], references: [id], onDelete: Cascade)
submissions TaskSubmission[] submissions TaskSubmission[]
@@index([internId])
@@index([assignedBy])
@@map("task_assignments") @@map("task_assignments")
} }
...@@ -198,6 +204,8 @@ model TaskSubmission { ...@@ -198,6 +204,8 @@ model TaskSubmission {
assignment TaskAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade) assignment TaskAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
reviewer User? @relation("ReviewedSubmissions", fields: [reviewedBy], references: [id], onDelete: SetNull) reviewer User? @relation("ReviewedSubmissions", fields: [reviewedBy], references: [id], onDelete: SetNull)
@@index([assignmentId])
@@index([reviewedBy])
@@map("task_submissions") @@map("task_submissions")
} }
...@@ -213,6 +221,7 @@ model DailyReport { ...@@ -213,6 +221,7 @@ model DailyReport {
intern InternProfile @relation(fields: [internId], references: [id], onDelete: Cascade) intern InternProfile @relation(fields: [internId], references: [id], onDelete: Cascade)
@@index([internId])
@@map("daily_reports") @@map("daily_reports")
} }
...@@ -235,6 +244,7 @@ model WeeklyEvaluation { ...@@ -235,6 +244,7 @@ model WeeklyEvaluation {
leader User @relation("LeaderEvaluations", fields: [leaderId], references: [id], onDelete: Cascade) leader User @relation("LeaderEvaluations", fields: [leaderId], references: [id], onDelete: Cascade)
@@unique([internId, week]) @@unique([internId, week])
@@index([leaderId])
@@map("weekly_evaluations") @@map("weekly_evaluations")
} }
...@@ -252,6 +262,7 @@ model Notification { ...@@ -252,6 +262,7 @@ model Notification {
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
logs NotificationLog[] logs NotificationLog[]
@@index([userId])
@@map("notifications") @@map("notifications")
} }
...@@ -276,6 +287,7 @@ model NotificationLog { ...@@ -276,6 +287,7 @@ model NotificationLog {
notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade) notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade)
@@index([notificationId])
@@map("notification_logs") @@map("notification_logs")
} }
...@@ -284,10 +296,13 @@ model RefreshToken { ...@@ -284,10 +296,13 @@ model RefreshToken {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
token String @unique token String @unique
userId String @db.Uuid @map("user_id") userId String @db.Uuid @map("user_id")
userAgent String? @map("user_agent")
ipAddress String? @map("ip_address")
expiresAt DateTime @map("expires_at") expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("refresh_tokens") @@map("refresh_tokens")
} }
...@@ -7,6 +7,8 @@ import routes from './routes'; ...@@ -7,6 +7,8 @@ import routes from './routes';
const app = express(); const app = express();
app.set('trust proxy', true);
app.use(helmet()); app.use(helmet());
app.use(cors()); app.use(cors());
app.use(morgan('dev')); app.use(morgan('dev'));
......
...@@ -7,7 +7,9 @@ export class AuthController { ...@@ -7,7 +7,9 @@ export class AuthController {
login = async (req: Request, res: Response, next: NextFunction) => { login = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const { email, password } = req.body; const { email, password } = req.body;
const result = await this.service.login(email, password); const userAgent = req.headers['user-agent'];
const ipAddress = req.ip;
const result = await this.service.login(email, password, { userAgent, ipAddress });
res.json({ res.json({
success: true, success: true,
...@@ -34,7 +36,9 @@ export class AuthController { ...@@ -34,7 +36,9 @@ export class AuthController {
refresh = async (req: Request, res: Response, next: NextFunction) => { refresh = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const { refreshToken } = req.body; const { refreshToken } = req.body;
const result = await this.service.refresh(refreshToken); const userAgent = req.headers['user-agent'];
const ipAddress = req.ip;
const result = await this.service.refresh(refreshToken, { userAgent, ipAddress });
res.json({ res.json({
success: true, success: true,
......
...@@ -15,12 +15,14 @@ export class AuthRepository { ...@@ -15,12 +15,14 @@ export class AuthRepository {
}); });
} }
async saveRefreshToken(userId: string, token: string, expiresAt: Date) { async saveRefreshToken(userId: string, token: string, expiresAt: Date, userAgent?: string, ipAddress?: string) {
return prisma.refreshToken.create({ return prisma.refreshToken.create({
data: { data: {
userId, userId,
token, token,
expiresAt, expiresAt,
userAgent,
ipAddress,
}, },
}); });
} }
......
...@@ -8,7 +8,7 @@ import { jwtConfig } from '../../config/jwt.config'; ...@@ -8,7 +8,7 @@ import { jwtConfig } from '../../config/jwt.config';
export class AuthService { export class AuthService {
private readonly repository = new AuthRepository(); private readonly repository = new AuthRepository();
async login(email: string, password: string) { async login(email: string, password: string, metadata?: { userAgent?: string; ipAddress?: string }) {
const user = await this.repository.findByEmail(email); const user = await this.repository.findByEmail(email);
if (!user) { if (!user) {
...@@ -37,7 +37,7 @@ export class AuthService { ...@@ -37,7 +37,7 @@ export class AuthService {
const decoded = jwt.decode(refreshToken) as { exp: number }; const decoded = jwt.decode(refreshToken) as { exp: number };
const expiresAt = new Date(decoded.exp * 1000); const expiresAt = new Date(decoded.exp * 1000);
await this.repository.saveRefreshToken(user.id, refreshToken, expiresAt); await this.repository.saveRefreshToken(user.id, refreshToken, expiresAt, metadata?.userAgent, metadata?.ipAddress);
return { return {
accessToken, accessToken,
...@@ -50,7 +50,7 @@ export class AuthService { ...@@ -50,7 +50,7 @@ export class AuthService {
}; };
} }
async refresh(token: string) { async refresh(token: string, metadata?: { userAgent?: string; ipAddress?: string }) {
let payload: any; let payload: any;
try { try {
payload = jwt.verify(token, jwtConfig.refreshSecret); payload = jwt.verify(token, jwtConfig.refreshSecret);
...@@ -87,7 +87,7 @@ export class AuthService { ...@@ -87,7 +87,7 @@ export class AuthService {
const decoded = jwt.decode(newRefreshToken) as { exp: number }; const decoded = jwt.decode(newRefreshToken) as { exp: number };
const expiresAt = new Date(decoded.exp * 1000); const expiresAt = new Date(decoded.exp * 1000);
await this.repository.saveRefreshToken(user.id, newRefreshToken, expiresAt); await this.repository.saveRefreshToken(user.id, newRefreshToken, expiresAt, metadata?.userAgent, metadata?.ipAddress);
return { return {
accessToken: newAccessToken, accessToken: newAccessToken,
...@@ -96,7 +96,11 @@ export class AuthService { ...@@ -96,7 +96,11 @@ export class AuthService {
} }
async logout(token: string) { async logout(token: string) {
await this.repository.deleteRefreshToken(token); const result = await this.repository.deleteRefreshToken(token);
if (result.count === 0) {
throw new AppError('Refresh token not found or already invalidated', 400, ERROR_CODE.TOKEN_INVALID);
}
} }
async getMe(userId: string) { async getMe(userId: string) {
......
import { z } from 'zod'; import { z } from 'zod';
export const loginSchema = z.object({ export const loginSchema = z.object({
email: z.string().email('Invalid email format'), email: z.string().min(1, 'Email is required').email('Invalid email format'),
password: z.string().min(1, 'Password is required'), password: z.string().min(1, 'Password is required'),
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment