ФУНКЦИИ - NODEDC LAUNCHER: регистрация по инвайту
This commit is contained in:
@@ -461,6 +461,21 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||
};
|
||||
}
|
||||
|
||||
function prepareInviteRegistration(token, payload = {}) {
|
||||
const data = readData();
|
||||
const result = applyInviteRegistration(data, token, payload, { commit: false });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function commitInviteRegistration(token, payload = {}, provisioning) {
|
||||
const data = readData();
|
||||
const result = applyInviteRegistration(data, token, payload, { commit: true, provisioning });
|
||||
|
||||
await writeData(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function acceptInvite(token, identity) {
|
||||
const data = readData();
|
||||
const invite = findInviteByToken(data, token);
|
||||
@@ -997,8 +1012,10 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||
deleteMembership,
|
||||
deleteService,
|
||||
acceptInvite,
|
||||
commitInviteRegistration,
|
||||
getInviteByToken,
|
||||
getSnapshot,
|
||||
prepareInviteRegistration,
|
||||
readData,
|
||||
replaceData,
|
||||
reorderServices,
|
||||
@@ -1169,6 +1186,94 @@ function toPublicClient(client) {
|
||||
};
|
||||
}
|
||||
|
||||
function applyInviteRegistration(data, token, payload, { commit, provisioning = null } = {}) {
|
||||
const invite = findInviteByToken(data, token);
|
||||
const client = findById(data.clients, invite.clientId, "client");
|
||||
const now = isoNow();
|
||||
const email = invite.email.toLowerCase();
|
||||
const name = optionalString(payload?.name, email.split("@")[0]);
|
||||
|
||||
validateInviteCanBeRegistered(invite);
|
||||
|
||||
let user = data.users.find((item) => item.email.toLowerCase() === email);
|
||||
|
||||
if (user?.authentikUserId && !provisioning) {
|
||||
throw new Error("Аккаунт уже существует. Войдите под почтой инвайта.");
|
||||
}
|
||||
|
||||
if (user) {
|
||||
user.name = name;
|
||||
user.globalStatus = "active";
|
||||
user.authentikUserId = provisioning?.authentikUserId ?? user.authentikUserId ?? null;
|
||||
user.updatedAt = now;
|
||||
} else {
|
||||
user = {
|
||||
id: uniqueId(data.users, "user", email),
|
||||
authentikUserId: provisioning?.authentikUserId ?? null,
|
||||
name,
|
||||
email,
|
||||
phone: null,
|
||||
position: null,
|
||||
notes: `Создан через публичную регистрацию по инвайту клиента ${client.name}.`,
|
||||
avatarUrl: null,
|
||||
globalStatus: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
data.users.push(user);
|
||||
}
|
||||
|
||||
let membership = data.memberships.find((item) => item.clientId === invite.clientId && item.userId === user.id);
|
||||
|
||||
if (membership) {
|
||||
membership.role = invite.role;
|
||||
membership.status = "active";
|
||||
membership.updatedAt = now;
|
||||
} else {
|
||||
membership = {
|
||||
id: uniqueId(data.memberships, "mem", `${invite.clientId}-${email}`),
|
||||
clientId: invite.clientId,
|
||||
userId: user.id,
|
||||
role: invite.role,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
data.memberships.push(membership);
|
||||
}
|
||||
|
||||
invite.status = "accepted";
|
||||
invite.updatedAt = now;
|
||||
markPendingSync(data, user, "user", email);
|
||||
|
||||
if (commit) {
|
||||
addAuditEvent(data, { id: user.id, name: user.name, email: user.email, source: "invite" }, {
|
||||
action: "Регистрация по инвайту",
|
||||
objectType: "invite",
|
||||
objectName: invite.email,
|
||||
clientId: client.id,
|
||||
result: "success",
|
||||
details: `Role: ${invite.role}`,
|
||||
});
|
||||
}
|
||||
|
||||
return { invite, client, user, membership, data };
|
||||
}
|
||||
|
||||
function validateInviteCanBeRegistered(invite) {
|
||||
if (invite.status === "accepted") {
|
||||
throw new Error("Инвайт уже принят");
|
||||
}
|
||||
|
||||
if (invite.status === "revoked") {
|
||||
throw new Error("Инвайт отозван");
|
||||
}
|
||||
|
||||
if (invite.status === "expired" || isInviteExpired(invite)) {
|
||||
throw new Error("Срок действия инвайта истёк");
|
||||
}
|
||||
}
|
||||
|
||||
function findById(items, id, label) {
|
||||
const item = items.find((candidate) => candidate.id === id);
|
||||
|
||||
|
||||
@@ -361,6 +361,52 @@ app.get("/api/invites/:token", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/invites/:token/register", asyncRoute(async (req, res) => {
|
||||
let payload;
|
||||
|
||||
try {
|
||||
payload = sanitizeInviteRegistrationPayload(req.body);
|
||||
} catch (error) {
|
||||
sendInviteApiError(res, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authentikSyncClient.isConfigured()) {
|
||||
res.status(503).json({ error: "Регистрация временно недоступна: Authentik API не настроен" });
|
||||
return;
|
||||
}
|
||||
|
||||
let draft;
|
||||
|
||||
try {
|
||||
draft = controlPlaneStore.prepareInviteRegistration(req.params.token, payload);
|
||||
} catch (error) {
|
||||
sendInviteApiError(res, error);
|
||||
return;
|
||||
}
|
||||
|
||||
const provisionedUser = await authentikSyncClient.provisionUser({
|
||||
data: draft.data,
|
||||
userId: draft.user.id,
|
||||
password: payload.password,
|
||||
});
|
||||
const result = await controlPlaneStore.commitInviteRegistration(req.params.token, payload, provisionedUser);
|
||||
const storeResult = await controlPlaneStore.markUserAuthentikProvisioned(result.user.id, provisionedUser, {
|
||||
sub: provisionedUser.authentikUserId,
|
||||
email: result.user.email,
|
||||
name: result.user.name,
|
||||
});
|
||||
|
||||
publishControlPlaneEvent("invite.registered", [result.user.id]);
|
||||
res.json({
|
||||
...result,
|
||||
user: storeResult.user,
|
||||
data: storeResult.data,
|
||||
provisioning: toProvisioningResponse(provisionedUser),
|
||||
loginUrl: buildLoginRedirectUrl("/", { forceLogin: true }),
|
||||
});
|
||||
}));
|
||||
|
||||
app.post("/api/invites/:token/accept", requireSession, asyncRoute(async (req, res) => {
|
||||
let result;
|
||||
|
||||
@@ -783,6 +829,15 @@ function sanitizeNewPassword(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function sanitizeInviteRegistrationPayload(payload) {
|
||||
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
||||
|
||||
return {
|
||||
name: name.slice(0, 120),
|
||||
password: sanitizeNewPassword(payload?.password),
|
||||
};
|
||||
}
|
||||
|
||||
function sendInviteApiError(res, error) {
|
||||
const message = error instanceof Error ? error.message : "Инвайт недоступен";
|
||||
const status =
|
||||
|
||||
Reference in New Issue
Block a user