Add account management and LDAP login
Admins can now create local agent/admin accounts and configure LDAP directly from the UI (Настройки → Аккаунты / LDAP), with login trying LDAP first and falling back to the local password. This is the first place users.role is actually enforced (requireAdminSession). Fixed a bug in the generated 0005 migration: the INSERT into __new_users selected auth_source from the old users table, which doesn't have that column yet — caused drizzle-kit migrate to fail silently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcXH24ky6zjk2UyK5oZUPH
This commit is contained in:
1 parent
8121d8aa51
commit
cb12711e91
23 files changed
+2379
-13
No files matched your search
@@ -27,6 +27,12 @@ export async function POST(request: Request) {
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
if (!user.passwordHash) {
|
||||
return NextResponse.json(
|
||||
{ error: "Этот аккаунт входит через LDAP — локальный пароль не используется" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const ok = await verifyPassword(user.passwordHash, parsed.data.currentPassword);
|
||||
if (!ok) {
|
||||
|
||||
@@ -5,6 +5,9 @@ import { z } from "zod";
|
||||
import { db } from "@/lib/db/client";
|
||||
import { verifyPassword } from "@/lib/auth/password";
|
||||
import { createSession, setSessionCookie } from "@/lib/auth/session";
|
||||
import { authenticateLdapUser } from "@/lib/ldap/client";
|
||||
import { findOrCreateUserFromLdap } from "@/lib/auth/users";
|
||||
import { getLdapSettings } from "@/lib/auth/ldap-config";
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
@@ -40,6 +43,25 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "Too many attempts — try again later" }, { status: 429 });
|
||||
}
|
||||
|
||||
// LDAP first — if configured and this login succeeds against the
|
||||
// directory, JIT-provision/reuse the local account and skip local auth
|
||||
// entirely. A directory outage or non-match returns null, never throws,
|
||||
// so it always falls through cleanly to the local password check below.
|
||||
const ldapUser = await authenticateLdapUser(email, parsed.data.password);
|
||||
if (ldapUser) {
|
||||
const ldapSettings = await getLdapSettings();
|
||||
const user = await findOrCreateUserFromLdap({
|
||||
email: ldapUser.email,
|
||||
name: ldapUser.name,
|
||||
defaultRole: ldapSettings?.defaultRole ?? "agent",
|
||||
});
|
||||
|
||||
const token = await createSession(user.id);
|
||||
await setSessionCookie(token);
|
||||
|
||||
return NextResponse.json({ ok: true, user: { id: user.id, name: user.name, role: user.role } });
|
||||
}
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: (u, { eq }) => eq(u.email, email),
|
||||
});
|
||||
@@ -52,7 +74,7 @@ export async function POST(request: Request) {
|
||||
parsed.data.password,
|
||||
);
|
||||
|
||||
if (!user || !ok) {
|
||||
if (!user || !user.passwordHash || !ok) {
|
||||
return NextResponse.json({ error: "Invalid email or password" }, { status: 401 });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
export const runtime = "nodejs";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { requireAdminSession } from "@/lib/auth/require";
|
||||
import { getLdapStatus, saveLdapSettings, disableLdap } from "@/lib/auth/ldap-config";
|
||||
import { testLdapBind } from "@/lib/ldap/client";
|
||||
|
||||
export async function GET() {
|
||||
const { session, response } = await requireAdminSession();
|
||||
if (!session) return response;
|
||||
|
||||
return NextResponse.json(await getLdapStatus());
|
||||
}
|
||||
|
||||
const configureSchema = z.object({
|
||||
host: z.string().min(1),
|
||||
port: z.coerce.number().int().positive(),
|
||||
useTls: z.boolean(),
|
||||
bindDn: z.string().min(1),
|
||||
bindPassword: z.string().min(1),
|
||||
baseDn: z.string().min(1),
|
||||
userFilter: z.string().min(1),
|
||||
listFilter: z.string().min(1),
|
||||
defaultRole: z.enum(["admin", "agent"]),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { session, response } = await requireAdminSession();
|
||||
if (!session) return response;
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = configureSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await testLdapBind(parsed.data);
|
||||
await saveLdapSettings(parsed.data);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
return NextResponse.json({ error: `Не удалось подключиться: ${message}` }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE() {
|
||||
const { session, response } = await requireAdminSession();
|
||||
if (!session) return response;
|
||||
|
||||
await disableLdap();
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export const runtime = "nodejs";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { requireAdminSession } from "@/lib/auth/require";
|
||||
import { getLdapSettings } from "@/lib/auth/ldap-config";
|
||||
import { searchLdapDirectory } from "@/lib/ldap/client";
|
||||
import { findOrCreateUserFromLdap } from "@/lib/auth/users";
|
||||
|
||||
const importSchema = z.object({ emails: z.array(z.string().email()).min(1) });
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { session, response } = await requireAdminSession();
|
||||
if (!session) return response;
|
||||
|
||||
const settings = await getLdapSettings();
|
||||
if (!settings) {
|
||||
return NextResponse.json({ error: "LDAP не настроен" }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = importSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await searchLdapDirectory();
|
||||
const wanted = new Set(parsed.data.emails.map((e) => e.toLowerCase()));
|
||||
const toImport = entries.filter((e) => wanted.has(e.email.toLowerCase()));
|
||||
|
||||
const imported = [];
|
||||
for (const entry of toImport) {
|
||||
const user = await findOrCreateUserFromLdap({
|
||||
email: entry.email,
|
||||
name: entry.name,
|
||||
defaultRole: settings.defaultRole,
|
||||
});
|
||||
imported.push(user.email);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, imported });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
return NextResponse.json({ error: `Не удалось импортировать: ${message}` }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export const runtime = "nodejs";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireAdminSession } from "@/lib/auth/require";
|
||||
import { getLdapSettings } from "@/lib/auth/ldap-config";
|
||||
import { searchLdapDirectory } from "@/lib/ldap/client";
|
||||
import { listUsers } from "@/lib/auth/users";
|
||||
|
||||
export async function GET() {
|
||||
const { session, response } = await requireAdminSession();
|
||||
if (!session) return response;
|
||||
|
||||
const settings = await getLdapSettings();
|
||||
if (!settings) {
|
||||
return NextResponse.json({ configured: false, entries: [] });
|
||||
}
|
||||
|
||||
try {
|
||||
const [entries, existing] = await Promise.all([searchLdapDirectory(), listUsers()]);
|
||||
const existingEmails = new Set(existing.map((u) => u.email.toLowerCase()));
|
||||
const fresh = entries.filter((e) => !existingEmails.has(e.email.toLowerCase()));
|
||||
return NextResponse.json({ configured: true, entries: fresh });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
return NextResponse.json({ error: `Не удалось получить список из LDAP: ${message}` }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export const runtime = "nodejs";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { requireAdminSession } from "@/lib/auth/require";
|
||||
import { updateUserRole, deleteUser } from "@/lib/auth/users";
|
||||
|
||||
const patchSchema = z.object({ role: z.enum(["admin", "agent"]) });
|
||||
|
||||
export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { session, response } = await requireAdminSession();
|
||||
if (!session) return response;
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = patchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await updateUserRole(id, parsed.data.role);
|
||||
return NextResponse.json({ user });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Не удалось изменить роль";
|
||||
return NextResponse.json({ error: message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { session, response } = await requireAdminSession();
|
||||
if (!session) return response;
|
||||
|
||||
const { id } = await params;
|
||||
if (id === session.user.id) {
|
||||
return NextResponse.json({ error: "Нельзя удалить собственный аккаунт" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteUser(id);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Не удалось удалить аккаунт";
|
||||
return NextResponse.json({ error: message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export const runtime = "nodejs";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { requireAdminSession } from "@/lib/auth/require";
|
||||
import { listUsers, createLocalUser } from "@/lib/auth/users";
|
||||
|
||||
export async function GET() {
|
||||
const { session, response } = await requireAdminSession();
|
||||
if (!session) return response;
|
||||
|
||||
return NextResponse.json({ users: await listUsers() });
|
||||
}
|
||||
|
||||
const createSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8, "Пароль должен быть не короче 8 символов"),
|
||||
role: z.enum(["admin", "agent"]),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { session, response } = await requireAdminSession();
|
||||
if (!session) return response;
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = createSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid input" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await createLocalUser(parsed.data);
|
||||
return NextResponse.json({ user }, { status: 201 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Такой email уже используется" }, { status: 409 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user