🎉 feat: initialization

This commit is contained in:
web
2025-11-26 19:56:16 -08:00
commit a801849fb2
553 changed files with 213088 additions and 0 deletions
@@ -0,0 +1,318 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Switch } from "@workspace/ui/components/switch";
import { Textarea } from "@workspace/ui/components/textarea";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
import {
getAuthMethodConfig,
updateAuthMethodConfig,
} from "@workspace/ui/services/admin/authMethod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { z } from "zod";
const appleSchema = z.object({
enabled: z.boolean(),
config: z
.object({
team_id: z.string().optional(),
key_id: z.string().optional(),
client_id: z.string().optional(),
client_secret: z.string().optional(),
redirect_url: z.string().optional(),
})
.optional(),
});
type AppleFormData = z.infer<typeof appleSchema>;
export default function AppleForm() {
const { t } = useTranslation("auth-control");
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ["getAuthMethodConfig", "apple"],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: "apple",
});
return data.data;
},
enabled: open,
});
const form = useForm<AppleFormData>({
resolver: zodResolver(appleSchema),
defaultValues: {
enabled: false,
config: {
team_id: "",
key_id: "",
client_id: "",
client_secret: "",
redirect_url: "",
},
},
});
useEffect(() => {
if (data) {
form.reset({
enabled: data.enabled,
config: {
team_id: data.config?.team_id || "",
key_id: data.config?.key_id || "",
client_id: data.config?.client_id || "",
client_secret: data.config?.client_secret || "",
redirect_url: data.config?.redirect_url || "",
},
});
}
}, [data, form]);
async function onSubmit(values: AppleFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...data,
enabled: values.enabled,
config: {
...data?.config,
...values.config,
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t("common.saveSuccess", "Saved successfully"));
refetch();
setOpen(false);
} catch (_error) {
toast.error(t("common.saveFailed", "Save failed"));
} finally {
setLoading(false);
}
}
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<div className="flex cursor-pointer items-center justify-between transition-colors">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Icon className="h-5 w-5 text-primary" icon="mdi:apple" />
</div>
<div className="flex-1">
<p className="font-medium">{t("apple.title", "Apple Sign-In")}</p>
<p className="text-muted-foreground text-sm">
{t(
"apple.description",
"Authenticate users with Apple accounts"
)}
</p>
</div>
</div>
<Icon className="size-6" icon="mdi:chevron-right" />
</div>
</SheetTrigger>
<SheetContent className="w-[500px] max-w-full md:max-w-screen-md">
<SheetHeader>
<SheetTitle>{t("apple.title", "Apple Sign-In")}</SheetTitle>
</SheetHeader>
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-24px-env(safe-area-inset-top))] px-6">
<Form {...form}>
<form
className="space-y-2 pt-4"
id="apple-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t("apple.enable", "Enable")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"apple.enableDescription",
"When enabled, users can sign in with their Apple ID"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.team_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("apple.teamId", "Team ID")}</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="ABCDE1FGHI"
value={field.value}
/>
</FormControl>
<FormDescription>
{t("apple.teamIdDescription", "Apple Developer Team ID")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.key_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("apple.keyId", "Key ID")}</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="ABC1234567"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"apple.keyIdDescription",
"Your private key ID from Apple Developer Portal"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.client_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("apple.clientId", "Service ID")}</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="com.your.app.service"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"apple.clientIdDescription",
"Apple Service ID, available from Apple Developer Portal"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.client_secret"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("apple.clientSecret", "Private Key")}
</FormLabel>
<FormControl>
<Textarea
className="h-20"
onChange={field.onChange}
placeholder={
"-----BEGIN PRIVATE KEY-----\nMIGTAgEA...\n-----END PRIVATE KEY-----"
}
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"apple.clientSecretDescription",
"Private key content (.p8 file) for authenticating with Apple"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.redirect_url"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("apple.redirectUri", "Redirect URL")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="https://your-domain.com"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"apple.redirectUriDescription",
"API address for redirect URL after successful Apple authentication. Do not end with /"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className="flex-row justify-end gap-2 pt-3">
<Button
disabled={loading}
onClick={() => setOpen(false)}
variant="outline"
>
{t("common.cancel", "Cancel")}
</Button>
<Button disabled={loading} form="apple-form" type="submit">
{loading && (
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
)}
{t("common.save", "Save")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,295 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Switch } from "@workspace/ui/components/switch";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
import {
getAuthMethodConfig,
updateAuthMethodConfig,
} from "@workspace/ui/services/admin/authMethod";
import { uid } from "radash";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { z } from "zod";
const deviceSchema = z.object({
id: z.number(),
method: z.string(),
enabled: z.boolean(),
config: z
.object({
show_ads: z.boolean().optional(),
only_real_device: z.boolean().optional(),
enable_security: z.boolean().optional(),
security_secret: z.string().optional(),
})
.optional(),
});
type DeviceFormData = z.infer<typeof deviceSchema>;
export default function DeviceForm() {
const { t } = useTranslation("auth-control");
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ["getAuthMethodConfig", "device"],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: "device",
});
return data.data;
},
enabled: open,
});
const form = useForm<DeviceFormData>({
resolver: zodResolver(deviceSchema),
defaultValues: {
id: 0,
method: "device",
enabled: false,
config: {
show_ads: false,
only_real_device: false,
enable_security: false,
security_secret: "",
},
},
});
useEffect(() => {
if (data) {
form.reset(data);
}
}, [data, form]);
async function onSubmit(values: DeviceFormData) {
setLoading(true);
try {
await updateAuthMethodConfig(values as API.UpdateAuthMethodConfigRequest);
toast.success(t("common.saveSuccess", "Saved successfully"));
refetch();
setOpen(false);
} catch (_error) {
toast.error(t("common.saveFailed", "Save failed"));
} finally {
setLoading(false);
}
}
function generateSecurityKey() {
const id = uid(32).toLowerCase();
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
form.setValue("config.security_secret", formatted);
}
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<div className="flex cursor-pointer items-center justify-between transition-colors">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Icon className="h-5 w-5 text-primary" icon="mdi:devices" />
</div>
<div className="flex-1">
<p className="font-medium">
{t("device.title", "Device Sign-In")}
</p>
<p className="text-muted-foreground text-sm">
{t("device.description", "Authenticate users with device")}
</p>
</div>
</div>
<Icon className="size-6" icon="mdi:chevron-right" />
</div>
</SheetTrigger>
<SheetContent className="w-[600px] max-w-full md:max-w-screen-md">
<SheetHeader>
<SheetTitle>{t("device.title", "Device Sign-In")}</SheetTitle>
</SheetHeader>
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-24px-env(safe-area-inset-top))] px-6">
<Form {...form}>
<form
className="space-y-2 pt-4"
id="device-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t("device.enable", "Enable")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"device.enableDescription",
"When enabled, users can sign in with device"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.show_ads"
render={({ field }) => (
<FormItem>
<FormLabel>{t("device.showAds", "Show Ads")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"device.showAdsDescription",
"When enabled, ads will be shown"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.only_real_device"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("device.blockVirtualMachine", "Block Virtual Machine")}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"device.blockVirtualMachineDescription",
"Block virtual machine login, only allow real device"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.enable_security"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("device.enableSecurity", "Enable Security")}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"device.enableSecurityDescription",
"When enabled, application requests must carry communication key"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.security_secret"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("device.communicationKey", "Communication Key")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="e.g., 12345678-1234-1234-1234-123456789abc"
suffix={
<div className="flex h-9 items-center text-nowrap bg-muted px-3">
<Icon
className="size-4 cursor-pointer"
icon="mdi:dice-multiple"
onClick={generateSecurityKey}
/>
</div>
}
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"device.communicationKeyDescription",
"The key used for secure communication between application and server"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className="flex-row justify-end gap-2 pt-3">
<Button
disabled={loading}
onClick={() => setOpen(false)}
variant="outline"
>
{t("common.cancel", "Cancel")}
</Button>
<Button disabled={loading} form="device-form" type="submit">
{loading && (
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
)}
{t("common.save", "Save")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,865 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Switch } from "@workspace/ui/components/switch";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@workspace/ui/components/tabs";
import { Textarea } from "@workspace/ui/components/textarea";
import { HTMLEditor } from "@workspace/ui/composed/editor/html";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
import {
getAuthMethodConfig,
testEmailSend,
updateAuthMethodConfig,
} from "@workspace/ui/services/admin/authMethod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { z } from "zod";
const emailSettingsSchema = z.object({
id: z.number(),
method: z.string(),
enabled: z.boolean(),
config: z
.object({
enable_verify: z.boolean(),
enable_domain_suffix: z.boolean(),
domain_suffix_list: z.string().optional(),
verify_email_template: z.string().optional(),
expiration_email_template: z.string().optional(),
maintenance_email_template: z.string().optional(),
traffic_exceed_email_template: z.string().optional(),
platform: z.string(),
platform_config: z
.object({
host: z.string().optional(),
port: z.number().optional(),
ssl: z.boolean(),
user: z.string().optional(),
pass: z.string().optional(),
from: z.string().optional(),
})
.optional(),
})
.optional(),
});
type EmailSettingsFormData = z.infer<typeof emailSettingsSchema>;
export default function EmailSettingsForm() {
const { t } = useTranslation("auth-control");
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [testEmail, setTestEmail] = useState<string>();
const { data, refetch, isFetching } = useQuery({
queryKey: ["getAuthMethodConfig", "email"],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: "email",
});
return data.data;
},
enabled: open,
});
const form = useForm<EmailSettingsFormData>({
resolver: zodResolver(emailSettingsSchema),
defaultValues: {
id: 0,
method: "email",
enabled: false,
config: {
enable_verify: false,
enable_domain_suffix: false,
domain_suffix_list: "",
verify_email_template: "",
expiration_email_template: "",
maintenance_email_template: "",
traffic_exceed_email_template: "",
platform: "smtp",
platform_config: {
host: "",
port: 587,
ssl: false,
user: "",
pass: "",
from: "",
},
},
},
});
useEffect(() => {
if (data) {
form.reset(data);
}
}, [data, form]);
async function onSubmit(values: EmailSettingsFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...values,
config: {
...values.config,
platform: "smtp",
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t("common.saveSuccess", "Saved successfully"));
refetch();
setOpen(false);
} catch (_error) {
toast.error(t("common.saveFailed", "Save failed"));
} finally {
setLoading(false);
}
}
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<div className="flex cursor-pointer items-center justify-between transition-colors">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Icon className="h-5 w-5 text-primary" icon="mdi:email-outline" />
</div>
<div className="flex-1">
<p className="font-medium">
{t("email.title", "Email Settings")}
</p>
<p className="text-muted-foreground text-sm">
{t(
"email.description",
"Configure email authentication and templates"
)}
</p>
</div>
</div>
<Icon className="size-6" icon="mdi:chevron-right" />
</div>
</SheetTrigger>
<SheetContent className="md:!max-w-screen-lg max-w-full">
<SheetHeader>
<SheetTitle>{t("email.title", "Email Settings")}</SheetTitle>
</SheetHeader>
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-24px-env(safe-area-inset-top))] px-6">
<Form {...form}>
<form
className="space-y-2 pt-4"
id="email-settings-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<Tabs className="space-y-2" defaultValue="basic">
<TabsList className="flex h-full w-full flex-wrap *:flex-auto md:flex-nowrap">
<TabsTrigger value="basic">
{t("email.basicSettings", "Basic Settings")}
</TabsTrigger>
<TabsTrigger value="smtp">
{t("email.smtpSettings", "SMTP Settings")}
</TabsTrigger>
<TabsTrigger value="verify">
{t("email.verifyTemplate", "Verify Template")}
</TabsTrigger>
<TabsTrigger value="expiration">
{t("email.expirationTemplate", "Expiration Template")}
</TabsTrigger>
<TabsTrigger value="maintenance">
{t("email.maintenanceTemplate", "Maintenance Template")}
</TabsTrigger>
<TabsTrigger value="traffic">
{t("email.trafficTemplate", "Traffic Template")}
</TabsTrigger>
</TabsList>
<TabsContent className="space-y-2" value="basic">
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t("email.enable", "Enable")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"email.enableDescription",
"When enabled, users can sign in with email"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.enable_verify"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("email.emailVerification", "Email Verification")}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"email.emailVerificationDescription",
"Require email verification for new users"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.enable_domain_suffix"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"email.emailSuffixWhitelist",
"Email Suffix Whitelist"
)}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"email.emailSuffixWhitelistDescription",
"Only allow emails from whitelisted domains"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.domain_suffix_list"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("email.whitelistSuffixes", "Whitelist Suffixes")}
</FormLabel>
<FormControl>
<Textarea
className="h-32"
onChange={field.onChange}
placeholder={t(
"email.whitelistSuffixesPlaceholder",
"gmail.com, outlook.com"
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"email.whitelistSuffixesDescription",
"One domain suffix per line"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent className="space-y-2" value="smtp">
<FormField
control={form.control}
name="config.platform_config.host"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("email.smtpServerAddress", "SMTP Server Address")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder={t(
"email.inputPlaceholder",
"Please enter"
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"email.smtpServerAddressDescription",
"The SMTP server hostname"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.platform_config.port"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("email.smtpServerPort", "SMTP Server Port")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={(value) =>
field.onChange(Number(value))
}
placeholder="587"
type="number"
value={field.value?.toString()}
/>
</FormControl>
<FormDescription>
{t(
"email.smtpServerPortDescription",
"The SMTP server port (usually 25, 465, or 587)"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.platform_config.ssl"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"email.smtpEncryptionMethod",
"SSL/TLS Encryption"
)}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"email.smtpEncryptionMethodDescription",
"Enable SSL/TLS encryption for SMTP connection"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.platform_config.user"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("email.smtpAccount", "SMTP Account")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder={t(
"email.inputPlaceholder",
"Please enter"
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"email.smtpAccountDescription",
"The SMTP authentication username"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.platform_config.pass"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("email.smtpPassword", "SMTP Password")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder={t(
"email.inputPlaceholder",
"Please enter"
)}
type="password"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"email.smtpPasswordDescription",
"The SMTP authentication password"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.platform_config.from"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("email.senderAddress", "Sender Address")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder={t(
"email.inputPlaceholder",
"Please enter"
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"email.senderAddressDescription",
"The email address that appears in the From field"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-2 border-t pt-4">
<FormLabel>
{t("email.sendTestEmail", "Send Test Email")}
</FormLabel>
<div className="flex items-center gap-2">
<EnhancedInput
onValueChange={(value) => setTestEmail(value as string)}
placeholder="test@example.com"
type="email"
value={testEmail}
/>
<Button
disabled={!testEmail || isFetching}
onClick={async () => {
if (!testEmail) return;
try {
await testEmailSend({ email: testEmail });
toast.success(
t("email.sendSuccess", "Email sent successfully")
);
} catch {
toast.error(
t("email.sendFailure", "Email send failed")
);
}
}}
type="button"
>
{t("email.sendTestEmail", "Send Test Email")}
</Button>
</div>
<p className="text-muted-foreground text-xs">
{t(
"email.sendTestEmailDescription",
"Send a test email to verify your SMTP configuration"
)}
</p>
</div>
</TabsContent>
<TabsContent className="space-y-2" value="verify">
<FormField
control={form.control}
name="config.verify_email_template"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"email.verifyEmailTemplate",
"Verify Email Template"
)}
</FormLabel>
<FormControl>
<HTMLEditor
onBlur={field.onChange}
placeholder={t(
"email.inputPlaceholder",
"Please enter"
)}
value={field.value}
/>
</FormControl>
<div className="mt-4 space-y-2 border-t pt-4">
<p className="font-medium text-muted-foreground text-sm">
{t(
"email.templateVariables.title",
"Template Variables"
)}
</p>
<div className="space-y-2 text-muted-foreground text-xs">
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.Type}}"}
</code>
<span>
{t(
"email.templateVariables.type.description",
"Email type (1: Register, 2: Reset Password)"
)}
</span>
</div>
<div className="pl-6 text-orange-600 dark:text-orange-400">
💡{" "}
{t(
"email.templateVariables.type.conditionalSyntax",
"Use conditional syntax to display different content"
)}
<br />
<code className="rounded bg-orange-50 px-1 text-xs dark:bg-orange-900/20">
{"{{if eq .Type 1}}...{{else}}...{{end}}"}
</code>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.SiteLogo}}"}
</code>
<span>
{t(
"email.templateVariables.siteLogo.description",
"Site logo URL"
)}
</span>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.SiteName}}"}
</code>
<span>
{t(
"email.templateVariables.siteName.description",
"Site name"
)}
</span>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.Expire}}"}
</code>
<span>
{t(
"email.templateVariables.expire.description",
"Code expiration time"
)}
</span>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.Code}}"}
</code>
<span>
{t(
"email.templateVariables.code.description",
"Verification code"
)}
</span>
</div>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent className="space-y-2" value="expiration">
<FormField
control={form.control}
name="config.expiration_email_template"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"email.expirationEmailTemplate",
"Expiration Email Template"
)}
</FormLabel>
<FormControl>
<HTMLEditor
onBlur={field.onChange}
placeholder={t(
"email.inputPlaceholder",
"Please enter"
)}
value={field.value}
/>
</FormControl>
<div className="mt-4 space-y-2 border-t pt-4">
<p className="font-medium text-muted-foreground text-sm">
{t(
"email.templateVariables.title",
"Template Variables"
)}
</p>
<div className="space-y-2 text-muted-foreground text-xs">
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.SiteLogo}}"}
</code>
<span>
{t(
"email.templateVariables.siteLogo.description",
"Site logo URL"
)}
</span>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.SiteName}}"}
</code>
<span>
{t(
"email.templateVariables.siteName.description",
"Site name"
)}
</span>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.ExpireDate}}"}
</code>
<span>
{t(
"email.templateVariables.expireDate.description",
"Subscription expiration date"
)}
</span>
</div>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent className="space-y-2" value="maintenance">
<FormField
control={form.control}
name="config.maintenance_email_template"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"email.maintenanceEmailTemplate",
"Maintenance Email Template"
)}
</FormLabel>
<FormControl>
<HTMLEditor
onBlur={field.onChange}
placeholder={t(
"email.inputPlaceholder",
"Please enter"
)}
value={field.value}
/>
</FormControl>
<div className="mt-4 space-y-2 border-t pt-4">
<p className="font-medium text-muted-foreground text-sm">
{t(
"email.templateVariables.title",
"Template Variables"
)}
</p>
<div className="space-y-2 text-muted-foreground text-xs">
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.SiteLogo}}"}
</code>
<span>
{t(
"email.templateVariables.siteLogo.description",
"Site logo URL"
)}
</span>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.SiteName}}"}
</code>
<span>
{t(
"email.templateVariables.siteName.description",
"Site name"
)}
</span>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.MaintenanceDate}}"}
</code>
<span>
{t(
"email.templateVariables.maintenanceDate.description",
"Maintenance date"
)}
</span>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.MaintenanceTime}}"}
</code>
<span>
{t(
"email.templateVariables.maintenanceTime.description",
"Maintenance time"
)}
</span>
</div>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent className="space-y-2" value="traffic">
<FormField
control={form.control}
name="config.traffic_exceed_email_template"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"email.trafficExceedEmailTemplate",
"Traffic Exceed Email Template"
)}
</FormLabel>
<FormControl>
<HTMLEditor
onBlur={field.onChange}
placeholder={t(
"email.inputPlaceholder",
"Please enter"
)}
value={field.value}
/>
</FormControl>
<div className="mt-4 space-y-2 border-t pt-4">
<p className="font-medium text-muted-foreground text-sm">
{t(
"email.templateVariables.title",
"Template Variables"
)}
</p>
<div className="space-y-2 text-muted-foreground text-xs">
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.SiteLogo}}"}
</code>
<span>
{t(
"email.templateVariables.siteLogo.description",
"Site logo URL"
)}
</span>
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
{"{{.SiteName}}"}
</code>
<span>
{t(
"email.templateVariables.siteName.description",
"Site name"
)}
</span>
</div>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
</Tabs>
</form>
</Form>
</ScrollArea>
<SheetFooter className="flex-row justify-end gap-2 pt-3">
<Button
disabled={loading}
onClick={() => setOpen(false)}
variant="outline"
>
{t("common.cancel", "Cancel")}
</Button>
<Button disabled={loading} form="email-settings-form" type="submit">
{loading && (
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
)}
{t("common.save", "Save")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,229 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Switch } from "@workspace/ui/components/switch";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
import {
getAuthMethodConfig,
updateAuthMethodConfig,
} from "@workspace/ui/services/admin/authMethod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { z } from "zod";
const facebookSchema = z.object({
enabled: z.boolean(),
client_id: z.string().optional(),
client_secret: z.string().optional(),
});
type FacebookFormData = z.infer<typeof facebookSchema>;
export default function FacebookForm() {
const { t } = useTranslation("auth-control");
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ["getAuthMethodConfig", "facebook"],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: "facebook",
});
return data.data;
},
enabled: open,
});
const form = useForm<FacebookFormData>({
resolver: zodResolver(facebookSchema),
defaultValues: {
enabled: false,
client_id: "",
client_secret: "",
},
});
useEffect(() => {
if (data) {
form.reset({
enabled: data.enabled,
client_id: data.config?.client_id || "",
client_secret: data.config?.client_secret || "",
});
}
}, [data, form]);
async function onSubmit(values: FacebookFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...data,
enabled: values.enabled,
config: {
...data?.config,
client_id: values.client_id,
client_secret: values.client_secret,
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t("common.saveSuccess", "Saved successfully"));
refetch();
setOpen(false);
} catch (_error) {
toast.error(t("common.saveFailed", "Save failed"));
} finally {
setLoading(false);
}
}
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<div className="flex cursor-pointer items-center justify-between transition-colors">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Icon className="h-5 w-5 text-primary" icon="mdi:facebook" />
</div>
<div className="flex-1">
<p className="font-medium">
{t("facebook.title", "Facebook Sign-In")}
</p>
<p className="text-muted-foreground text-sm">
{t(
"facebook.description",
"Authenticate users with Facebook accounts"
)}
</p>
</div>
</div>
<Icon className="size-6" icon="mdi:chevron-right" />
</div>
</SheetTrigger>
<SheetContent className="w-[500px] max-w-full md:max-w-screen-md">
<SheetHeader>
<SheetTitle>{t("facebook.title", "Facebook Sign-In")}</SheetTitle>
</SheetHeader>
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-24px-env(safe-area-inset-top))] px-6">
<Form {...form}>
<form
className="space-y-2 pt-4"
id="facebook-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t("facebook.enable", "Enable")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"facebook.enableDescription",
"When enabled, users can sign in with their Facebook account"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="client_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("facebook.clientId", "App ID")}</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="1234567890123456"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"facebook.clientIdDescription",
"Facebook App ID, available from Facebook Developer Portal"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="client_secret"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("facebook.clientSecret", "App Secret")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="1234567890abcdef1234567890abcdef"
type="password"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"facebook.clientSecretDescription",
"Facebook App Secret, available from Facebook Developer Portal"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className="flex-row justify-end gap-2 pt-3">
<Button
disabled={loading}
onClick={() => setOpen(false)}
variant="outline"
>
{t("common.cancel", "Cancel")}
</Button>
<Button disabled={loading} form="facebook-form" type="submit">
{loading && (
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
)}
{t("common.save", "Save")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,230 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Switch } from "@workspace/ui/components/switch";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
import {
getAuthMethodConfig,
updateAuthMethodConfig,
} from "@workspace/ui/services/admin/authMethod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { z } from "zod";
const githubSchema = z.object({
enabled: z.boolean(),
client_id: z.string().optional(),
client_secret: z.string().optional(),
});
type GithubFormData = z.infer<typeof githubSchema>;
export default function GithubForm() {
const { t } = useTranslation("auth-control");
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ["getAuthMethodConfig", "github"],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: "github",
});
return data.data;
},
enabled: open,
});
const form = useForm<GithubFormData>({
resolver: zodResolver(githubSchema),
defaultValues: {
enabled: false,
client_id: "",
client_secret: "",
},
});
useEffect(() => {
if (data) {
form.reset({
enabled: data.enabled,
client_id: data.config?.client_id || "",
client_secret: data.config?.client_secret || "",
});
}
}, [data, form]);
async function onSubmit(values: GithubFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...data,
enabled: values.enabled,
config: {
...data?.config,
client_id: values.client_id,
client_secret: values.client_secret,
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t("common.saveSuccess", "Saved successfully"));
refetch();
setOpen(false);
} catch (_error) {
toast.error(t("common.saveFailed", "Save failed"));
} finally {
setLoading(false);
}
}
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<div className="flex cursor-pointer items-center justify-between transition-colors">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Icon className="h-5 w-5 text-primary" icon="mdi:github" />
</div>
<div className="flex-1">
<p className="font-medium">
{t("github.title", "GitHub Sign-In")}
</p>
<p className="text-muted-foreground text-sm">
{t(
"github.description",
"Authenticate users with GitHub accounts"
)}
</p>
</div>
</div>
<Icon className="size-6" icon="mdi:chevron-right" />
</div>
</SheetTrigger>
<SheetContent className="w-[500px] max-w-full md:max-w-screen-md">
<SheetHeader>
<SheetTitle>{t("github.title", "GitHub Sign-In")}</SheetTitle>
</SheetHeader>
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-24px-env(safe-area-inset-top))] px-6">
<Form {...form}>
<form
className="space-y-2 pt-4"
id="github-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t("github.enable", "Enable")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"github.enableDescription",
"When enabled, users can sign in with their GitHub account"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="client_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("github.clientId", "Client ID")}</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="e.g., Iv1.1234567890abcdef"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"github.clientIdDescription",
"GitHub OAuth App Client ID, available from GitHub Developer Settings"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="client_secret"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("github.clientSecret", "Client Secret")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="e.g., 1234567890abcdef1234567890abcdef12345678"
type="password"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"github.clientSecretDescription",
"GitHub OAuth App Client Secret, available from GitHub Developer Settings"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className="flex-row justify-end gap-2 pt-3">
<Button
disabled={loading}
onClick={() => setOpen(false)}
variant="outline"
>
{t("common.cancel", "Cancel")}
</Button>
<Button disabled={loading} form="github-form" type="submit">
{loading && (
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
)}
{t("common.save", "Save")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,227 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Switch } from "@workspace/ui/components/switch";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
import {
getAuthMethodConfig,
updateAuthMethodConfig,
} from "@workspace/ui/services/admin/authMethod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { z } from "zod";
const googleSchema = z.object({
id: z.number(),
method: z.string().default("google").optional(),
enabled: z.boolean().default(false).optional(),
config: z
.object({
client_id: z.string().optional(),
client_secret: z.string().optional(),
})
.optional(),
});
type GoogleFormData = z.infer<typeof googleSchema>;
export default function GoogleForm() {
const { t } = useTranslation("auth-control");
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ["getAuthMethodConfig", "google"],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: "google",
});
return data.data;
},
enabled: open,
});
const form = useForm<GoogleFormData>({
resolver: zodResolver(googleSchema),
defaultValues: {
id: 0,
method: "google",
enabled: false,
config: {
client_id: "",
client_secret: "",
},
},
});
useEffect(() => {
if (data) {
form.reset(data);
}
}, [data, form]);
async function onSubmit(values: GoogleFormData) {
setLoading(true);
try {
await updateAuthMethodConfig(values as API.UpdateAuthMethodConfigRequest);
toast.success(t("common.saveSuccess", "Saved successfully"));
refetch();
setOpen(false);
} catch (_error) {
toast.error(t("common.saveFailed", "Save failed"));
} finally {
setLoading(false);
}
}
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<div className="flex cursor-pointer items-center justify-between transition-colors">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Icon className="h-5 w-5 text-primary" icon="mdi:google" />
</div>
<div className="flex-1">
<p className="font-medium">
{t("google.title", "Google Sign-In")}
</p>
<p className="text-muted-foreground text-sm">
{t(
"google.description",
"Authenticate users with Google accounts"
)}
</p>
</div>
</div>
<Icon className="size-6" icon="mdi:chevron-right" />
</div>
</SheetTrigger>
<SheetContent className="w-[600px] max-w-full md:max-w-screen-md">
<SheetHeader>
<SheetTitle>{t("google.title", "Google Sign-In")}</SheetTitle>
</SheetHeader>
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-24px-env(safe-area-inset-top))] px-6">
<Form {...form}>
<form
className="space-y-2 pt-4"
id="google-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t("google.enable", "Enable")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"google.enableDescription",
"When enabled, users can sign in with their Google account"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.client_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("google.clientId", "Client ID")}</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="123456789-abc123def456.apps.googleusercontent.com"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"google.clientIdDescription",
"Google OAuth Client ID, available from Google Cloud Console"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.client_secret"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("google.clientSecret", "Client Secret")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
type="password"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"google.clientSecretDescription",
"Google OAuth Client Secret, available from Google Cloud Console"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className="flex-row justify-end gap-2 pt-3">
<Button
disabled={loading}
onClick={() => setOpen(false)}
variant="outline"
>
{t("common.cancel", "Cancel")}
</Button>
<Button disabled={loading} form="google-form" type="submit">
{loading && (
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
)}
{t("common.save", "Save")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,622 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Switch } from "@workspace/ui/components/switch";
import { Textarea } from "@workspace/ui/components/textarea";
import { AreaCodeSelect } from "@workspace/ui/composed/area-code-select";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
import TagInput from "@workspace/ui/composed/tag-input";
import {
getAuthMethodConfig,
getSmsPlatform,
testSmsSend,
updateAuthMethodConfig,
} from "@workspace/ui/services/admin/authMethod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { z } from "zod";
const phoneSettingsSchema = z.object({
id: z.number(),
method: z.string(),
enabled: z.boolean(),
config: z
.object({
enable_whitelist: z.boolean().optional(),
whitelist: z.array(z.string()).optional(),
platform: z.string().optional(),
platform_config: z
.object({
access: z.string().optional(),
endpoint: z.string().optional(),
secret: z.string().optional(),
template_code: z.string().optional(),
sign_name: z.string().optional(),
phone_number: z.string().optional(),
template: z.string().optional(),
})
.optional(),
})
.optional(),
});
type PhoneSettingsFormData = z.infer<typeof phoneSettingsSchema>;
export default function PhoneSettingsForm() {
const { t } = useTranslation("auth-control");
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [testParams, setTestParams] = useState<API.TestSmsSendRequest>({
telephone: "",
area_code: "1",
});
const { data, refetch, isFetching } = useQuery({
queryKey: ["getAuthMethodConfig", "mobile"],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: "mobile",
});
return data.data;
},
enabled: open,
});
const { data: platforms } = useQuery({
queryKey: ["getSmsPlatform"],
queryFn: async () => {
const { data } = await getSmsPlatform();
return data.data?.list;
},
enabled: open,
});
const form = useForm<PhoneSettingsFormData>({
resolver: zodResolver(phoneSettingsSchema),
defaultValues: {
id: 0,
method: "mobile",
enabled: false,
config: {
enable_whitelist: false,
whitelist: [],
platform: "",
platform_config: {
access: "",
endpoint: "",
secret: "",
template_code: "code",
sign_name: "",
phone_number: "",
template: "",
},
},
},
});
const selectedPlatform = platforms?.find(
(platform) => platform.platform === form.watch("config.platform")
);
const { platform_url, platform_field_description: platformConfig } =
selectedPlatform ?? {};
useEffect(() => {
if (data) {
form.reset(data);
}
}, [data, form]);
async function onSubmit(values: PhoneSettingsFormData) {
setLoading(true);
try {
await updateAuthMethodConfig(values as API.UpdateAuthMethodConfigRequest);
toast.success(t("common.saveSuccess", "Saved successfully"));
refetch();
setOpen(false);
} catch (_error) {
toast.error(t("common.saveFailed", "Save failed"));
} finally {
setLoading(false);
}
}
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<div className="flex cursor-pointer items-center justify-between transition-colors">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Icon
className="h-5 w-5 text-primary"
icon="mdi:phone-settings"
/>
</div>
<div className="flex-1">
<p className="font-medium">{t("phone.title", "SMS Settings")}</p>
<p className="text-muted-foreground text-sm">
{t("phone.description", "Configure SMS authentication")}
</p>
</div>
</div>
<Icon className="size-6" icon="mdi:chevron-right" />
</div>
</SheetTrigger>
<SheetContent className="w-[600px] max-w-full md:max-w-screen-md">
<SheetHeader>
<SheetTitle>{t("phone.title", "SMS Settings")}</SheetTitle>
</SheetHeader>
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-24px-env(safe-area-inset-top))] px-6">
<Form {...form}>
<form
className="space-y-2 pt-4"
id="phone-settings-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t("phone.enable", "Enable")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
disabled={isFetching}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"phone.enableTip",
"When enabled, users can sign in with their phone number"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.enable_whitelist"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("phone.whitelistValidation", "Whitelist Validation")}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"phone.whitelistValidationTip",
"Only allow phone numbers with whitelisted area codes"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.whitelist"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("phone.whitelistAreaCode", "Whitelist Area Codes")}
</FormLabel>
<FormControl>
<TagInput
onChange={field.onChange}
placeholder="1, 852, 886, 888"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"phone.whitelistAreaCodeTip",
"Enter area codes separated by commas"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.platform"
render={({ field }) => (
<FormItem>
<FormLabel>{t("phone.platform", "SMS Platform")}</FormLabel>
<div className="flex items-center gap-1">
<FormControl>
<Select
disabled={isFetching}
onValueChange={field.onChange}
value={field.value}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{platforms?.map((item) => (
<SelectItem
key={item.platform}
value={item.platform}
>
{item.platform}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
{platform_url && (
<Button asChild size="sm">
<Link target="_blank" to={platform_url}>
{t("phone.applyPlatform", "Apply")}
</Link>
</Button>
)}
</div>
<FormDescription>
{t("phone.platformTip", "Select SMS service provider")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.platform_config.access"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("phone.accessLabel", "Access Key")}
</FormLabel>
<FormControl>
<EnhancedInput
disabled={isFetching}
onValueChange={field.onChange}
placeholder={t(
"phone.platformConfigTip",
"Please enter {{key}}",
{
key: platformConfig?.access,
}
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t("phone.platformConfigTip", "Please enter {{key}}", {
key: platformConfig?.access,
})}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{platformConfig?.endpoint && (
<FormField
control={form.control}
name="config.platform_config.endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("phone.endpointLabel", "Endpoint")}
</FormLabel>
<FormControl>
<EnhancedInput
disabled={isFetching}
onValueChange={field.onChange}
placeholder={t(
"phone.platformConfigTip",
"Please enter {{key}}",
{
key: platformConfig?.endpoint,
}
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t("phone.platformConfigTip", "Please enter {{key}}", {
key: platformConfig?.endpoint,
})}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="config.platform_config.secret"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("phone.secretLabel", "Secret Key")}
</FormLabel>
<FormControl>
<EnhancedInput
disabled={isFetching}
onValueChange={field.onChange}
placeholder={t(
"phone.platformConfigTip",
"Please enter {{key}}",
{
key: platformConfig?.secret,
}
)}
type="password"
value={field.value}
/>
</FormControl>
<FormDescription>
{t("phone.platformConfigTip", "Please enter {{key}}", {
key: platformConfig?.secret,
})}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{platformConfig?.template_code && (
<FormField
control={form.control}
name="config.platform_config.template_code"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("phone.templateCodeLabel", "Template Code")}
</FormLabel>
<FormControl>
<EnhancedInput
disabled={isFetching}
onValueChange={field.onChange}
placeholder={t(
"phone.platformConfigTip",
"Please enter {{key}}",
{
key: platformConfig?.template_code,
}
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t("phone.platformConfigTip", "Please enter {{key}}", {
key: platformConfig?.template_code,
})}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{platformConfig?.sign_name && (
<FormField
control={form.control}
name="config.platform_config.sign_name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("phone.signNameLabel", "Sign Name")}
</FormLabel>
<FormControl>
<EnhancedInput
disabled={isFetching}
onValueChange={field.onChange}
placeholder={t(
"phone.platformConfigTip",
"Please enter {{key}}",
{
key: platformConfig?.sign_name,
}
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t("phone.platformConfigTip", "Please enter {{key}}", {
key: platformConfig?.sign_name,
})}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{platformConfig?.phone_number && (
<FormField
control={form.control}
name="config.platform_config.phone_number"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("phone.phoneNumberLabel", "Phone Number")}
</FormLabel>
<FormControl>
<EnhancedInput
disabled={isFetching}
onValueChange={field.onChange}
placeholder={t(
"phone.platformConfigTip",
"Please enter {{key}}",
{
key: platformConfig?.phone_number,
}
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t("phone.platformConfigTip", "Please enter {{key}}", {
key: platformConfig?.phone_number,
})}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{platformConfig?.code_variable && (
<FormField
control={form.control}
name="config.platform_config.template"
render={({ field }) => (
<FormItem>
<FormLabel>{t("phone.template", "Template")}</FormLabel>
<FormControl>
<Textarea
disabled={isFetching}
onChange={field.onChange}
placeholder={t(
"phone.placeholders.template",
"Use {{code}} for verification code",
{
code: platformConfig?.code_variable,
}
)}
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"phone.templateTip",
"Use {{code}} variable for the verification code",
{
code: platformConfig?.code_variable,
}
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="space-y-4 border-t pt-4">
<div>
<FormLabel>{t("phone.testSms", "Test SMS")}</FormLabel>
<p className="mb-3 text-muted-foreground text-sm">
{t(
"phone.testSmsTip",
"Send a test SMS to verify configuration"
)}
</p>
<div className="flex items-center gap-2">
<AreaCodeSelect
onChange={(value) => {
if (value.phone) {
setTestParams((prev) => ({
...prev,
area_code: value.phone!,
}));
}
}}
value={testParams.area_code}
/>
<EnhancedInput
onValueChange={(value) => {
setTestParams((prev) => ({
...prev,
telephone: value as string,
}));
}}
placeholder={t("phone.testSmsPhone", "Phone number")}
value={testParams.telephone}
/>
<Button
disabled={
!(testParams.telephone && testParams.area_code) ||
isFetching
}
onClick={async () => {
if (
isFetching ||
!testParams.telephone ||
!testParams.area_code
)
return;
try {
await testSmsSend(testParams);
toast.success(
t("phone.sendSuccess", "SMS sent successfully")
);
} catch {
toast.error(t("phone.sendFailed", "SMS send failed"));
}
}}
type="button"
>
{t("phone.testSms", "Test SMS")}
</Button>
</div>
</div>
</div>
</form>
</Form>
</ScrollArea>
<SheetFooter className="flex-row justify-end gap-2 pt-3">
<Button
disabled={loading}
onClick={() => setOpen(false)}
variant="outline"
>
{t("common.cancel", "Cancel")}
</Button>
<Button disabled={loading} form="phone-settings-form" type="submit">
{loading && (
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
)}
{t("common.save", "Save")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,230 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Switch } from "@workspace/ui/components/switch";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
import {
getAuthMethodConfig,
updateAuthMethodConfig,
} from "@workspace/ui/services/admin/authMethod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { z } from "zod";
const telegramSchema = z.object({
enabled: z.boolean(),
bot: z.string().optional(),
bot_token: z.string().optional(),
});
type TelegramFormData = z.infer<typeof telegramSchema>;
export default function TelegramForm() {
const { t } = useTranslation("auth-control");
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ["getAuthMethodConfig", "telegram"],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: "telegram",
});
return data.data;
},
enabled: open,
});
const form = useForm<TelegramFormData>({
resolver: zodResolver(telegramSchema),
defaultValues: {
enabled: false,
bot: "",
bot_token: "",
},
});
useEffect(() => {
if (data) {
form.reset({
enabled: data.enabled,
bot: data.config?.bot || "",
bot_token: data.config?.bot_token || "",
});
}
}, [data, form]);
async function onSubmit(values: TelegramFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...data,
enabled: values.enabled,
config: {
...data?.config,
bot: values.bot,
bot_token: values.bot_token,
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t("common.saveSuccess", "Saved successfully"));
refetch();
setOpen(false);
} catch (_error) {
toast.error(t("common.saveFailed", "Save failed"));
} finally {
setLoading(false);
}
}
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<div className="flex cursor-pointer items-center justify-between transition-colors">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Icon className="h-5 w-5 text-primary" icon="mdi:telegram" />
</div>
<div className="flex-1">
<p className="font-medium">
{t("telegram.title", "Telegram Sign-In")}
</p>
<p className="text-muted-foreground text-sm">
{t(
"telegram.description",
"Authenticate users with Telegram accounts"
)}
</p>
</div>
</div>
<Icon className="size-6" icon="mdi:chevron-right" />
</div>
</SheetTrigger>
<SheetContent className="w-[500px] max-w-full md:max-w-screen-md">
<SheetHeader>
<SheetTitle>{t("telegram.title", "Telegram Sign-In")}</SheetTitle>
</SheetHeader>
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-24px-env(safe-area-inset-top))] px-6">
<Form {...form}>
<form
className="space-y-2 pt-4"
id="telegram-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t("telegram.enable", "Enable")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t(
"telegram.enableDescription",
"When enabled, users can sign in with their Telegram account"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bot"
render={({ field }) => (
<FormItem>
<FormLabel>{t("telegram.clientId", "Bot ID")}</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="6123456789"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"telegram.clientIdDescription",
"Telegram Bot ID, available from @BotFather"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bot_token"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("telegram.clientSecret", "Bot Token")}
</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={field.onChange}
placeholder="6123456789:AAHn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
type="password"
value={field.value}
/>
</FormControl>
<FormDescription>
{t(
"telegram.clientSecretDescription",
"Telegram Bot Token, available from @BotFather"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className="flex-row justify-end gap-2 pt-3">
<Button
disabled={loading}
onClick={() => setOpen(false)}
variant="outline"
>
{t("common.cancel", "Cancel")}
</Button>
<Button disabled={loading} form="telegram-form" type="submit">
{loading && (
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
)}
{t("common.save", "Save")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,67 @@
import {
Table,
TableBody,
TableCell,
TableRow,
} from "@workspace/ui/components/table";
import { useTranslation } from "react-i18next";
import AppleForm from "./forms/apple-form";
import DeviceForm from "./forms/device-form";
import EmailSettingsForm from "./forms/email-settings-form";
import FacebookForm from "./forms/facebook-form";
import GithubForm from "./forms/github-form";
import GoogleForm from "./forms/google-form";
import PhoneSettingsForm from "./forms/phone-settings-form";
import TelegramForm from "./forms/telegram-form";
export default function AuthControl() {
const { t } = useTranslation("auth-control");
const formSections = [
{
title: t("communicationMethods", "Communication Methods"),
forms: [
{ component: EmailSettingsForm },
{ component: PhoneSettingsForm },
],
},
{
title: t("socialAuthMethods", "Social Authentication Methods"),
forms: [
{ component: AppleForm },
{ component: GoogleForm },
{ component: FacebookForm },
{ component: GithubForm },
{ component: TelegramForm },
],
},
{
title: t("deviceAuthMethods", "Device Authentication Methods"),
forms: [{ component: DeviceForm }],
},
];
return (
<div className="space-y-8">
{formSections.map((section, sectionIndex) => (
<div key={sectionIndex}>
<h2 className="mb-4 font-semibold text-lg">{section.title}</h2>
<Table>
<TableBody>
{section.forms.map((form, formIndex) => {
const FormComponent = form.component;
return (
<TableRow key={formIndex}>
<TableCell>
<FormComponent />
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
))}
</div>
);
}