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
Binary file added lint_output.json
Binary file not shown.
16 changes: 14 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ datasource db {
model User {
id String @id @default(uuid())
email String @unique
name String
username String @unique
password String
role Role @default(LEARNER)
walletAddress String? @unique
passwordHash String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
completions Completion[]
credentials Credential[]
transactions Transaction[]

@@map("users")
}

model Module {
Expand Down Expand Up @@ -71,6 +75,14 @@ model Transaction {
updatedAt DateTime @updatedAt
}

enum Role {
ADMIN
LEARNER
INSTRUCTOR
}



model WebhookEndpoint {
id String @id @default(uuid())
url String
Expand Down
11 changes: 8 additions & 3 deletions src/config/database.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
prisma: any | undefined
prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()
const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}

export default prisma
export { prisma }
163 changes: 163 additions & 0 deletions src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Request, Response } from 'express'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import prisma from '../config/database'
import { loginSchema, registerSchema } from '../schemas/auth.schema'
import { UserRole } from '../types/user.types'

const JWT_SECRET = process.env.JWT_SECRET || 'your-default-secret'
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1d'

export class AuthController {
/**
* @route POST /api/v1/auth/register
* @desc Register a new user
* @access Public
*/
async register(req: Request, res: Response): Promise<void> {
try {
// Validate input
const validation = registerSchema.safeParse(req.body)
if (!validation.success) {
res.status(400).json({
error: 'Validation failed',
details: validation.error.format()
})

return
}

const { email, password, username, role } = validation.data

// Check if user already exists
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ email },
{ username }
]
}
})

if (existingUser) {
res.status(409).json({ error: 'User with this email or username already exists' })

return
}

// Hash password
const salt = await bcrypt.genSalt(10)
const hashedPassword = await bcrypt.hash(password, salt)

// Create user
const user = await prisma.user.create({
data: {
email,
username,
password: hashedPassword,
role: (role as any) || UserRole.LEARNER,
}
})

// Generate token
const token = this.generateToken(user.id, user.role)

res.status(201).json({
message: 'User registered successfully',
token,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role
}
})
} catch (error) {
console.error('Registration error:', error)
res.status(500).json({ error: 'Internal server error during registration' })
}
}

/**
* @route POST /api/v1/auth/login
* @desc Login a user
* @access Public
*/
async login(req: Request, res: Response): Promise<void> {
try {
// Validate input
const validation = loginSchema.safeParse(req.body)
if (!validation.success) {
res.status(400).json({
error: 'Validation failed',
details: validation.error.format()
})

return
}

const { email, password } = validation.data

// Find user
const user = await prisma.user.findUnique({
where: { email }
})

if (!user) {
res.status(401).json({ error: 'Invalid credentials' })

return
}

// Verify password
const isMatch = await bcrypt.compare(password, user.password)
if (!isMatch) {
res.status(401).json({ error: 'Invalid credentials' })

return
}

// Update last login
await prisma.user.update({
where: { id: user.id },
data: { lastLoginAt: new Date() }
})

// Generate token
const token = this.generateToken(user.id, user.role)

res.status(200).json({
message: 'Login successful',
token,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role
}
})
} catch (error) {
console.error('Login error:', error)
res.status(500).json({ error: 'Internal server error during login' })
}
}

/**
* @route POST /api/v1/auth/logout
* @desc Logout user (client-side usually handles this by deleting token, but can track server-side)
* @access Private (optional, here public)
*/
async logout(req: Request, res: Response): Promise<void> {
// For stateless JWT, we can't truly "logout" unless we blacklist tokens.
// For now, let's just return success message as the client will clear the token.
res.status(200).json({ message: 'Logged out successfully. Please clear your token client-side.' })
}

private generateToken(userId: string, role: string): string {
return jwt.sign(
{ id: userId, role },
JWT_SECRET as string,
{ expiresIn: JWT_EXPIRES_IN as any }
)
}
}
46 changes: 30 additions & 16 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import express, { Router } from 'express'
import userRoutes from './v1/users.routes'
import rewardRoutes from './v1/rewards.routes'
import moduleRoutes from './v1/modules.routes'

const router: express.Router = Router()

router.get('/', (req, res) => {
res.json({ message: 'API is running' })
})

router.use('/v1/users', userRoutes)
router.use('/v1/rewards', rewardRoutes)
router.use('/v1/modules', moduleRoutes)

export default router
import { Router } from 'express'
import userRoutes from './v1/users.routes'
import authRoutes from './v1/auth.routes'

const router: Router = Router()

router.get('/', (req, res) => {
res.json({ message: 'API is running' })
})

router.use('/v1/users', userRoutes)
router.use('/v1/auth', authRoutes)

export default router
import express, { Router } from 'express'
import userRoutes from './v1/users.routes'
import rewardRoutes from './v1/rewards.routes'
import moduleRoutes from './v1/modules.routes'

const router: express.Router = Router()

router.get('/', (req, res) => {
res.json({ message: 'API is running' })
})

router.use('/v1/users', userRoutes)
router.use('/v1/rewards', rewardRoutes)
router.use('/v1/modules', moduleRoutes)

export default router
28 changes: 28 additions & 0 deletions src/routes/v1/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Router } from 'express'
import { AuthController } from '../../controllers/auth.controller'

const router: Router = Router()
const authController = new AuthController()

/**
* @route POST /api/v1/auth/register
* @desc Register a new user
* @access Public
*/
router.post('/register', authController.register.bind(authController))

/**
* @route POST /api/v1/auth/login
* @desc Login user
* @access Public
*/
router.post('/login', authController.login.bind(authController))

/**
* @route POST /api/v1/auth/logout
* @desc Logout user
* @access Public
*/
router.post('/logout', authController.logout.bind(authController))

export default router
17 changes: 17 additions & 0 deletions src/schemas/auth.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from 'zod'
import { UserRole } from '../types/user.types'

export const registerSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters long'),
username: z.string().min(3, 'Username must be at least 3 characters long'),
role: z.nativeEnum(UserRole).optional().default(UserRole.LEARNER),
})

export const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string(),
})

export type RegisterInput = z.infer<typeof registerSchema>;
export type LoginInput = z.infer<typeof loginSchema>;
Loading