mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
294 lines
9.1 KiB
JavaScript
294 lines
9.1 KiB
JavaScript
const express = require('express')
|
|
const { body, validationResult } = require('express-validator')
|
|
const createAuthenticator = require('../middleware/authenticator')
|
|
const { ApiError } = require('../middleware/error-handler')
|
|
|
|
/**
|
|
* Account Management API Routes
|
|
* GET /account - Get account info with stats
|
|
* POST /verify-email - Add forwarding email (triggers verification)
|
|
* DELETE /verify-email/:id - Remove forwarding email
|
|
* POST /change-password - Change password
|
|
* DELETE /account - Delete account
|
|
* GET /token - Get API token info
|
|
* POST /token - Generate/regenerate API token
|
|
* DELETE /token - Revoke API token
|
|
*/
|
|
function createAccountRouter(dependencies) {
|
|
const router = express.Router()
|
|
const {
|
|
authService,
|
|
userRepository,
|
|
apiTokenRepository,
|
|
inboxLock,
|
|
config
|
|
} = dependencies
|
|
|
|
// Check if auth is enabled
|
|
if (!authService || !config.user.authEnabled) {
|
|
router.all('*', (req, res) => {
|
|
res.apiError('Authentication is disabled', 'AUTH_DISABLED', 503)
|
|
})
|
|
return router
|
|
}
|
|
|
|
const { requireAuth } = createAuthenticator(apiTokenRepository)
|
|
|
|
/**
|
|
* GET /account - Get account information
|
|
*/
|
|
router.get('/', requireAuth, async(req, res, next) => {
|
|
try {
|
|
const userId = req.user.id
|
|
|
|
// Get user stats
|
|
const stats = userRepository.getUserStats(userId)
|
|
|
|
// Get verified emails
|
|
const verifiedEmails = userRepository.getForwardEmails(userId)
|
|
|
|
// Get locked inboxes
|
|
let lockedInboxes = []
|
|
if (inboxLock) {
|
|
lockedInboxes = inboxLock.getUserLockedInboxes(userId)
|
|
}
|
|
|
|
// Get API token info (without exposing the token itself)
|
|
let tokenInfo = null
|
|
if (apiTokenRepository) {
|
|
const token = apiTokenRepository.getByUserId(userId)
|
|
if (token) {
|
|
tokenInfo = {
|
|
hasToken: true,
|
|
createdAt: token.created_at,
|
|
lastUsed: token.last_used
|
|
}
|
|
}
|
|
}
|
|
|
|
res.apiSuccess({
|
|
userId: userId,
|
|
username: req.user.username,
|
|
createdAt: stats.created_at,
|
|
lastLogin: stats.last_login,
|
|
verifiedEmails: verifiedEmails,
|
|
lockedInboxes: lockedInboxes,
|
|
apiToken: tokenInfo
|
|
})
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* POST /verify-email - Add forwarding email (triggers verification)
|
|
*/
|
|
router.post('/verify-email',
|
|
requireAuth,
|
|
body('email').isEmail().normalizeEmail(),
|
|
async(req, res, next) => {
|
|
try {
|
|
const errors = validationResult(req)
|
|
if (!errors.isEmpty()) {
|
|
return res.apiError('Invalid email address', 'VALIDATION_ERROR', 400)
|
|
}
|
|
|
|
const { email } = req.body
|
|
const userId = req.user.id
|
|
|
|
// Check if user already has max verified emails
|
|
const count = userRepository.countVerifiedEmails(userId)
|
|
if (count >= config.user.maxVerifiedEmails) {
|
|
return res.apiError(
|
|
`Maximum ${config.user.maxVerifiedEmails} verified emails allowed`,
|
|
'MAX_EMAILS_REACHED',
|
|
400
|
|
)
|
|
}
|
|
|
|
// Add email (will be marked as verified immediately for API)
|
|
// In a real implementation, you'd send a verification email
|
|
userRepository.addVerifiedEmail(userId, email)
|
|
|
|
res.apiSuccess({
|
|
message: 'Email added successfully',
|
|
email: email
|
|
}, 201)
|
|
} catch (error) {
|
|
if (error.message.includes('UNIQUE')) {
|
|
return res.apiError('Email already verified', 'DUPLICATE_EMAIL', 400)
|
|
}
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* DELETE /verify-email/:id - Remove forwarding email
|
|
*/
|
|
router.delete('/verify-email/:id', requireAuth, async(req, res, next) => {
|
|
try {
|
|
const emailId = parseInt(req.params.id)
|
|
const userId = req.user.id
|
|
|
|
if (isNaN(emailId)) {
|
|
return res.apiError('Invalid email ID', 'VALIDATION_ERROR', 400)
|
|
}
|
|
|
|
const result = userRepository.removeVerifiedEmail(userId, emailId)
|
|
|
|
if (result.changes === 0) {
|
|
return res.apiError('Email not found or unauthorized', 'NOT_FOUND', 404)
|
|
}
|
|
|
|
res.apiSuccess({ message: 'Email removed successfully' })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* POST /change-password - Change password
|
|
*/
|
|
router.post('/change-password',
|
|
requireAuth,
|
|
body('currentPassword').notEmpty(),
|
|
body('newPassword').isLength({ min: 8 }),
|
|
async(req, res, next) => {
|
|
try {
|
|
const errors = validationResult(req)
|
|
if (!errors.isEmpty()) {
|
|
return res.apiError('Invalid password format', 'VALIDATION_ERROR', 400)
|
|
}
|
|
|
|
const { currentPassword, newPassword } = req.body
|
|
const userId = req.user.id
|
|
|
|
// Verify current password
|
|
const isValid = userRepository.checkPassword(userId, currentPassword)
|
|
if (!isValid) {
|
|
return res.apiError('Current password is incorrect', 'INVALID_PASSWORD', 401)
|
|
}
|
|
|
|
// Validate new password
|
|
const validation = authService.validatePassword(newPassword)
|
|
if (!validation.isValid) {
|
|
return res.apiError(validation.error, 'WEAK_PASSWORD', 400)
|
|
}
|
|
|
|
// Change password
|
|
userRepository.changePassword(userId, newPassword)
|
|
|
|
res.apiSuccess({ message: 'Password changed successfully' })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* DELETE /account - Delete account
|
|
*/
|
|
router.delete('/',
|
|
requireAuth,
|
|
body('password').notEmpty(),
|
|
async(req, res, next) => {
|
|
try {
|
|
const errors = validationResult(req)
|
|
if (!errors.isEmpty()) {
|
|
return res.apiError('Password is required', 'VALIDATION_ERROR', 400)
|
|
}
|
|
|
|
const { password } = req.body
|
|
const userId = req.user.id
|
|
|
|
// Verify password
|
|
const isValid = userRepository.checkPassword(userId, password)
|
|
if (!isValid) {
|
|
return res.apiError('Incorrect password', 'INVALID_PASSWORD', 401)
|
|
}
|
|
|
|
// Delete user (cascades to tokens, emails, locks)
|
|
userRepository.deleteUser(userId)
|
|
|
|
// Destroy session
|
|
req.session.destroy((err) => {
|
|
if (err) {
|
|
return next(err)
|
|
}
|
|
|
|
res.apiSuccess({ message: 'Account deleted successfully' })
|
|
})
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* GET /token - Get API token info (not the token itself)
|
|
*/
|
|
router.get('/token', requireAuth, async(req, res, next) => {
|
|
try {
|
|
const userId = req.user.id
|
|
|
|
const token = apiTokenRepository.getByUserId(userId)
|
|
|
|
if (!token) {
|
|
return res.apiSuccess({
|
|
hasToken: false
|
|
})
|
|
}
|
|
|
|
res.apiSuccess({
|
|
hasToken: true,
|
|
createdAt: token.created_at,
|
|
lastUsed: token.last_used
|
|
})
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* POST /token - Generate or regenerate API token
|
|
*/
|
|
router.post('/token', requireAuth, async(req, res, next) => {
|
|
try {
|
|
const userId = req.user.id
|
|
|
|
// Generate new token (replaces existing if any)
|
|
const newToken = apiTokenRepository.create(userId)
|
|
|
|
res.apiSuccess({
|
|
token: newToken,
|
|
message: 'API token generated successfully. Save this token - it will not be shown again.'
|
|
}, 201)
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* DELETE /token - Revoke API token
|
|
*/
|
|
router.delete('/token', requireAuth, async(req, res, next) => {
|
|
try {
|
|
const userId = req.user.id
|
|
|
|
const revoked = apiTokenRepository.revoke(userId)
|
|
|
|
if (!revoked) {
|
|
return res.apiError('No token to revoke', 'NOT_FOUND', 404)
|
|
}
|
|
|
|
res.apiSuccess({ message: 'API token revoked successfully' })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
})
|
|
|
|
return router
|
|
}
|
|
|
|
module.exports = createAccountRouter
|