feat(auth): membuat user registration, login, sama pengecekan profile user yang sudah dilogin

This commit is contained in:
Rafi Athallah
2026-05-26 11:57:00 +07:00
parent 724cfd50e2
commit 4b9bc534cc
9 changed files with 506 additions and 15 deletions
+5
View File
@@ -0,0 +1,5 @@
import { PrismaClient } from '../generated/prisma/client';
const prisma = new PrismaClient();
export default prisma;
+67
View File
@@ -0,0 +1,67 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import prisma from '../lib/prisma';
import { CustomError } from './errorHandler';
export interface AuthenticatedRequest extends Request {
user?: {
id: string;
email: string;
role: 'ADMIN' | 'STUDENT';
};
}
export const protect = async (req: AuthenticatedRequest, _res: Response, next: NextFunction) => {
let token: string | undefined;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
const error: CustomError = new Error('Not authorized to access this route, token is missing');
error.statusCode = 401;
return next(error);
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'internify_secret_key_2026_dev') as {
id: string;
email: string;
role: 'ADMIN' | 'STUDENT';
};
const user = await prisma.user.findUnique({
where: { id: decoded.id },
select: {
id: true,
email: true,
role: true,
},
});
if (!user) {
const error: CustomError = new Error('User belonging to this token no longer exists');
error.statusCode = 401;
return next(error);
}
req.user = user as { id: string; email: string; role: 'ADMIN' | 'STUDENT' };
next();
} catch (err) {
const error: CustomError = new Error('Not authorized to access this route, token is invalid or expired');
error.statusCode = 401;
return next(error);
}
};
export const restrictTo = (...roles: ('ADMIN' | 'STUDENT')[]) => {
return (req: AuthenticatedRequest, _res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
const error: CustomError = new Error('You do not have permission to perform this action');
error.statusCode = 403;
return next(error);
}
next();
};
};
+200
View File
@@ -0,0 +1,200 @@
import { Router, Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import prisma from '../lib/prisma';
import { protect, AuthenticatedRequest } from '../middleware/auth';
import { CustomError } from '../middleware/errorHandler';
const router = Router();
const generateToken = (payload: { id: string; email: string; role: string }) => {
return jwt.sign(
payload,
process.env.JWT_SECRET || 'internify_secret_key_2026_dev',
{ expiresIn: (process.env.JWT_EXPIRES_IN || '7d') as any }
);
};
/**
* @route POST /api/auth/register
* @desc Mendaftarkan user baru dengan role STUDENT (Mahasiswa)
* @access Public
*/
router.post('/register', async (req: Request, res: Response, next: NextFunction) => {
try {
const {
email,
password,
namaDepan,
namaBelakang,
kontak,
jurusan,
universitas,
negara,
cvPath,
portofolioPath,
motivasi,
relevantSkills,
} = req.body;
if (!email || !password || !namaDepan || !kontak || !jurusan || !universitas || !negara) {
const error: CustomError = new Error('Mohon lengkapi semua field wajib yang diperlukan');
error.statusCode = 400;
return next(error);
}
const existingUser = await prisma.user.findUnique({
where: { email },
});
if (existingUser) {
const error: CustomError = new Error('Email sudah terdaftar');
error.statusCode = 400;
return next(error);
}
const salt = await bcrypt.genSalt(10);
const passwordHash = await bcrypt.hash(password, salt);
const newUser = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
email,
passwordHash,
role: 'STUDENT',
},
});
await tx.mahasiswa.create({
data: {
idUser: user.id,
namaDepan,
namaBelakang: namaBelakang || null,
kontak,
jurusan,
universitas,
negara,
cvPath: cvPath || '',
portofolioPath: portofolioPath || '',
motivasi: motivasi || '',
relevantSkills: relevantSkills || '',
},
});
return user;
});
res.status(201).json({
success: true,
message: 'Registrasi mahasiswa berhasil',
data: {
id: newUser.id,
email: newUser.email,
role: newUser.role,
createdAt: newUser.createdAt,
},
});
} catch (error) {
next(error);
}
});
/**
* @route POST /api/auth/login
* @desc Autentikasi user dan dapatin token
* @access Public
*/
router.post('/login', async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, password } = req.body;
if (!email || !password) {
const error: CustomError = new Error('Mohon masukkan email dan password');
error.statusCode = 400;
return next(error);
}
const user = await prisma.user.findUnique({
where: { email },
include: {
admin: true,
mahasiswa: true,
},
});
if (!user) {
const error: CustomError = new Error('Email atau password salah');
error.statusCode = 401;
return next(error);
}
const isMatch = await bcrypt.compare(password, user.passwordHash);
if (!isMatch) {
const error: CustomError = new Error('Email atau password salah');
error.statusCode = 401;
return next(error);
}
const profile = user.role === 'ADMIN' ? user.admin : user.mahasiswa;
const token = generateToken({
id: user.id,
email: user.email,
role: user.role,
});
res.status(200).json({
success: true,
token,
user: {
id: user.id,
email: user.email,
role: user.role,
profile,
},
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/auth/profile
* @desc Dapatin profile user yang sedang login
* @access Private
*/
router.get('/profile', protect, async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const userId = req.user?.id;
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
admin: true,
mahasiswa: true,
},
});
if (!user) {
const error: CustomError = new Error('User tidak ditemukan');
error.statusCode = 404;
return next(error);
}
const profile = user.role === 'ADMIN' ? user.admin : user.mahasiswa;
res.status(200).json({
success: true,
user: {
id: user.id,
email: user.email,
role: user.role,
profile,
},
});
} catch (error) {
next(error);
}
});
export default router;
+29
View File
@@ -1,7 +1,11 @@
import { Router, Request, Response, NextFunction } from 'express';
import prisma from '../lib/prisma';
import authRouter from './auth';
const router = Router();
router.use('/auth', authRouter);
/**
* @route GET /api/health
* @desc Endpoint untuk memeriksa kesehatan/status server
@@ -15,6 +19,31 @@ router.get('/health', (_req: Request, res: Response) => {
});
});
/**
* @route GET /api/users
* @desc Mendapatkan daftar semua user dari database menggunakan Prisma
* @access Public
*/
router.get('/users', async (_req: Request, res: Response, next: NextFunction) => {
try {
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
role: true,
createdAt: true,
},
});
res.status(200).json({
success: true,
count: users.length,
data: users,
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/error-test
* @desc Mensimulasikan error server internal untuk memverifikasi middleware penanganan error global