Files
tier-maker/backend/src/routes/auth.js

122 lines
3.7 KiB
JavaScript

const express = require('express')
const bcrypt = require('bcryptjs')
const { z } = require('zod')
const { nanoid } = require('nanoid')
const multer = require('multer')
const {
countUsers,
findUserByEmail,
findUserById,
createUser,
updateUserProfile,
} = require('../db')
const { requireAuth } = require('../middleware/auth')
const { createMemoryUpload, writeOptimizedImage } = require('../lib/image-storage')
const router = express.Router()
const signupSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
})
const profileSchema = z.object({
nickname: z.string().trim().min(1).max(40),
removeAvatar: z.union([z.string(), z.undefined()]).optional(),
})
router.post('/signup', async (req, res) => {
const parsed = signupSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const { email, password } = parsed.data
const exists = await findUserByEmail(email)
if (exists) return res.status(409).json({ error: 'email_taken' })
const passwordHash = await bcrypt.hash(password, 10)
const isAdmin = (await countUsers()) === 0
const user = await createUser({ id: nanoid(), email, nickname: '', passwordHash, isAdmin })
req.session.userId = user.id
req.session.isAdmin = !!user.isAdmin
req.session.save((err) => {
if (err) return res.status(500).json({ error: 'session_save_failed' })
res.json(user)
})
})
router.post('/login', async (req, res) => {
const parsed = signupSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const { email, password } = parsed.data
const user = await findUserByEmail(email)
if (!user) return res.status(401).json({ error: 'invalid_credentials' })
const ok = await bcrypt.compare(password, user.passwordHash)
if (!ok) return res.status(401).json({ error: 'invalid_credentials' })
req.session.userId = user.id
req.session.isAdmin = !!user.isAdmin
req.session.save((err) => {
if (err) return res.status(500).json({ error: 'session_save_failed' })
res.json({
id: user.id,
email: user.email,
nickname: user.nickname || '',
isAdmin: !!user.isAdmin,
avatarSrc: user.avatarSrc || '',
createdAt: user.createdAt,
})
})
})
router.post('/logout', async (req, res) => {
if (!req.session) return res.json({ ok: true })
req.session.destroy(() => res.json({ ok: true }))
})
router.get('/me', async (req, res) => {
if (!req.session || !req.session.userId) return res.json({ user: null })
const user = await findUserById(req.session.userId)
if (!user) return res.json({ user: null })
res.json({ user })
})
router.get('/meta', async (req, res) => {
res.json({ hasUsers: (await countUsers()) > 0 })
})
const upload = createMemoryUpload(multer, { fileSize: 4 * 1024 * 1024 })
router.post('/profile', requireAuth, upload.single('avatar'), async (req, res) => {
const parsed = profileSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const user = await findUserById(req.session.userId)
if (!user) return res.status(404).json({ error: 'not_found' })
const optimized = req.file
? await writeOptimizedImage({
file: req.file,
directory: 'avatars',
width: 512,
height: 512,
fit: 'cover',
quality: 82,
})
: null
const shouldRemoveAvatar = parsed.data.removeAvatar === '1'
const nextAvatarSrc = shouldRemoveAvatar ? '' : optimized?.src || user.avatarSrc || ''
const updated = await updateUserProfile({
id: user.id,
nickname: parsed.data.nickname,
avatarSrc: nextAvatarSrc,
})
res.json({ user: updated })
})
module.exports = router