Commit a1e1b9d4 authored by ThinhNC's avatar ThinhNC

feat: get ip & userAgent + validation

parent 8d3bead5
NODE_ENV=development
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
DB_HOST=aws-1-[region].pooler.supabase.com
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 {
notifications Notification[]
refreshTokens RefreshToken[]
@@index([roleId])
@@map("users")
}
......@@ -74,6 +75,7 @@ model Application {
approver User? @relation("ApprovedApplications", fields: [approvedBy], references: [id], onDelete: SetNull)
@@index([approvedBy])
@@map("applications")
}
......@@ -108,6 +110,7 @@ model InternProfile {
dailyReports DailyReport[]
weeklyEvaluations WeeklyEvaluation[]
@@index([leaderId])
@@map("intern_profiles")
}
......@@ -146,6 +149,7 @@ model Task {
creator User @relation("CreatedTasks", fields: [createdBy], references: [id], onDelete: Cascade)
assignment TaskAssignment?
@@index([createdBy])
@@map("tasks")
}
......@@ -171,6 +175,8 @@ model TaskAssignment {
assigner User @relation("AssignedTasks", fields: [assignedBy], references: [id], onDelete: Cascade)
submissions TaskSubmission[]
@@index([internId])
@@index([assignedBy])
@@map("task_assignments")
}
......@@ -198,6 +204,8 @@ model TaskSubmission {
assignment TaskAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
reviewer User? @relation("ReviewedSubmissions", fields: [reviewedBy], references: [id], onDelete: SetNull)
@@index([assignmentId])
@@index([reviewedBy])
@@map("task_submissions")
}
......@@ -213,6 +221,7 @@ model DailyReport {
intern InternProfile @relation(fields: [internId], references: [id], onDelete: Cascade)
@@index([internId])
@@map("daily_reports")
}
......@@ -235,6 +244,7 @@ model WeeklyEvaluation {
leader User @relation("LeaderEvaluations", fields: [leaderId], references: [id], onDelete: Cascade)
@@unique([internId, week])
@@index([leaderId])
@@map("weekly_evaluations")
}
......@@ -252,6 +262,7 @@ model Notification {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
logs NotificationLog[]
@@index([userId])
@@map("notifications")
}
......@@ -276,6 +287,7 @@ model NotificationLog {
notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade)
@@index([notificationId])
@@map("notification_logs")
}
......@@ -284,10 +296,13 @@ model RefreshToken {
id String @id @default(uuid()) @db.Uuid
token String @unique
userId String @db.Uuid @map("user_id")
userAgent String? @map("user_agent")
ipAddress String? @map("ip_address")
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("refresh_tokens")
}
......@@ -7,6 +7,8 @@ import routes from './routes';
const app = express();
app.set('trust proxy', true);
app.use(helmet());
app.use(cors());
app.use(morgan('dev'));
......
......@@ -7,7 +7,9 @@ export class AuthController {
login = async (req: Request, res: Response, next: NextFunction) => {
try {
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({
success: true,
......@@ -34,7 +36,9 @@ export class AuthController {
refresh = async (req: Request, res: Response, next: NextFunction) => {
try {
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({
success: true,
......
......@@ -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({
data: {
userId,
token,
expiresAt,
userAgent,
ipAddress,
},
});
}
......
......@@ -8,7 +8,7 @@ import { jwtConfig } from '../../config/jwt.config';
export class AuthService {
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);
if (!user) {
......@@ -37,7 +37,7 @@ export class AuthService {
const decoded = jwt.decode(refreshToken) as { exp: number };
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 {
accessToken,
......@@ -50,7 +50,7 @@ export class AuthService {
};
}
async refresh(token: string) {
async refresh(token: string, metadata?: { userAgent?: string; ipAddress?: string }) {
let payload: any;
try {
payload = jwt.verify(token, jwtConfig.refreshSecret);
......@@ -87,7 +87,7 @@ export class AuthService {
const decoded = jwt.decode(newRefreshToken) as { exp: number };
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 {
accessToken: newAccessToken,
......@@ -96,7 +96,11 @@ export class AuthService {
}
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) {
......
import { z } from 'zod';
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'),
});
......
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