feat: latin-only slugs, server-side avatar generation, remove unused User fields
This commit is contained in:
@@ -7,9 +7,6 @@ export type AuthUser = {
|
|||||||
id: string
|
id: string
|
||||||
email: string
|
email: string
|
||||||
displayName?: string | null
|
displayName?: string | null
|
||||||
firstName?: string | null
|
|
||||||
lastName?: string | null
|
|
||||||
gender?: string | null
|
|
||||||
avatar?: string | null
|
avatar?: string | null
|
||||||
avatarStyle?: string | null
|
avatarStyle?: string | null
|
||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `firstName` on the `User` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `gender` on the `User` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `lastName` on the `User` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_User" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"displayName" TEXT,
|
||||||
|
"avatar" TEXT,
|
||||||
|
"avatarStyle" TEXT,
|
||||||
|
"passwordHash" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_User" ("avatar", "avatarStyle", "createdAt", "displayName", "email", "id", "passwordHash", "updatedAt") SELECT "avatar", "avatarStyle", "createdAt", "displayName", "email", "id", "passwordHash", "updatedAt" FROM "User";
|
||||||
|
DROP TABLE "User";
|
||||||
|
ALTER TABLE "new_User" RENAME TO "User";
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -78,9 +78,6 @@ model User {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
email String @unique
|
email String @unique
|
||||||
displayName String?
|
displayName String?
|
||||||
firstName String?
|
|
||||||
lastName String?
|
|
||||||
gender String?
|
|
||||||
avatar String?
|
avatar String?
|
||||||
avatarStyle String?
|
avatarStyle String?
|
||||||
passwordHash String?
|
passwordHash String?
|
||||||
|
|||||||
Vendored
+3
-1
@@ -1,4 +1,5 @@
|
|||||||
import { normalizeEmail } from './auth.js'
|
import { normalizeEmail } from './auth.js'
|
||||||
|
import { generateAvatar } from './generate-avatar.js'
|
||||||
import { prisma } from './prisma.js'
|
import { prisma } from './prisma.js'
|
||||||
|
|
||||||
export async function ensureAdminUser() {
|
export async function ensureAdminUser() {
|
||||||
@@ -8,10 +9,11 @@ export async function ensureAdminUser() {
|
|||||||
throw new Error('ADMIN_EMAIL должен быть валидным email')
|
throw new Error('ADMIN_EMAIL должен быть валидным email')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const avatarUri = await generateAvatar(adminEmail)
|
||||||
await prisma.user.upsert({
|
await prisma.user.upsert({
|
||||||
where: { email: adminEmail },
|
where: { email: adminEmail },
|
||||||
update: {},
|
update: {},
|
||||||
create: { email: adminEmail },
|
create: { email: adminEmail, avatar: avatarUri, avatarStyle: 'avataaars' },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ensure admin notification settings exist
|
// Ensure admin notification settings exist
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createAvatar } from '@dicebear/core'
|
||||||
|
import { avataaars } from '@dicebear/collection'
|
||||||
|
|
||||||
|
const DEFAULT_STYLE = avataaars
|
||||||
|
|
||||||
|
export async function generateAvatar(seed) {
|
||||||
|
const avatar = createAvatar(DEFAULT_STYLE, { seed: String(seed) })
|
||||||
|
return avatar.toDataUri()
|
||||||
|
}
|
||||||
@@ -2,22 +2,16 @@ import { describe, it, expect } from 'vitest'
|
|||||||
import { prisma } from '../../lib/prisma.js'
|
import { prisma } from '../../lib/prisma.js'
|
||||||
|
|
||||||
describe('OAuth — User model fields', () => {
|
describe('OAuth — User model fields', () => {
|
||||||
it('stores displayName, firstName, lastName, gender, avatar fields on User model', async () => {
|
it('stores displayName and avatar fields on User model', async () => {
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email: 'test-oauth@example.com',
|
email: 'test-oauth@example.com',
|
||||||
displayName: 'Test User',
|
displayName: 'Test User',
|
||||||
firstName: 'Test',
|
|
||||||
lastName: 'User',
|
|
||||||
gender: 'male',
|
|
||||||
avatar: 'https://example.com/avatar.jpg',
|
avatar: 'https://example.com/avatar.jpg',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(user.displayName).toBe('Test User')
|
expect(user.displayName).toBe('Test User')
|
||||||
expect(user.firstName).toBe('Test')
|
|
||||||
expect(user.lastName).toBe('User')
|
|
||||||
expect(user.gender).toBe('male')
|
|
||||||
expect(user.avatar).toBe('https://example.com/avatar.jpg')
|
expect(user.avatar).toBe('https://example.com/avatar.jpg')
|
||||||
|
|
||||||
await prisma.user.delete({ where: { id: user.id } })
|
await prisma.user.delete({ where: { id: user.id } })
|
||||||
@@ -31,9 +25,6 @@ describe('OAuth — User model fields', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(user.displayName).toBeNull()
|
expect(user.displayName).toBeNull()
|
||||||
expect(user.firstName).toBeNull()
|
|
||||||
expect(user.lastName).toBeNull()
|
|
||||||
expect(user.gender).toBeNull()
|
|
||||||
expect(user.avatar).toBeNull()
|
expect(user.avatar).toBeNull()
|
||||||
|
|
||||||
await prisma.user.delete({ where: { id: user.id } })
|
await prisma.user.delete({ where: { id: user.id } })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function slugify(input) {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
.replace(/[^a-z0-9-а-яё]/gi, '')
|
.replace(/[^a-z0-9-]/gi, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeExtFromFilename(filename) {
|
export function safeExtFromFilename(filename) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
validatePassword,
|
validatePassword,
|
||||||
verifyEmailCode,
|
verifyEmailCode,
|
||||||
} from '../lib/auth.js'
|
} from '../lib/auth.js'
|
||||||
|
import { generateAvatar } from '../lib/generate-avatar.js'
|
||||||
import { prisma } from '../lib/prisma.js'
|
import { prisma } from '../lib/prisma.js'
|
||||||
import { checkLoginRateLimit } from '../lib/rate-limit.js'
|
import { checkLoginRateLimit } from '../lib/rate-limit.js'
|
||||||
|
|
||||||
@@ -18,9 +19,6 @@ export function mapUserForClient(user) {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
firstName: user.firstName,
|
|
||||||
lastName: user.lastName,
|
|
||||||
gender: user.gender,
|
|
||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
avatarStyle: user.avatarStyle,
|
avatarStyle: user.avatarStyle,
|
||||||
isAdmin: Boolean(adminEmail) && userEmail === adminEmail,
|
isAdmin: Boolean(adminEmail) && userEmail === adminEmail,
|
||||||
@@ -55,10 +53,11 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
const ok = await verifyEmailCode({ email, purpose: 'login', code })
|
const ok = await verifyEmailCode({ email, purpose: 'login', code })
|
||||||
if (!ok) return reply.code(401).send({ error: 'Неверный или истёкший код' })
|
if (!ok) return reply.code(401).send({ error: 'Неверный или истёкший код' })
|
||||||
|
|
||||||
|
const avatarUri = await generateAvatar(email)
|
||||||
const user = await prisma.user.upsert({
|
const user = await prisma.user.upsert({
|
||||||
where: { email },
|
where: { email },
|
||||||
update: {},
|
update: {},
|
||||||
create: { email },
|
create: { email, avatar: avatarUri, avatarStyle: 'avataaars' },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ensure notification preference exists
|
// Ensure notification preference exists
|
||||||
@@ -88,12 +87,13 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
if (exists) return reply.code(409).send({ error: 'Эта почта уже зарегистрирована' })
|
if (exists) return reply.code(409).send({ error: 'Эта почта уже зарегистрирована' })
|
||||||
|
|
||||||
const passwordHash = await hashPassword(password)
|
const passwordHash = await hashPassword(password)
|
||||||
|
const avatarUri = await generateAvatar(email)
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email,
|
email,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
displayName: displayName || null,
|
displayName: displayName || null,
|
||||||
avatar: null,
|
avatar: avatarUri,
|
||||||
avatarStyle: 'avataaars',
|
avatarStyle: 'avataaars',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { normalizeEmail } from '../lib/auth.js'
|
import { normalizeEmail } from '../lib/auth.js'
|
||||||
|
import { generateAvatar } from '../lib/generate-avatar.js'
|
||||||
import { prisma } from '../lib/prisma.js'
|
import { prisma } from '../lib/prisma.js'
|
||||||
|
|
||||||
function clientRedirect(fastify, reply, token) {
|
function clientRedirect(fastify, reply, token) {
|
||||||
@@ -57,7 +58,7 @@ async function findOrCreateUserFromOAuth({ provider, providerUserId, accessToken
|
|||||||
data: {
|
data: {
|
||||||
email: norm,
|
email: norm,
|
||||||
displayName: norm.split('@')[0],
|
displayName: norm.split('@')[0],
|
||||||
avatar: null,
|
avatar: await generateAvatar(norm),
|
||||||
avatarStyle: 'avataaars',
|
avatarStyle: 'avataaars',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user