84 lines
2.7 KiB
JavaScript
84 lines
2.7 KiB
JavaScript
import { Router } from 'express';
|
|
import bcrypt from 'bcryptjs';
|
|
import { requireTeacher } from './auth.js';
|
|
import { findUserByUsername, createUser, listStudents } from './db.js';
|
|
|
|
const router = Router();
|
|
const BCRYPT_ROUNDS = 8;
|
|
const USERNAME_RE = /^[a-zA-Z0-9_.-]{3,64}$/;
|
|
|
|
const WORDS_A = ['red', 'blue', 'green', 'happy', 'sunny', 'brave', 'tiny', 'swift', 'kind', 'bright'];
|
|
const WORDS_B = ['cat', 'dog', 'fox', 'duck', 'frog', 'bear', 'star', 'moon', 'kite', 'fish'];
|
|
const WORDS_C = ['jump', 'play', 'run', 'smile', 'spin', 'dance', 'wave', 'clap', 'sing', 'grow'];
|
|
|
|
function sanitizeStudent(row) {
|
|
return {
|
|
id: row.id,
|
|
username: row.username,
|
|
role: row.role,
|
|
createdAt: row.created_at,
|
|
};
|
|
}
|
|
|
|
function randomItem(list) {
|
|
return list[Math.floor(Math.random() * list.length)];
|
|
}
|
|
|
|
function candidateUsername() {
|
|
const useThree = Math.random() < 0.45;
|
|
const first = randomItem(WORDS_A);
|
|
const second = randomItem(WORDS_B);
|
|
if (!useThree) return `${first}${second}`;
|
|
const third = randomItem(WORDS_C);
|
|
return `${first}${second}${third}`;
|
|
}
|
|
|
|
async function generateUniqueUsername() {
|
|
for (let i = 0; i < 40; i += 1) {
|
|
const candidate = candidateUsername();
|
|
const existing = await findUserByUsername(candidate);
|
|
if (!existing) return candidate;
|
|
}
|
|
return `${candidateUsername()}${Math.floor(100 + Math.random() * 900)}`;
|
|
}
|
|
|
|
router.get('/students', requireTeacher, async (_req, res) => {
|
|
try {
|
|
const students = (await listStudents()).map(sanitizeStudent);
|
|
res.json({ students });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.get('/students/suggest-username', requireTeacher, async (_req, res) => {
|
|
try {
|
|
const username = await generateUniqueUsername();
|
|
res.json({ username });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/students', requireTeacher, async (req, res) => {
|
|
let { username } = req.body || {};
|
|
try {
|
|
username = username ? String(username).trim() : '';
|
|
if (!username) username = await generateUniqueUsername();
|
|
if (!USERNAME_RE.test(username)) {
|
|
return res.status(400).json({ error: 'username must be 3-64 chars, letters/digits/._-' });
|
|
}
|
|
const existing = await findUserByUsername(username);
|
|
if (existing) return res.status(409).json({ error: 'username already taken' });
|
|
|
|
// Requested behavior: default password equals username.
|
|
const hash = await bcrypt.hash(username, BCRYPT_ROUNDS);
|
|
const user = await createUser({ username, passwordHash: hash, role: 'student' });
|
|
res.json({ student: sanitizeStudent(user), defaultPassword: username });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
export default router;
|