chore(auth): update OAuth login descriptions in multiple languages

This commit is contained in:
Fatih Kadir Akın
2025-12-11 01:09:51 +03:00
parent 35e3774e2d
commit dd95a41a64
19 changed files with 172 additions and 20 deletions
+3
View File
@@ -18,3 +18,6 @@ NEXTAUTH_SECRET="your-super-secret-key-change-in-production"
# S3_ACCESS_KEY_ID=""
# S3_SECRET_ACCESS_KEY=""
# S3_ENDPOINT="" # For S3-compatible services like MinIO
# GITHUB_CLIENT_ID=your_client_id
# GITHUB_CLIENT_SECRET=your_client_secret
+1
View File
@@ -45,6 +45,7 @@
"auth": {
"login": "تسجيل الدخول",
"loginDescription": "أدخل بياناتك للمتابعة",
"loginDescriptionOAuth": "سجل الدخول بحسابك للمتابعة",
"register": "إنشاء حساب",
"registerDescription": "أنشئ حساباً للبدء",
"logout": "تسجيل الخروج",
+1
View File
@@ -45,6 +45,7 @@
"auth": {
"login": "Login",
"loginDescription": "Enter your credentials to continue",
"loginDescriptionOAuth": "Sign in with your account to continue",
"register": "Register",
"registerDescription": "Create an account to get started",
"logout": "Logout",
+1
View File
@@ -45,6 +45,7 @@
"auth": {
"login": "Iniciar sesión",
"loginDescription": "Ingresa tus credenciales para continuar",
"loginDescriptionOAuth": "Inicia sesión con tu cuenta para continuar",
"register": "Registrarse",
"registerDescription": "Crea una cuenta para comenzar",
"logout": "Cerrar sesión",
+1
View File
@@ -45,6 +45,7 @@
"auth": {
"login": "ログイン",
"loginDescription": "続行するには認証情報を入力してください",
"loginDescriptionOAuth": "アカウントでサインインして続行",
"register": "登録",
"registerDescription": "アカウントを作成して始めましょう",
"logout": "ログアウト",
+1
View File
@@ -45,6 +45,7 @@
"auth": {
"login": "Giriş Yap",
"loginDescription": "Devam etmek için bilgilerinizi girin",
"loginDescriptionOAuth": "Devam etmek için hesabınızla giriş yapın",
"register": "Kayıt Ol",
"registerDescription": "Başlamak için bir hesap oluşturun",
"logout": "Çıkış Yap",
+1
View File
@@ -45,6 +45,7 @@
"auth": {
"login": "登录",
"loginDescription": "输入您的凭据以继续",
"loginDescriptionOAuth": "使用您的账户登录以继续",
"register": "注册",
"registerDescription": "创建账户以开始使用",
"logout": "退出登录",
+3 -3
View File
@@ -25,10 +25,10 @@ export default defineConfig({
// Authentication plugin
auth: {
// Available: "credentials" | "google" | "azure" | custom
provider: "credentials",
// Available: "credentials" | "google" | "azure" | "github" | custom
provider: "github",
// Allow public registration
allowRegistration: true,
allowRegistration: false,
},
// Storage plugin for media uploads
+14 -7
View File
@@ -1,7 +1,8 @@
import { Metadata } from "next";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { LoginForm } from "@/components/auth/login-form";
import { getConfig } from "@/lib/config";
import { AuthContent } from "@/components/auth/auth-content";
export const metadata: Metadata = {
title: "Login",
@@ -10,21 +11,27 @@ export const metadata: Metadata = {
export default async function LoginPage() {
const t = await getTranslations("auth");
const config = await getConfig();
const isCredentials = config.auth.provider === "credentials";
return (
<div className="container flex min-h-[calc(100vh-6rem)] flex-col items-center justify-center py-8">
<div className="w-full max-w-sm space-y-4">
<div className="text-center space-y-1">
<h1 className="text-xl font-semibold">{t("login")}</h1>
<p className="text-xs text-muted-foreground">{t("loginDescription")}</p>
<p className="text-xs text-muted-foreground">
{isCredentials ? t("loginDescription") : t("loginDescriptionOAuth")}
</p>
</div>
<div className="border rounded-lg p-4">
<LoginForm />
<AuthContent provider={config.auth.provider} mode="login" />
</div>
<p className="text-center text-xs text-muted-foreground">
{t("noAccount")}{" "}
<Link href="/register" className="text-foreground hover:underline">{t("register")}</Link>
</p>
{isCredentials && (
<p className="text-center text-xs text-muted-foreground">
{t("noAccount")}{" "}
<Link href="/register" className="text-foreground hover:underline">{t("register")}</Link>
</p>
)}
</div>
</div>
);
+11 -2
View File
@@ -1,7 +1,9 @@
import { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { RegisterForm } from "@/components/auth/register-form";
import { getConfig } from "@/lib/config";
import { AuthContent } from "@/components/auth/auth-content";
export const metadata: Metadata = {
title: "Register",
@@ -10,6 +12,13 @@ export const metadata: Metadata = {
export default async function RegisterPage() {
const t = await getTranslations("auth");
const config = await getConfig();
const isCredentials = config.auth.provider === "credentials";
// Block registration if disabled or using OAuth providers
if (!isCredentials || !config.auth.allowRegistration) {
redirect("/login");
}
return (
<div className="container flex min-h-[calc(100vh-6rem)] flex-col items-center justify-center py-8">
@@ -19,7 +28,7 @@ export default async function RegisterPage() {
<p className="text-xs text-muted-foreground">{t("registerDescription")}</p>
</div>
<div className="border rounded-lg p-4">
<RegisterForm />
<AuthContent provider={config.auth.provider} mode="register" />
</div>
<p className="text-center text-xs text-muted-foreground">
{t("hasAccount")}{" "}
+10
View File
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import { z } from "zod";
import { db } from "@/lib/db";
import { getConfig } from "@/lib/config";
const registerSchema = z.object({
name: z.string().min(2),
@@ -12,6 +13,15 @@ const registerSchema = z.object({
export async function POST(request: Request) {
try {
// Check if registration is allowed
const config = await getConfig();
if (!config.auth.allowRegistration) {
return NextResponse.json(
{ error: "registration_disabled", message: "Registration is disabled" },
{ status: 403 }
);
}
const body = await request.json();
const parsed = registerSchema.safeParse(body);
+1 -1
View File
@@ -89,7 +89,7 @@ export default async function RootLayout({
<body className={`${fontClasses} antialiased`}>
<Providers locale={locale} messages={messages} theme={config.theme} branding={config.branding}>
<div className="relative min-h-screen flex flex-col">
<Header />
<Header showRegister={config.auth.provider === "credentials" && config.auth.allowRegistration} />
<main className="flex-1">{children}</main>
<Footer />
</div>
+32
View File
@@ -0,0 +1,32 @@
"use client";
import { LoginForm } from "./login-form";
import { RegisterForm } from "./register-form";
import { OAuthButton } from "./oauth-button";
interface AuthContentProps {
provider: string;
mode: "login" | "register";
}
const providerNames: Record<string, string> = {
github: "GitHub",
google: "Google",
azure: "Microsoft",
credentials: "Email",
};
export function AuthContent({ provider, mode }: AuthContentProps) {
// For OAuth providers, show the OAuth button
if (provider !== "credentials") {
return (
<OAuthButton
provider={provider}
providerName={providerNames[provider] || provider}
/>
);
}
// For credentials, show the appropriate form
return mode === "login" ? <LoginForm /> : <RegisterForm />;
}
+65
View File
@@ -0,0 +1,65 @@
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
interface OAuthButtonProps {
provider: string;
providerName: string;
}
const providerIcons: Record<string, React.ReactNode> = {
github: (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
),
google: (
<svg className="h-4 w-4" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
),
azure: (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M5.483 21.3H24L14.025 4.013l-3.038 8.347 5.836 6.938L5.483 21.3zM13.23 2.7L6.105 8.677 0 19.253h5.505l7.725-16.553z"/>
</svg>
),
};
export function OAuthButton({ provider, providerName }: OAuthButtonProps) {
const t = useTranslations("auth");
const [isLoading, setIsLoading] = useState(false);
const handleSignIn = async () => {
setIsLoading(true);
try {
await signIn(provider, { callbackUrl: "/" });
} catch (error) {
console.error("Sign in error:", error);
setIsLoading(false);
}
};
return (
<Button
type="button"
variant="outline"
className="w-full h-10"
onClick={handleSignIn}
disabled={isLoading}
>
{isLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<span className="mr-2">{providerIcons[provider]}</span>
)}
{t("signInWith", { provider: providerName })}
</Button>
);
}
+10 -4
View File
@@ -42,7 +42,11 @@ const languages = [
{ code: "ar", name: "العربية" },
];
export function Header() {
interface HeaderProps {
showRegister?: boolean;
}
export function Header({ showRegister = true }: HeaderProps) {
const { data: session } = useSession();
const t = useTranslations();
const { theme, setTheme } = useTheme();
@@ -230,9 +234,11 @@ export function Header() {
<Button variant="ghost" size="sm" className="h-8 text-xs" asChild>
<Link href="/login">{t("nav.login")}</Link>
</Button>
<Button size="sm" className="h-8 text-xs" asChild>
<Link href="/register">{t("nav.register")}</Link>
</Button>
{showRegister && (
<Button size="sm" className="h-8 text-xs" asChild>
<Link href="/register">{t("nav.register")}</Link>
</Button>
)}
</div>
)}
</div>
+1 -1
View File
@@ -19,7 +19,7 @@ export interface ThemeConfig {
}
export interface AuthConfig {
provider: "credentials" | "google" | "azure" | string;
provider: "credentials" | "google" | "azure" | "github" | string;
allowRegistration: boolean;
}
+12
View File
@@ -0,0 +1,12 @@
import GitHub from "next-auth/providers/github";
import type { AuthPlugin } from "../types";
export const githubPlugin: AuthPlugin = {
id: "github",
name: "GitHub",
getProvider: () =>
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
};
+3 -1
View File
@@ -2,12 +2,14 @@ import { registerAuthPlugin } from "../registry";
import { credentialsPlugin } from "./credentials";
import { googlePlugin } from "./google";
import { azurePlugin } from "./azure";
import { githubPlugin } from "./github";
// Register all built-in auth plugins
export function registerBuiltInAuthPlugins(): void {
registerAuthPlugin(credentialsPlugin);
registerAuthPlugin(googlePlugin);
registerAuthPlugin(azurePlugin);
registerAuthPlugin(githubPlugin);
}
export { credentialsPlugin, googlePlugin, azurePlugin };
export { credentialsPlugin, googlePlugin, azurePlugin, githubPlugin };
+1 -1
View File
@@ -30,7 +30,7 @@ export async function getConfiguredAuthPlugin() {
if (!plugin) {
throw new Error(
`Auth plugin "${config.auth.provider}" not found. ` +
`Available plugins: credentials, google, azure`
`Available plugins: credentials, google, azure, github`
);
}