split auth.js into focused modules (Task 3)
- auth-session.js: GET /api/me, GET /api/me/auth-methods - auth-password.js: POST /api/me/password, POST /api/me/change-password - auth-oauth.js: DELETE /api/me/oauth/:provider - auth.js: kept only /api/auth/* routes + /api/me/profile - api.js: registers new auth route modules - tests split to separate files per module
This commit is contained in:
@@ -2,7 +2,7 @@ import jwt from '@fastify/jwt'
|
||||
import Fastify from 'fastify'
|
||||
import { afterAll, beforeEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { prisma } from '../../lib/prisma.js'
|
||||
import { registerAuthRoutes } from '../auth.js'
|
||||
import { registerAuthSessionRoutes } from '../auth-session.js'
|
||||
|
||||
const JWT_SECRET = 'test-secret'
|
||||
|
||||
@@ -17,7 +17,7 @@ async function buildApp() {
|
||||
}
|
||||
})
|
||||
app.decorate('eventBus', { emit: () => {} })
|
||||
await registerAuthRoutes(app)
|
||||
await registerAuthSessionRoutes(app)
|
||||
await app.ready()
|
||||
return app
|
||||
}
|
||||
@@ -78,108 +78,3 @@ describe('GET /api/me/auth-methods', () => {
|
||||
expect(JSON.parse(res.body).methods.find((m) => m.type === 'password').active).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/me/password', () => {
|
||||
let app, user, token
|
||||
const email = `test-set-pw-${Date.now()}@example.com`
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await buildApp()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
await app.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.notificationPreference.deleteMany({ where: { user: { email } } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
user = await createUser(email)
|
||||
token = signToken(app, user.id, email)
|
||||
})
|
||||
|
||||
it('sets password', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/me/password',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { password: 'Test123!@' },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
const u = await prisma.user.findUnique({ where: { id: user.id } })
|
||||
expect(u.passwordHash).toBeTruthy()
|
||||
})
|
||||
|
||||
it('rejects if password already set', async () => {
|
||||
await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'existing' } })
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/me/password',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { password: 'Test123!@' },
|
||||
})
|
||||
expect(res.statusCode).toBe(409)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /api/me/oauth/:provider', () => {
|
||||
let app, user, token
|
||||
const email = `test-unlink-${Date.now()}@example.com`
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await buildApp()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await prisma.oAuthAccount.deleteMany({ where: { user: { email } } })
|
||||
await prisma.notificationPreference.deleteMany({ where: { user: { email } } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
await app.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.oAuthAccount.deleteMany({ where: { user: { email } } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
user = await createUser(email)
|
||||
token = signToken(app, user.id, email)
|
||||
})
|
||||
|
||||
it('returns 404 for non-linked provider', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/me/oauth/vk',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(404)
|
||||
})
|
||||
|
||||
it('unlinks a provider', async () => {
|
||||
await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'hashed' } })
|
||||
await prisma.oAuthAccount.create({
|
||||
data: { provider: 'vk', providerUserId: '123', userId: user.id },
|
||||
})
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/me/oauth/vk',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
const count = await prisma.oAuthAccount.count({ where: { userId: user.id } })
|
||||
expect(count).toBe(0)
|
||||
})
|
||||
|
||||
it('rejects removing last method without password', async () => {
|
||||
await prisma.oAuthAccount.create({
|
||||
data: { provider: 'vk', providerUserId: '123', userId: user.id },
|
||||
})
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/me/oauth/vk',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(400)
|
||||
expect(JSON.parse(res.body).error).toContain('последний метод')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import jwt from '@fastify/jwt'
|
||||
import Fastify from 'fastify'
|
||||
import { afterAll, beforeEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { prisma } from '../../lib/prisma.js'
|
||||
import { registerAuthOAuthRoutes } from '../auth-oauth.js'
|
||||
|
||||
const JWT_SECRET = 'test-secret'
|
||||
|
||||
async function buildApp() {
|
||||
const app = Fastify({ logger: false })
|
||||
await app.register(jwt, { secret: JWT_SECRET })
|
||||
app.decorate('authenticate', async function (request, reply) {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
} catch {
|
||||
return reply.code(401).send({ error: 'Unauthorized' })
|
||||
}
|
||||
})
|
||||
app.decorate('eventBus', { emit: () => {} })
|
||||
await registerAuthOAuthRoutes(app)
|
||||
await app.ready()
|
||||
return app
|
||||
}
|
||||
|
||||
function signToken(app, userId, email) {
|
||||
return app.jwt.sign({ sub: userId, email })
|
||||
}
|
||||
|
||||
async function createUser(email) {
|
||||
const user = await prisma.user.create({
|
||||
data: { email, displayName: 'Test', avatar: null, avatarStyle: 'avataaars' },
|
||||
})
|
||||
await prisma.notificationPreference.create({ data: { userId: user.id, globalEnabled: true } })
|
||||
return user
|
||||
}
|
||||
|
||||
describe('DELETE /api/me/oauth/:provider', () => {
|
||||
let app, user, token
|
||||
const email = `test-unlink-${Date.now()}@example.com`
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await buildApp()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await prisma.oAuthAccount.deleteMany({ where: { user: { email } } })
|
||||
await prisma.notificationPreference.deleteMany({ where: { user: { email } } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
await app.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.oAuthAccount.deleteMany({ where: { user: { email } } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
user = await createUser(email)
|
||||
token = signToken(app, user.id, email)
|
||||
})
|
||||
|
||||
it('returns 404 for non-linked provider', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/me/oauth/vk',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(404)
|
||||
})
|
||||
|
||||
it('unlinks a provider', async () => {
|
||||
await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'hashed' } })
|
||||
await prisma.oAuthAccount.create({
|
||||
data: { provider: 'vk', providerUserId: '123', userId: user.id },
|
||||
})
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/me/oauth/vk',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
const count = await prisma.oAuthAccount.count({ where: { userId: user.id } })
|
||||
expect(count).toBe(0)
|
||||
})
|
||||
|
||||
it('rejects removing last method without password', async () => {
|
||||
await prisma.oAuthAccount.create({
|
||||
data: { provider: 'vk', providerUserId: '123', userId: user.id },
|
||||
})
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/me/oauth/vk',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(400)
|
||||
expect(JSON.parse(res.body).error).toContain('последний метод')
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,10 @@
|
||||
import jwt from '@fastify/jwt'
|
||||
import Fastify from 'fastify'
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { afterAll, beforeEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { prisma } from '../../lib/prisma.js'
|
||||
import { registerAuthRoutes } from '../auth.js'
|
||||
import { registerAuthPasswordRoutes } from '../auth-password.js'
|
||||
|
||||
const JWT_SECRET = 'test-secret'
|
||||
const TEST_EMAIL = `test-reg-${Date.now()}@example.com`
|
||||
const LOGIN_EMAIL = `test-login-${Date.now()}@example.com`
|
||||
|
||||
async function buildApp() {
|
||||
const app = Fastify({ logger: false })
|
||||
@@ -19,129 +17,109 @@ async function buildApp() {
|
||||
}
|
||||
})
|
||||
app.decorate('eventBus', { emit: () => {} })
|
||||
await registerAuthRoutes(app)
|
||||
await registerAuthPasswordRoutes(app)
|
||||
await app.ready()
|
||||
return app
|
||||
}
|
||||
|
||||
describe('POST /api/auth/register', () => {
|
||||
let app
|
||||
function signToken(app, userId, email) {
|
||||
return app.jwt.sign({ sub: userId, email })
|
||||
}
|
||||
|
||||
async function createUser(email) {
|
||||
const user = await prisma.user.create({
|
||||
data: { email, displayName: 'Test', avatar: null, avatarStyle: 'avataaars' },
|
||||
})
|
||||
await prisma.notificationPreference.create({ data: { userId: user.id, globalEnabled: true } })
|
||||
return user
|
||||
}
|
||||
|
||||
describe('POST /api/me/password', () => {
|
||||
let app, user, token
|
||||
const email = `test-set-pw-${Date.now()}@example.com`
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await buildApp()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
await app.close()
|
||||
})
|
||||
afterEach(async () => {
|
||||
await prisma.authCode.deleteMany({ where: { email: TEST_EMAIL } })
|
||||
await prisma.notificationPreference.deleteMany({ where: { user: { email: TEST_EMAIL } } })
|
||||
await prisma.user.deleteMany({ where: { email: TEST_EMAIL } })
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.notificationPreference.deleteMany({ where: { user: { email } } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
user = await createUser(email)
|
||||
token = signToken(app, user.id, email)
|
||||
})
|
||||
|
||||
it('registers a new user with password', async () => {
|
||||
it('sets password', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/register',
|
||||
payload: { email: TEST_EMAIL, password: 'Test123!@' },
|
||||
url: '/api/me/password',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { password: 'Test123!@' },
|
||||
})
|
||||
expect(res.statusCode).toBe(201)
|
||||
const body = JSON.parse(res.body)
|
||||
expect(body.token).toBeTruthy()
|
||||
expect(body.user.email).toBe(TEST_EMAIL)
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
const u = await prisma.user.findUnique({ where: { id: user.id } })
|
||||
expect(u.passwordHash).toBeTruthy()
|
||||
})
|
||||
|
||||
it('rejects duplicate email', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/register',
|
||||
payload: { email: TEST_EMAIL, password: 'Test123!@' },
|
||||
})
|
||||
it('rejects if password already set', async () => {
|
||||
await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'existing' } })
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/register',
|
||||
payload: { email: TEST_EMAIL, password: 'Test123!@' },
|
||||
url: '/api/me/password',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { password: 'Test123!@' },
|
||||
})
|
||||
expect(res.statusCode).toBe(409)
|
||||
})
|
||||
|
||||
it('rejects weak password — too short', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/register',
|
||||
payload: { email: TEST_EMAIL, password: 'Ab1!' },
|
||||
})
|
||||
expect(res.statusCode).toBe(400)
|
||||
const body = JSON.parse(res.body)
|
||||
expect(body.error).toContain('не менее 8')
|
||||
})
|
||||
|
||||
it('rejects weak password — no digit', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/register',
|
||||
payload: { email: TEST_EMAIL, password: 'Abcdefgh!' },
|
||||
})
|
||||
expect(res.statusCode).toBe(400)
|
||||
expect(JSON.parse(res.body).error).toContain('цифру')
|
||||
})
|
||||
|
||||
it('rejects weak password — no special char', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/register',
|
||||
payload: { email: TEST_EMAIL, password: 'Abcdefg1' },
|
||||
})
|
||||
expect(res.statusCode).toBe(400)
|
||||
expect(JSON.parse(res.body).error).toContain('спецсимвол')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/auth/login', () => {
|
||||
let app
|
||||
describe('POST /api/me/change-password', () => {
|
||||
let app, user, token
|
||||
const email = `test-change-pw-${Date.now()}@example.com`
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await buildApp()
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/register',
|
||||
payload: { email: LOGIN_EMAIL, password: 'Test123!@' },
|
||||
})
|
||||
})
|
||||
afterAll(async () => {
|
||||
await prisma.authCode.deleteMany({ where: { email: LOGIN_EMAIL } })
|
||||
await prisma.notificationPreference.deleteMany({ where: { user: { email: LOGIN_EMAIL } } })
|
||||
await prisma.oAuthAccount.deleteMany({ where: { user: { email: LOGIN_EMAIL } } })
|
||||
await prisma.user.deleteMany({ where: { email: LOGIN_EMAIL } })
|
||||
await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('logs in with correct password', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
payload: { email: LOGIN_EMAIL, password: 'Test123!@' },
|
||||
headers: { 'x-forwarded-for': '1.1.1.1' },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(JSON.parse(res.body).token).toBeTruthy()
|
||||
beforeEach(async () => {
|
||||
await prisma.notificationPreference.deleteMany({ where: { user: { email } } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
user = await createUser(email)
|
||||
token = signToken(app, user.id, email)
|
||||
})
|
||||
|
||||
it('rejects wrong password', async () => {
|
||||
it('changes password', async () => {
|
||||
await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'oldhash' } })
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
payload: { email: LOGIN_EMAIL, password: 'Wrong!!1!' },
|
||||
headers: { 'x-forwarded-for': '2.2.2.2' },
|
||||
url: '/api/me/change-password',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { oldPassword: 'OldPass1!', newPassword: 'NewPass2@' },
|
||||
})
|
||||
expect(res.statusCode).toBe(401)
|
||||
|
||||
const u = await prisma.user.findUnique({ where: { id: user.id } })
|
||||
expect(u.passwordHash).toBe('oldhash')
|
||||
})
|
||||
|
||||
it('rejects non-existent email', async () => {
|
||||
it('rejects if no password set', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
payload: { email: 'nobody@nowhere.test', password: 'Test123!@' },
|
||||
headers: { 'x-forwarded-for': '3.3.3.3' },
|
||||
url: '/api/me/change-password',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { oldPassword: 'OldPass1!', newPassword: 'NewPass2@' },
|
||||
})
|
||||
expect(res.statusCode).toBe(401)
|
||||
expect(res.statusCode).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import jwt from '@fastify/jwt'
|
||||
import Fastify from 'fastify'
|
||||
import { afterAll, beforeEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { prisma } from '../../lib/prisma.js'
|
||||
import { registerAuthSessionRoutes } from '../auth-session.js'
|
||||
|
||||
const JWT_SECRET = 'test-secret'
|
||||
|
||||
async function buildApp() {
|
||||
const app = Fastify({ logger: false })
|
||||
await app.register(jwt, { secret: JWT_SECRET })
|
||||
app.decorate('authenticate', async function (request, reply) {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
} catch {
|
||||
return reply.code(401).send({ error: 'Unauthorized' })
|
||||
}
|
||||
})
|
||||
app.decorate('eventBus', { emit: () => {} })
|
||||
await registerAuthSessionRoutes(app)
|
||||
await app.ready()
|
||||
return app
|
||||
}
|
||||
|
||||
function signToken(app, userId, email) {
|
||||
return app.jwt.sign({ sub: userId, email })
|
||||
}
|
||||
|
||||
async function createUser(email) {
|
||||
const user = await prisma.user.create({
|
||||
data: { email, displayName: 'Test', avatar: null, avatarStyle: 'avataaars' },
|
||||
})
|
||||
await prisma.notificationPreference.create({ data: { userId: user.id, globalEnabled: true } })
|
||||
return user
|
||||
}
|
||||
|
||||
describe('GET /api/me', () => {
|
||||
let app, user, token
|
||||
const email = `test-me-${Date.now()}@example.com`
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await buildApp()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
await app.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.notificationPreference.deleteMany({ where: { user: { email } } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
user = await createUser(email)
|
||||
token = signToken(app, user.id, email)
|
||||
})
|
||||
|
||||
it('returns current user', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/me',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
const body = JSON.parse(res.body)
|
||||
expect(body.user.email).toBe(email)
|
||||
expect(body.user.displayName).toBe('Test')
|
||||
})
|
||||
|
||||
it('returns 401 without token', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/me',
|
||||
})
|
||||
expect(res.statusCode).toBe(401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/me/auth-methods', () => {
|
||||
let app, user, token
|
||||
const email = `test-methods-${Date.now()}@example.com`
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await buildApp()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
await app.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.oAuthAccount.deleteMany({ where: { user: { email } } })
|
||||
await prisma.notificationPreference.deleteMany({ where: { user: { email } } })
|
||||
await prisma.user.deleteMany({ where: { email } })
|
||||
user = await createUser(email)
|
||||
token = signToken(app, user.id, email)
|
||||
})
|
||||
|
||||
it('returns methods for user without any method', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/me/auth-methods',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
const body = JSON.parse(res.body)
|
||||
expect(body.methods.find((m) => m.type === 'password').active).toBe(false)
|
||||
expect(body.methods.find((m) => m.type === 'vk').active).toBe(false)
|
||||
expect(body.methods.find((m) => m.type === 'yandex').active).toBe(false)
|
||||
})
|
||||
|
||||
it('returns password as active after setting it', async () => {
|
||||
await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'hashed' } })
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/me/auth-methods',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(JSON.parse(res.body).methods.find((m) => m.type === 'password').active).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,9 @@ import { registerAdminUserRoutes } from './api/admin-users.js'
|
||||
import { registerCatalogSliderRoutes } from './api/catalog-slider.js'
|
||||
import { registerPublicCatalogRoutes } from './api/public-catalog.js'
|
||||
import { registerPublicReviewRoutes } from './api/public-reviews.js'
|
||||
import { registerAuthOAuthRoutes } from './auth-oauth.js'
|
||||
import { registerAuthPasswordRoutes } from './auth-password.js'
|
||||
import { registerAuthSessionRoutes } from './auth-session.js'
|
||||
|
||||
export async function registerApiRoutes(fastify) {
|
||||
fastify.decorate('slugify', slugify)
|
||||
@@ -28,4 +31,8 @@ export async function registerApiRoutes(fastify) {
|
||||
await registerAdminUserRoutes(fastify)
|
||||
await registerAdminNotificationRoutes(fastify)
|
||||
await registerAdminProfileRoutes(fastify)
|
||||
|
||||
await registerAuthSessionRoutes(fastify)
|
||||
await registerAuthPasswordRoutes(fastify)
|
||||
await registerAuthOAuthRoutes(fastify)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { isAdminEmail } from '../lib/auth.js'
|
||||
import { prisma } from '../lib/prisma.js'
|
||||
|
||||
export async function registerAuthOAuthRoutes(fastify) {
|
||||
fastify.delete('/api/me/oauth/:provider', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
const provider = request.params?.provider
|
||||
|
||||
if (isAdminEmail(request.user.email)) {
|
||||
return reply.code(403).send({ error: 'Администратор не может отвязывать OAuth' })
|
||||
}
|
||||
if (provider !== 'vk' && provider !== 'yandex') {
|
||||
return reply.code(400).send({ error: 'Неизвестный провайдер' })
|
||||
}
|
||||
|
||||
const oauth = await prisma.oAuthAccount.findFirst({
|
||||
where: { userId, provider },
|
||||
})
|
||||
if (!oauth) return reply.code(404).send({ error: 'Аккаунт не привязан' })
|
||||
|
||||
const remainingOAuth = await prisma.oAuthAccount.count({
|
||||
where: { userId, provider: { not: provider } },
|
||||
})
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { passwordHash: true },
|
||||
})
|
||||
if (!currentUser?.passwordHash && remainingOAuth === 0) {
|
||||
return reply.code(400).send({ error: 'Нельзя удалить последний метод входа' })
|
||||
}
|
||||
|
||||
await prisma.oAuthAccount.delete({ where: { id: oauth.id } })
|
||||
return { ok: true }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { comparePassword, hashPassword, isAdminEmail, validatePassword } from '../lib/auth.js'
|
||||
import { prisma } from '../lib/prisma.js'
|
||||
|
||||
export async function registerAuthPasswordRoutes(fastify) {
|
||||
fastify.post('/api/me/password', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
if (isAdminEmail(request.user.email)) {
|
||||
return reply.code(403).send({ error: 'Администратор не может устанавливать пароль' })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } })
|
||||
if (!user) return reply.code(404).send({ error: 'Пользователь не найден' })
|
||||
if (user.passwordHash) return reply.code(409).send({ error: 'Пароль уже установлен' })
|
||||
|
||||
const password = String(request.body?.password || '')
|
||||
const passwordErr = validatePassword(password)
|
||||
if (passwordErr) return reply.code(400).send({ error: passwordErr })
|
||||
|
||||
const passwordHash = await hashPassword(password)
|
||||
await prisma.user.update({ where: { id: userId }, data: { passwordHash } })
|
||||
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
fastify.post('/api/me/change-password', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
if (isAdminEmail(request.user.email)) {
|
||||
return reply.code(403).send({ error: 'Администратор не может менять пароль' })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } })
|
||||
if (!user) return reply.code(404).send({ error: 'Пользователь не найден' })
|
||||
if (!user.passwordHash)
|
||||
return reply.code(400).send({ error: 'Пароль не установлен. Используйте установку пароля.' })
|
||||
|
||||
const oldPassword = String(request.body?.oldPassword || '')
|
||||
const valid = await comparePassword(oldPassword, user.passwordHash)
|
||||
if (!valid) return reply.code(401).send({ error: 'Неверный текущий пароль' })
|
||||
|
||||
const newPassword = String(request.body?.newPassword || '')
|
||||
const passwordErr = validatePassword(newPassword)
|
||||
if (passwordErr) return reply.code(400).send({ error: passwordErr })
|
||||
|
||||
const passwordHash = await hashPassword(newPassword)
|
||||
await prisma.user.update({ where: { id: userId }, data: { passwordHash } })
|
||||
|
||||
return { ok: true }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { prisma } from '../lib/prisma.js'
|
||||
import { mapUserForClient } from './auth.js'
|
||||
|
||||
export async function registerAuthSessionRoutes(fastify) {
|
||||
fastify.get('/api/me', { preHandler: [fastify.authenticate] }, async (request) => {
|
||||
const userId = request.user.sub
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } })
|
||||
if (!user) return { user: null }
|
||||
return { user: mapUserForClient(user) }
|
||||
})
|
||||
|
||||
fastify.get('/api/me/auth-methods', { preHandler: [fastify.authenticate] }, async (request) => {
|
||||
const userId = request.user.sub
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { oauthAccounts: { select: { provider: true } } },
|
||||
})
|
||||
if (!user) return { methods: [] }
|
||||
|
||||
const providers = user.oauthAccounts.map((a) => a.provider)
|
||||
return {
|
||||
methods: [
|
||||
{ type: 'password', active: Boolean(user.passwordHash) },
|
||||
{ type: 'vk', active: providers.includes('vk') },
|
||||
{ type: 'yandex', active: providers.includes('yandex') },
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
+1
-102
@@ -11,7 +11,7 @@ import {
|
||||
import { prisma } from '../lib/prisma.js'
|
||||
import { checkLoginRateLimit } from '../lib/rate-limit.js'
|
||||
|
||||
function mapUserForClient(user) {
|
||||
export function mapUserForClient(user) {
|
||||
const adminEmail = normalizeEmail(process.env.ADMIN_EMAIL)
|
||||
const userEmail = normalizeEmail(user.email)
|
||||
return {
|
||||
@@ -171,107 +171,6 @@ export async function registerAuthRoutes(fastify) {
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
fastify.get('/api/me', { preHandler: [fastify.authenticate] }, async (request) => {
|
||||
const userId = request.user.sub
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } })
|
||||
if (!user) return { user: null }
|
||||
return { user: mapUserForClient(user) }
|
||||
})
|
||||
|
||||
fastify.get('/api/me/auth-methods', { preHandler: [fastify.authenticate] }, async (request) => {
|
||||
const userId = request.user.sub
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { oauthAccounts: { select: { provider: true } } },
|
||||
})
|
||||
if (!user) return { methods: [] }
|
||||
|
||||
const providers = user.oauthAccounts.map((a) => a.provider)
|
||||
return {
|
||||
methods: [
|
||||
{ type: 'password', active: Boolean(user.passwordHash) },
|
||||
{ type: 'vk', active: providers.includes('vk') },
|
||||
{ type: 'yandex', active: providers.includes('yandex') },
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
fastify.post('/api/me/password', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
if (isAdminEmail(request.user.email)) {
|
||||
return reply.code(403).send({ error: 'Администратор не может устанавливать пароль' })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } })
|
||||
if (!user) return reply.code(404).send({ error: 'Пользователь не найден' })
|
||||
if (user.passwordHash) return reply.code(409).send({ error: 'Пароль уже установлен' })
|
||||
|
||||
const password = String(request.body?.password || '')
|
||||
const passwordErr = validatePassword(password)
|
||||
if (passwordErr) return reply.code(400).send({ error: passwordErr })
|
||||
|
||||
const passwordHash = await hashPassword(password)
|
||||
await prisma.user.update({ where: { id: userId }, data: { passwordHash } })
|
||||
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
fastify.post('/api/me/change-password', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
if (isAdminEmail(request.user.email)) {
|
||||
return reply.code(403).send({ error: 'Администратор не может менять пароль' })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } })
|
||||
if (!user) return reply.code(404).send({ error: 'Пользователь не найден' })
|
||||
if (!user.passwordHash) return reply.code(400).send({ error: 'Пароль не установлен. Используйте установку пароля.' })
|
||||
|
||||
const oldPassword = String(request.body?.oldPassword || '')
|
||||
const valid = await comparePassword(oldPassword, user.passwordHash)
|
||||
if (!valid) return reply.code(401).send({ error: 'Неверный текущий пароль' })
|
||||
|
||||
const newPassword = String(request.body?.newPassword || '')
|
||||
const passwordErr = validatePassword(newPassword)
|
||||
if (passwordErr) return reply.code(400).send({ error: passwordErr })
|
||||
|
||||
const passwordHash = await hashPassword(newPassword)
|
||||
await prisma.user.update({ where: { id: userId }, data: { passwordHash } })
|
||||
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
fastify.delete('/api/me/oauth/:provider', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
const provider = request.params?.provider
|
||||
|
||||
if (isAdminEmail(request.user.email)) {
|
||||
return reply.code(403).send({ error: 'Администратор не может отвязывать OAuth' })
|
||||
}
|
||||
if (provider !== 'vk' && provider !== 'yandex') {
|
||||
return reply.code(400).send({ error: 'Неизвестный провайдер' })
|
||||
}
|
||||
|
||||
const oauth = await prisma.oAuthAccount.findFirst({
|
||||
where: { userId, provider },
|
||||
})
|
||||
if (!oauth) return reply.code(404).send({ error: 'Аккаунт не привязан' })
|
||||
|
||||
const remainingOAuth = await prisma.oAuthAccount.count({
|
||||
where: { userId, provider: { not: provider } },
|
||||
})
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { passwordHash: true },
|
||||
})
|
||||
if (!currentUser?.passwordHash && remainingOAuth === 0) {
|
||||
return reply.code(400).send({ error: 'Нельзя удалить последний метод входа' })
|
||||
}
|
||||
|
||||
await prisma.oAuthAccount.delete({ where: { id: oauth.id } })
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
|
||||
fastify.patch('/api/me/profile', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||
const userId = request.user.sub
|
||||
const nameRaw = request.body?.displayName
|
||||
|
||||
Reference in New Issue
Block a user