mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 21:53:08 +00:00
invite and invite success pages
This commit is contained in:
@@ -135,6 +135,19 @@
|
|||||||
"submit": "Weiter"
|
"submit": "Weiter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"invite": {
|
||||||
|
"title": "Benutzer einladen",
|
||||||
|
"description": "Geben Sie die E-Mail-Adresse des Benutzers ein, den Sie einladen möchten.",
|
||||||
|
"info": "Der Benutzer erhält eine E-Mail mit einem Link, um sich zu registrieren.",
|
||||||
|
"submit": "Einladen",
|
||||||
|
"success": {
|
||||||
|
"title": "Einladung erfolgreich",
|
||||||
|
"description": "Der Benutzer wurde erfolgreich eingeladen.",
|
||||||
|
"verified": "Der Benutzer wurde eingeladen und hat seine E-Mail bereits verifiziert.",
|
||||||
|
"notVerifiedYet": "Der Benutzer wurde eingeladen. Er erhält eine E-Mail mit weiteren Anweisungen.",
|
||||||
|
"submit": "Weiteren Benutzer einladen"
|
||||||
|
}
|
||||||
|
},
|
||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Willkommen {user}!",
|
"title": "Willkommen {user}!",
|
||||||
"description": "Sie sind angemeldet."
|
"description": "Sie sind angemeldet."
|
||||||
|
|||||||
@@ -135,6 +135,19 @@
|
|||||||
"submit": "Continue"
|
"submit": "Continue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"invite": {
|
||||||
|
"title": "Invite User",
|
||||||
|
"description": "Provide the email address and the name of the user you want to invite.",
|
||||||
|
"info": "The user will receive an email with further instructions.",
|
||||||
|
"submit": "Continue",
|
||||||
|
"success": {
|
||||||
|
"title": "Invite User",
|
||||||
|
"description": "The email has successfully been sent.",
|
||||||
|
"verified": "The user has been invited and has already verified his email.",
|
||||||
|
"notVerifiedYet": "The user has been invited. They will receive an email with further instructions.",
|
||||||
|
"submit": "Invite another user"
|
||||||
|
}
|
||||||
|
},
|
||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Welcome {user}!",
|
"title": "Welcome {user}!",
|
||||||
"description": "You are signed in."
|
"description": "You are signed in."
|
||||||
|
|||||||
@@ -135,6 +135,19 @@
|
|||||||
"submit": "Continuar"
|
"submit": "Continuar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"invite": {
|
||||||
|
"title": "Invitar usuario",
|
||||||
|
"description": "Introduce el correo electrónico del usuario que deseas invitar.",
|
||||||
|
"info": "El usuario recibirá un correo electrónico con un enlace para completar el registro.",
|
||||||
|
"submit": "Invitar usuario",
|
||||||
|
"success": {
|
||||||
|
"title": "¡Usuario invitado!",
|
||||||
|
"description": "El usuario ha sido invitado.",
|
||||||
|
"verified": "El usuario ha sido invitado y ya ha verificado su correo electrónico.",
|
||||||
|
"notVerifiedYet": "El usuario ha sido invitado. Recibirá un correo electrónico con más instrucciones.",
|
||||||
|
"submit": "Invitar a otro usuario"
|
||||||
|
}
|
||||||
|
},
|
||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "¡Bienvenido {user}!",
|
"title": "¡Bienvenido {user}!",
|
||||||
"description": "Has iniciado sesión."
|
"description": "Has iniciado sesión."
|
||||||
|
|||||||
@@ -135,6 +135,19 @@
|
|||||||
"submit": "Continua"
|
"submit": "Continua"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"invite": {
|
||||||
|
"title": "Invita Utente",
|
||||||
|
"description": "Inserisci l'indirizzo email dell'utente che desideri invitare.",
|
||||||
|
"info": "L'utente riceverà un'email con ulteriori istruzioni.",
|
||||||
|
"submit": "Invita Utente",
|
||||||
|
"success": {
|
||||||
|
"title": "Invito inviato",
|
||||||
|
"description": "L'utente è stato invitato con successo.",
|
||||||
|
"verified": "L'utente è stato invitato e ha già verificato la sua email.",
|
||||||
|
"notVerifiedYet": "L'utente è stato invitato. Riceverà un'email con ulteriori istruzioni.",
|
||||||
|
"submit": "Invita un altro utente"
|
||||||
|
}
|
||||||
|
},
|
||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Benvenuto {user}!",
|
"title": "Benvenuto {user}!",
|
||||||
"description": "Sei connesso."
|
"description": "Sei connesso."
|
||||||
|
|||||||
54
apps/login/src/app/(login)/invite/page.tsx
Normal file
54
apps/login/src/app/(login)/invite/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Alert, AlertType } from "@/components/alert";
|
||||||
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
|
import { InviteForm } from "@/components/invite-form";
|
||||||
|
import {
|
||||||
|
getBrandingSettings,
|
||||||
|
getDefaultOrg,
|
||||||
|
getPasswordComplexitySettings,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const locale = getLocale();
|
||||||
|
const t = await getTranslations({ locale, namespace: "invite" });
|
||||||
|
|
||||||
|
let { firstname, lastname, email, organization } = searchParams;
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
const org = await getDefaultOrg();
|
||||||
|
if (!org) {
|
||||||
|
throw new Error("No default organization found");
|
||||||
|
}
|
||||||
|
|
||||||
|
organization = org.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordComplexitySettings =
|
||||||
|
await getPasswordComplexitySettings(organization);
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(organization);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{t("title")}</h1>
|
||||||
|
<p className="ztdl-p">{t("description")}</p>
|
||||||
|
|
||||||
|
<Alert type={AlertType.INFO}>{t("info")}</Alert>
|
||||||
|
|
||||||
|
{passwordComplexitySettings && (
|
||||||
|
<InviteForm
|
||||||
|
organization={organization}
|
||||||
|
firstname={firstname}
|
||||||
|
lastname={lastname}
|
||||||
|
email={email}
|
||||||
|
></InviteForm>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
apps/login/src/app/(login)/invite/success/page.tsx
Normal file
71
apps/login/src/app/(login)/invite/success/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Alert, AlertType } from "@/components/alert";
|
||||||
|
import { Button, ButtonVariants } from "@/components/button";
|
||||||
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
|
import { getBrandingSettings, getDefaultOrg, getUserByID } from "@/lib/zitadel";
|
||||||
|
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const locale = getLocale();
|
||||||
|
const t = await getTranslations({ locale, namespace: "invite" });
|
||||||
|
|
||||||
|
let { userId, organization } = searchParams;
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
const org = await getDefaultOrg();
|
||||||
|
if (!org) {
|
||||||
|
throw new Error("No default organization found");
|
||||||
|
}
|
||||||
|
|
||||||
|
organization = org.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(organization);
|
||||||
|
|
||||||
|
let user: User | undefined;
|
||||||
|
let human: HumanUser | undefined;
|
||||||
|
if (userId) {
|
||||||
|
const userResponse = await getUserByID(userId);
|
||||||
|
if (userResponse) {
|
||||||
|
user = userResponse.user;
|
||||||
|
if (user?.type.case === "human") {
|
||||||
|
human = user.type.value as HumanUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{t("success.title")}</h1>
|
||||||
|
<p className="ztdl-p">{t("success.description")}</p>
|
||||||
|
{user && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={user.preferredLoginName}
|
||||||
|
displayName={human?.profile?.displayName}
|
||||||
|
showDropdown={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{human?.email?.isVerified ? (
|
||||||
|
<Alert type={AlertType.INFO}>{t("success.verified")}</Alert>
|
||||||
|
) : (
|
||||||
|
<Alert type={AlertType.INFO}>{t("success.notVerifiedYet")}</Alert>
|
||||||
|
)}
|
||||||
|
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
||||||
|
<span></span>
|
||||||
|
<Link href="/invite">
|
||||||
|
<Button type="submit" variant={ButtonVariants.Primary}>
|
||||||
|
{t("success.submit")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { RegisterFormWithoutPassword } from "@/components/register-form-without-
|
|||||||
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
|
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
getDefaultOrg,
|
||||||
getLegalAndSupportSettings,
|
getLegalAndSupportSettings,
|
||||||
getPasswordComplexitySettings,
|
getPasswordComplexitySettings,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
@@ -16,11 +17,16 @@ export default async function Page({
|
|||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "register" });
|
const t = await getTranslations({ locale, namespace: "register" });
|
||||||
|
|
||||||
const { firstname, lastname, email, organization, authRequestId } =
|
let { firstname, lastname, email, organization, authRequestId } =
|
||||||
searchParams;
|
searchParams;
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
// TODO: get default organization
|
const org = await getDefaultOrg();
|
||||||
|
if (!org) {
|
||||||
|
throw new Error("No default organization found");
|
||||||
|
}
|
||||||
|
|
||||||
|
organization = org.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPassword = !!(firstname && lastname && email);
|
const setPassword = !!(firstname && lastname && email);
|
||||||
|
|||||||
140
apps/login/src/components/invite-form.tsx
Normal file
140
apps/login/src/components/invite-form.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { inviteUser } from "@/lib/server/invite";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FieldValues, useForm } from "react-hook-form";
|
||||||
|
import { Alert } from "./alert";
|
||||||
|
import { BackButton } from "./back-button";
|
||||||
|
import { Button, ButtonVariants } from "./button";
|
||||||
|
import { TextInput } from "./input";
|
||||||
|
import { Spinner } from "./spinner";
|
||||||
|
|
||||||
|
type Inputs =
|
||||||
|
| {
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
| FieldValues;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
firstname?: string;
|
||||||
|
lastname?: string;
|
||||||
|
email?: string;
|
||||||
|
organization?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InviteForm({
|
||||||
|
email,
|
||||||
|
firstname,
|
||||||
|
lastname,
|
||||||
|
organization,
|
||||||
|
}: Props) {
|
||||||
|
const t = useTranslations("register");
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||||
|
mode: "onBlur",
|
||||||
|
defaultValues: {
|
||||||
|
email: email ?? "",
|
||||||
|
firstName: firstname ?? "",
|
||||||
|
lastname: lastname ?? "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function submitAndContinue(values: Inputs) {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await inviteUser({
|
||||||
|
email: values.email,
|
||||||
|
firstName: values.firstname,
|
||||||
|
lastName: values.lastname,
|
||||||
|
organization: organization,
|
||||||
|
}).catch(() => {
|
||||||
|
setError("Could not create invitation Code");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (response && typeof response === "object" && "error" in response) {
|
||||||
|
setError(response.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
setError("Could not create invitation Code");
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
params.append("userId", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/invite/success?` + params);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { errors } = formState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="w-full">
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div className="col-span-2">
|
||||||
|
<TextInput
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
required
|
||||||
|
{...register("email", { required: "This field is required" })}
|
||||||
|
label="E-mail"
|
||||||
|
error={errors.email?.message as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<TextInput
|
||||||
|
type="firstname"
|
||||||
|
autoComplete="firstname"
|
||||||
|
required
|
||||||
|
{...register("firstname", { required: "This field is required" })}
|
||||||
|
label="First name"
|
||||||
|
error={errors.firstname?.message as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<TextInput
|
||||||
|
type="lastname"
|
||||||
|
autoComplete="lastname"
|
||||||
|
required
|
||||||
|
{...register("lastname", { required: "This field is required" })}
|
||||||
|
label="Last name"
|
||||||
|
error={errors.lastname?.message as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{error}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
||||||
|
<BackButton />
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant={ButtonVariants.Primary}
|
||||||
|
disabled={loading || !formState.isValid}
|
||||||
|
onClick={handleSubmit(submitAndContinue)}
|
||||||
|
>
|
||||||
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
|
{t("submit")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -162,9 +162,7 @@ export function RegisterFormWithoutPassword({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="mt-4 ztdl-p mb-6 block text-text-light-secondary-500 dark:text-text-dark-secondary-500">
|
<p className="mt-4 ztdl-p mb-6 block text-left">{t("selectMethod")}</p>
|
||||||
{t("selectMethod")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
<AuthenticationMethodRadio
|
<AuthenticationMethodRadio
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function UserAvatar({
|
|||||||
loginName={loginName ?? ""}
|
loginName={loginName ?? ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-4 text-14px max-w-[250px] text-ellipsis overflow-hidden">
|
<span className="ml-4 pr-4 text-14px max-w-[250px] text-ellipsis overflow-hidden">
|
||||||
{loginName}
|
{loginName}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex-grow"></span>
|
<span className="flex-grow"></span>
|
||||||
|
|||||||
41
apps/login/src/lib/server/invite.ts
Normal file
41
apps/login/src/lib/server/invite.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { addHumanUser, createInviteCode } from "@/lib/zitadel";
|
||||||
|
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
|
|
||||||
|
type InviteUserCommand = {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
password?: string;
|
||||||
|
organization?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegisterUserResponse = {
|
||||||
|
userId: string;
|
||||||
|
sessionId: string;
|
||||||
|
factors: Factors | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function inviteUser(command: InviteUserCommand) {
|
||||||
|
const human = await addHumanUser({
|
||||||
|
email: command.email,
|
||||||
|
firstName: command.firstName,
|
||||||
|
lastName: command.lastName,
|
||||||
|
password: command.password ? command.password : undefined,
|
||||||
|
organization: command.organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!human) {
|
||||||
|
return { error: "Could not create user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeResponse = await createInviteCode(human.userId);
|
||||||
|
|
||||||
|
if (!codeResponse || !human) {
|
||||||
|
return { error: "Could not create invite code" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return human.userId;
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
SearchQuery,
|
SearchQuery,
|
||||||
SearchQuerySchema,
|
SearchQuerySchema,
|
||||||
} from "@zitadel/proto/zitadel/user/v2/query_pb";
|
} from "@zitadel/proto/zitadel/user/v2/query_pb";
|
||||||
|
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { unstable_cache } from "next/cache";
|
import { unstable_cache } from "next/cache";
|
||||||
import { PROVIDER_MAPPING } from "./idp";
|
import { PROVIDER_MAPPING } from "./idp";
|
||||||
|
|
||||||
@@ -300,6 +301,41 @@ export async function getUserByID(userId: string) {
|
|||||||
return userService.getUserByID({ userId }, {});
|
return userService.getUserByID({ userId }, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifyInviteCode(
|
||||||
|
userId: string,
|
||||||
|
verificationCode: string,
|
||||||
|
) {
|
||||||
|
return userService.verifyInviteCode({ userId, verificationCode }, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resendInviteCode(userId: string) {
|
||||||
|
return userService.resendInviteCode({ userId }, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createInviteCode(userId: string, host?: string) {
|
||||||
|
let medium = create(SendInviteCodeSchema, {
|
||||||
|
applicationName: "Typescript Login",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
medium = {
|
||||||
|
...medium,
|
||||||
|
urlTemplate: `https://${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return userService.createInviteCode(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
verification: {
|
||||||
|
case: "sendCode",
|
||||||
|
value: medium,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function listUsers({
|
export async function listUsers({
|
||||||
loginName,
|
loginName,
|
||||||
userName,
|
userName,
|
||||||
@@ -370,6 +406,24 @@ export async function listUsers({
|
|||||||
return userService.listUsers({ queries: queries });
|
return userService.listUsers({ queries: queries });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDefaultOrg() {
|
||||||
|
return orgService
|
||||||
|
.listOrganizations(
|
||||||
|
{
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
case: "defaultQuery",
|
||||||
|
value: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
.then((resp) => resp.result[0]);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getOrgsByDomain(domain: string) {
|
export async function getOrgsByDomain(domain: string) {
|
||||||
return orgService.listOrganizations(
|
return orgService.listOrganizations(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ztdl-p {
|
.ztdl-p {
|
||||||
@apply text-sm text-text-light-secondary-500 dark:text-text-dark-secondary-500;
|
@apply text-sm text-center text-text-light-secondary-500 dark:text-text-dark-secondary-500 text-center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user