feat: registration, phone mask, username change

- Registration form on login page (tab toggle login/register)
- POST /api/auth/register — creates account with phone+name+password
- Phone mask +7 (XXX) XXX-XX-XX on login and profile pages
- Username now editable in profile (latin/digits/_, 3-30 chars)
- PUT /api/auth/me now accepts username with uniqueness validation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ai
2026-05-25 15:09:32 +03:00
parent 63f280f754
commit 2c362c06cd
3 changed files with 187 additions and 26 deletions

View File

@@ -5,6 +5,8 @@ import fs from 'fs';
import crypto from 'crypto';
import { pool } from '../db.js';
const COLORS = ['#3b82f6','#ef4444','#10b981','#f59e0b','#8b5cf6','#ec4899','#14b8a6','#f97316'];
function userDto(u: any) {
return {
id: u.id, username: u.username, displayName: u.display_name,
@@ -14,6 +16,50 @@ function userDto(u: any) {
}
export default async function authRoutes(app: FastifyInstance) {
// Register
app.post('/register', async (req, reply) => {
const { phone, displayName, password } = req.body as any;
if (!phone || !displayName || !password) {
return reply.status(400).send({ error: 'Заполните все поля' });
}
if (password.length < 6) {
return reply.status(400).send({ error: 'Пароль минимум 6 символов' });
}
// Check phone uniqueness
const { rows: [existingPhone] } = await pool.query(
'SELECT id FROM users WHERE phone = $1', [phone]
);
if (existingPhone) return reply.status(400).send({ error: 'Этот номер уже зарегистрирован' });
// Auto-generate username from displayName
let base = displayName.toLowerCase()
.replace(/[^a-z0-9а-яё]/gi, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.slice(0, 20) || 'user';
// transliterate basic cyrillic
base = base.replace(/[а-яё]/gi, (c: string) => {
const map: Record<string, string> = {а:'a',б:'b',в:'v',г:'g',д:'d',е:'e',ж:'zh',з:'z',и:'i',й:'j',к:'k',л:'l',м:'m',н:'n',о:'o',п:'p',р:'r',с:'s',т:'t',у:'u',ф:'f',х:'h',ц:'ts',ч:'ch',ш:'sh',щ:'sh',ъ:'',ы:'y',ь:'',э:'e',ю:'yu',я:'ya',ё:'e'};
return map[c.toLowerCase()] || '_';
}) || 'user';
let username = base;
const { rows: [taken] } = await pool.query('SELECT id FROM users WHERE username = $1', [username]);
if (taken) username = base + Math.floor(Math.random() * 9000 + 1000);
const hash = await bcrypt.hash(password, 10);
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
const { rows: [user] } = await pool.query(
`INSERT INTO users (username, display_name, password_hash, phone, avatar_color)
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
[username, displayName, hash, phone, color]
);
const token = app.jwt.sign({ id: user.id, isAdmin: false }, { expiresIn: '30d' });
return reply.status(201).send({ token, user: userDto(user) });
});
app.post('/login', async (req, reply) => {
const { username, phone, password } = req.body as { username?: string; phone?: string; password: string };
const login = phone || username || '';
@@ -45,7 +91,7 @@ export default async function authRoutes(app: FastifyInstance) {
app.put('/me', { preHandler: [app.authenticate] }, async (req, reply) => {
const { id } = req.user as { id: string };
const { displayName, bio, phone, position, password, newPassword } = req.body as any;
const { displayName, bio, phone, position, password, newPassword, username } = req.body as any;
if (newPassword) {
const { rows: [user] } = await pool.query('SELECT password_hash FROM users WHERE id = $1', [id]);
@@ -55,9 +101,25 @@ export default async function authRoutes(app: FastifyInstance) {
await pool.query('UPDATE users SET password_hash = $1 WHERE id = $2', [hash, id]);
}
if (username) {
if (!/^[a-z0-9_]{3,30}$/.test(username)) {
return reply.status(400).send({ error: 'Логин: только латиница, цифры, _ (330 символов)' });
}
const { rows: [taken] } = await pool.query(
'SELECT id FROM users WHERE username = $1 AND id != $2', [username, id]
);
if (taken) return reply.status(400).send({ error: 'Этот логин уже занят' });
}
await pool.query(
'UPDATE users SET display_name = COALESCE($1, display_name), bio = COALESCE($2, bio), phone = COALESCE($3, phone), position = COALESCE($4, position) WHERE id = $5',
[displayName, bio, phone, position, id]
`UPDATE users SET
display_name = COALESCE($1, display_name),
bio = COALESCE($2, bio),
phone = COALESCE($3, phone),
position = COALESCE($4, position),
username = COALESCE($5, username)
WHERE id = $6`,
[displayName, bio, phone, position, username || null, id]
);
const { rows: [user] } = await pool.query(