Uwierzytelnianie członków witryny
Uwierzytelniaj członków witryny przez backendowe mutacje GraphQL siteMember wywoływane przez bramę - rejestracja, logowanie, odświeżanie, wylogowanie, reset hasła i weryfikacja email. Slim SDK nie ma helperów auth; sesją zarządzasz sam.
Przegląd
Członkowie witryny to końcowi użytkownicy opublikowanej strony - osoby, które się rejestrują, logują i mają konto. Członek to rekord modelu treści w Twoim workspace, identyfikowany przez modelSlug (na przykład members) oraz identity (zwykle email). Nie ma specjalnej tabeli użytkowników - członkowie to rekordy.
Uwierzytelnianie członków to działająca funkcja backendu, udostępniona przez przestrzeń mutacji GraphQL siteMember. Wywołujesz ją przez bramę za pomocą createCmssyClient(cmssy).query(). Slim SDK nie dostarcza helperów auth - żadnej trasy auth, middleware ani czytnika sesji. Flow i sesja należą do Ciebie.
Sesją zarządzasz Ty. login i refresh zwracają surowe accessToken i refreshToken - backend nie ustawia ciasteczka. Twoja aplikacja decyduje, gdzie żyją (ciasteczko httpOnly ustawiane przez Twoją trasę to bezpieczny domyślny wybór) i dołącza Authorization: Bearer <accessToken> przy uwierzytelnionych wywołaniach.
1. Klient bramy
Utwórz jedną instancję createCmssyClient i trzymaj obok niej każdy dokument mutacji. Każda mutacja żyje w przestrzeni siteMember.
// lib/cmssy-members.ts
import { createCmssyClient } from "@cmssy/react";
import { cmssy } from "@/cmssy.config";
// One gateway client. Every siteMember mutation goes through client.query().
export const client = createCmssyClient(cmssy);
export const REGISTER = `mutation Register($input: SiteMemberRegisterInput!) {
siteMember { register(input: $input) { success message } }
}`;
export const LOGIN = `mutation Login($input: SiteMemberLoginInput!) {
siteMember {
login(input: $input) {
success
message
accessToken
refreshToken
accessTokenExpiresIn
}
}
}`;
export const REFRESH = `mutation Refresh($refreshToken: String!) {
siteMember {
refresh(refreshToken: $refreshToken) {
success
message
accessToken
refreshToken
accessTokenExpiresIn
}
}
}`;
export const LOGOUT = `mutation Logout($refreshToken: String!) {
siteMember { logout(refreshToken: $refreshToken) { success message } }
}`;
export const LOGOUT_EVERYWHERE = `mutation LogoutEverywhere {
siteMember { logoutEverywhere { success message } }
}`;
export const FORGOT_PASSWORD = `mutation ForgotPassword($modelSlug: String!, $identity: String!) {
siteMember { forgotPassword(modelSlug: $modelSlug, identity: $identity) { success message } }
}`;
export const RESET_PASSWORD = `mutation ResetPassword($token: String!, $newPassword: String!) {
siteMember { resetPassword(token: $token, newPassword: $newPassword) { success message } }
}`;
export const VERIFY_EMAIL = `mutation VerifyEmail($token: String!) {
siteMember { verifyEmail(token: $token) { success message } }
}`;2. Rejestracja
register tworzy rekord członka z modelSlug, identity i password; dodatkowe pola profilu trafiają do fields. Zwraca { success, message }, a gdy wymagana jest weryfikacja email, wysyła email weryfikacyjny.
// app/api/auth/register/route.ts
import { client, REGISTER } from "@/lib/cmssy-members";
export async function POST(request: Request) {
const { email, password, name } = await request.json();
const { siteMember } = await client.query(REGISTER, {
input: {
modelSlug: "members",
identity: email,
password,
fields: { name },
},
});
return Response.json(siteMember.register); // { success, message }
}3. Zaloguj i zapisz sesję
login zwraca { success, message, accessToken, refreshToken, accessTokenExpiresIn }. Backend nie ustawia ciasteczka - to Twoja trasa zapisuje tokeny. Poniżej trafiają do ciasteczek httpOnly kontrolowanych przez Twoją aplikację.
// app/api/auth/login/route.ts
import { cookies } from "next/headers";
import { client, LOGIN } from "@/lib/cmssy-members";
export async function POST(request: Request) {
const { email, password } = await request.json();
const { siteMember } = await client.query(LOGIN, {
input: { modelSlug: "members", identity: email, password },
});
const res = siteMember.login;
if (!res.success || !res.accessToken) {
return Response.json({ ok: false, message: res.message }, { status: 401 });
}
// You own the session - store the raw tokens yourself. The backend sets no cookie.
const jar = await cookies();
jar.set("member_access", res.accessToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: res.accessTokenExpiresIn ?? 900,
});
jar.set("member_refresh", res.refreshToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
});
return Response.json({ ok: true });
}4. Uwierzytelnione żądania
Każda operacja w zakresie członka potrzebuje access tokenu. Odczytaj go ze swojego magazynu i wyślij jako Authorization: Bearer <accessToken>; backend rozwiązuje go do zalogowanego członka. logoutEverywhere to jedno z takich wywołań - unieważnia wszystkie sesje członka.
// app/api/auth/logout-everywhere/route.ts
import { cookies } from "next/headers";
import { client, LOGOUT_EVERYWHERE } from "@/lib/cmssy-members";
export async function POST() {
const accessToken = (await cookies()).get("member_access")?.value;
if (!accessToken) return Response.json({ ok: false }, { status: 401 });
// Authenticated call: attach the access token as a Bearer header.
const { siteMember } = await client.query(LOGOUT_EVERYWHERE, {}, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return Response.json(siteMember.logoutEverywhere);
}5. Odświeżanie
refresh wymienia refreshToken na świeżą parę tokenów (ten sam kształt co login). Zrotuj oba ciasteczka nowymi wartościami; jeśli się nie powiedzie, wyczyść sesję.
// app/api/auth/refresh/route.ts
import { cookies } from "next/headers";
import { client, REFRESH } from "@/lib/cmssy-members";
export async function POST() {
const jar = await cookies();
const refreshToken = jar.get("member_refresh")?.value;
if (!refreshToken) return Response.json({ ok: false }, { status: 401 });
const { siteMember } = await client.query(REFRESH, { refreshToken });
const res = siteMember.refresh;
if (!res.success || !res.accessToken) {
jar.delete("member_access");
jar.delete("member_refresh");
return Response.json({ ok: false }, { status: 401 });
}
// refresh rotates the pair - overwrite both cookies with the new tokens.
jar.set("member_access", res.accessToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: res.accessTokenExpiresIn ?? 900,
});
jar.set("member_refresh", res.refreshToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
});
return Response.json({ ok: true });
}6. Wylogowanie
logout unieważnia pojedynczy refreshToken. Wywołaj go, a potem wyczyść własne ciasteczka.
// app/api/auth/logout/route.ts
import { cookies } from "next/headers";
import { client, LOGOUT } from "@/lib/cmssy-members";
export async function POST() {
const jar = await cookies();
const refreshToken = jar.get("member_refresh")?.value;
if (refreshToken) {
await client.query(LOGOUT, { refreshToken });
}
jar.delete("member_access");
jar.delete("member_refresh");
return Response.json({ ok: true });
}7. Reset hasła
forgotPassword(modelSlug, identity) wysyła link resetu i zawsze zgłasza sukces, więc nikt nie sprawdzi, które konta istnieją. resetPassword(token, newPassword) zużywa token z tego linku i ustawia nowe hasło.
// forgot password - always reports success (no account enumeration)
await client.query(FORGOT_PASSWORD, { modelSlug: "members", identity: email });
// reset password - token comes from the emailed link (/reset-password?token=...)
await client.query(RESET_PASSWORD, { token, newPassword });8. Weryfikacja email
verifyEmail(token) zużywa token z linku weryfikacyjnego. Gdy workspace wymaga weryfikacji, logowanie pozostaje zablokowane do czasu zweryfikowania członka.
// verify email - token comes from the verification link (/verify-email?token=...)
await client.query(VERIFY_EMAIL, { token });Referencja mutacji
Każda mutacja żyje w przestrzeni siteMember. login i refresh zwracają tokeny; reszta zwraca { success, message }.
| Mutacja | Argumenty | Zwraca |
|---|---|---|
register | input: { modelSlug, identity, password, fields } | { success, message } |
login | input: { modelSlug, identity, password } | { success, message, accessToken, refreshToken, accessTokenExpiresIn } |
refresh | refreshToken | { success, message, accessToken, refreshToken, accessTokenExpiresIn } |
logout | refreshToken | { success, message } |
logoutEverywhere | - (wymaga Bearer) | { success, message } |
forgotPassword | modelSlug, identity | { success, message } |
resetPassword | token, newPassword | { success, message } |
verifyEmail | token | { success, message } |