diff --git a/public/locales/de/settings.json b/public/locales/de/settings.json index 2ad4e48..2b5dddc 100644 --- a/public/locales/de/settings.json +++ b/public/locales/de/settings.json @@ -216,6 +216,9 @@ "auth_type": "Login-Typ", "admin": "Administrator", "user": "Benutzer", + "otp": "Zwei-Faktor-Authentifizierung", + "otp_enabled": "Aktiviert", + "otp_disabled": "Deaktiviert", "toasts": { "success_msg": "Der Benutzer wurde erfolgreich erstellt.", "error_msg": "Beim Anlegen des Benutzers ist ein Fehler aufgetreten." diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index ae26ecb..147d2bc 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -216,6 +216,9 @@ "auth_type": "Login Type", "admin": "Administrator", "user": "User", + "otp": "Two-Factor Authentication", + "otp_enabled": "Enabled", + "otp_disabled": "Disabled", "toasts": { "success_msg": "The user has been created successfully.", "error_msg": "An error occurred while creating the user." diff --git a/public/locales/tr/settings.json b/public/locales/tr/settings.json index d0f0fdc..ac987ec 100644 --- a/public/locales/tr/settings.json +++ b/public/locales/tr/settings.json @@ -216,6 +216,9 @@ "auth_type": "Giriş Türü", "admin": "Yönetici", "user": "Kullanıcı", + "otp": "İki Aşamalı Giriş", + "otp_enabled": "Aktif", + "otp_disabled": "Pasif", "toasts": { "success_msg": "Kullanıcı başarıyla oluşturuldu.", "error_msg": "Kullanıcı oluşturulurken bir hata oluştu." @@ -761,7 +764,7 @@ }, "OTP_ENABLED": { "label": "İki Faktörlü Doğrulama", - "subtext": "Aktif ettiğinizde tüm kullanıcılar zorunlu olarak tercih ettiğiniz iki faktörlü doğrulama uygulaması aracılığıyla kodlar alarak giriş yapacaktır." + "subtext": "Aktif ettiğinizde bir sonraki girişinizde telefonunuza gelen kodu girmeniz gerekecektir." }, "APP_NOTIFICATION_EMAIL": { "label": "Sistem E-Postası", diff --git a/src/components/ui/user-auth-form.tsx b/src/components/ui/user-auth-form.tsx index 50785a3..1ca3af6 100644 --- a/src/components/ui/user-auth-form.tsx +++ b/src/components/ui/user-auth-form.tsx @@ -1,18 +1,18 @@ -import * as React from "react" -import { useRouter } from "next/router" import { authService } from "@/services" import { AlertCircle } from "lucide-react" +import { useRouter } from "next/router" +import * as React from "react" -import { cn } from "@/lib/utils" -import { useLogin } from "@/hooks/auth/useLogin" import { Button } from "@/components/ui/button" import { Icons } from "@/components/ui/icons" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" +import { useLogin } from "@/hooks/auth/useLogin" +import { cn } from "@/lib/utils" import { Alert, AlertDescription, AlertTitle } from "./alert" -interface UserAuthFormProps extends React.HTMLAttributes {} +interface UserAuthFormProps extends React.HTMLAttributes { } export function UserAuthForm({ className, ...props }: UserAuthFormProps) { const router = useRouter() @@ -25,75 +25,94 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) { const [newPassword, setNewPassword] = React.useState("") const [newPasswordConfirm, setNewPasswordConfirm] = React.useState("") + const [otpSetup, setOtpSetup] = React.useState(false) + const [otpData, setOtpData] = React.useState<{ + secret: string + image: string + message: string + }>() + + const [enableOtp, setEnableOtp] = React.useState(false) + const [otp, setOtp] = React.useState("") + const [error, setError] = React.useState("") const { login } = useLogin() - const onSubmit = (e: React.SyntheticEvent) => { + const onSubmit = async (e: React.SyntheticEvent) => { e.preventDefault() setIsLoading(true) let redirectUri = (router.query.redirect || "/") as string redirectUri = redirectUri.replace("http", "") - if (forceChange) { - if (newPassword !== newPasswordConfirm) { - setError("Girdiğiniz şifreler uyuşmuyor.") - setIsLoading(false) - return + try { + if (forceChange) { + if (newPassword !== newPasswordConfirm) { + throw new Error("Girdiğiniz şifreler uyuşmuyor.") + } + + await authService.login(name, password, newPassword) + await login(name, newPassword, otp) + } else { + if (!name || !password) { + throw new Error("Kullanıcı adı veya şifre boş olamaz.") + } + + await login(name, password, otp) + + if (forceChange) { + setForceChange(false) + } } - authService - .login(name, password, newPassword) - .then(() => { - login(name, newPassword) - .then(() => { - setError("") - setTimeout(() => { - router.push(redirectUri) - }, 1000) - }) - .catch((e) => { - setError(e.response.data.message) - }) - .finally(() => { - setIsLoading(false) - }) - }) - .catch((e) => { - if (e.response && e.response.status === 422) { - setError(e.response.data[Object.keys(e.response.data)[0]][0]) - setIsLoading(false) - return - } - - setError(e.response.data.message) - setIsLoading(false) - }) + setError("") + setTimeout(() => { + router.push(redirectUri) + }, 1000) + } catch (e: any) { + if (e.response.data.message) { + setError(e.response.data.message) + } else { + setError(e.message) + } - return - } + if (e.response.status === 405) { + setForceChange(true) + } - if (!name || !password) { - setError("Kullanıcı adı veya şifre boş olamaz.") + if (e.response.status === 402) { + setOtpData(e.response.data) + setOtpSetup(true) + } + + if (e.response.status === 406) { + setEnableOtp(true) + } + } finally { setIsLoading(false) - } else { - login(name, password) - .then(() => { - setError("") - setTimeout(() => { - router.push(redirectUri) - }, 1000) + } + } + + const saveTwoFactorToken = async ( + secret: string, + username: string, + password: string + ) => { + try { + setIsLoading(true) + authService + .saveTwoFactorToken(secret, username, password) + .then((res) => { + setError("Kurulum başarılı. Tekrar giriş yapınız.") }) - .catch((e) => { - setError(e.response.data.message) - if (e.response.status === 405) { - setForceChange(true) - } + .catch((err) => { + setError(err.response.data.message) }) .finally(() => { - setTimeout(() => { - setIsLoading(false) - }, 1000) + setIsLoading(false) }) + setOtpSetup(false) + } catch (e: any) { + setError(e.message) } } @@ -109,37 +128,42 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) { )} -
- - setName(e.target.value)} - /> -
-
- - setPassword(e.target.value)} - /> -
+ {!otpSetup && !enableOtp && ( + <> +
+ + setName(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+ + )} + {forceChange && ( <>
@@ -178,12 +202,66 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) { )} - + {otpSetup && ( +
+
+ +

+ Google Authenticator uygulamasına QR kodu tarattıktan sonra + kaydet düğmesine basınız. +

+ + +
+ )} + + {enableOtp && ( + <> +
+ + setOtp(e.target.value)} + /> +
+ + )} + + {!otpSetup && ( + + )}
diff --git a/src/hooks/auth/useLogin.ts b/src/hooks/auth/useLogin.ts index 4d3844b..d8e5d69 100644 --- a/src/hooks/auth/useLogin.ts +++ b/src/hooks/auth/useLogin.ts @@ -3,8 +3,8 @@ import Cookies from "js-cookie" import { authService } from "../../services" export const useLogin = () => { - const login = async (username: string, password: string) => { - const user = await authService.login(username, password) + const login = async (username: string, password: string, token?: string) => { + const user = await authService.login(username, password, undefined, token) if (user) { Cookies.set("currentUser", JSON.stringify(user.data)) } diff --git a/src/pages/settings/advanced/tweaks.tsx b/src/pages/settings/advanced/tweaks.tsx index f175e88..af1eed2 100644 --- a/src/pages/settings/advanced/tweaks.tsx +++ b/src/pages/settings/advanced/tweaks.tsx @@ -2,7 +2,7 @@ import { ReactElement, useEffect } from "react" import { NextPageWithLayout } from "@/pages/_app" import { apiService } from "@/services" import { zodResolver } from "@hookform/resolvers/zod" -import { Bug, FolderArchive, Puzzle, Save, ShieldCheck } from "lucide-react" +import { Bug, FolderArchive, Puzzle, Save } from "lucide-react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import * as z from "zod" @@ -37,7 +37,6 @@ const AdvancedTweaksPage: NextPageWithLayout = () => { const formSchema = z.object({ APP_LANG: z.string(), - OTP_ENABLED: z.boolean(), APP_NOTIFICATION_EMAIL: z.string().email(), APP_URL: z.string().url(), EXTENSION_TIMEOUT: z @@ -121,32 +120,6 @@ const AdvancedTweaksPage: NextPageWithLayout = () => { )} /> - ( - -
- -
- - {t("advanced.tweaks.OTP_ENABLED.label")} - - - {t("advanced.tweaks.OTP_ENABLED.subtext")} - -
-
- - - -
- )} - /> - + ( + +
+ +
+ + {t("advanced.tweaks.OTP_ENABLED.label")} + + + {t("advanced.tweaks.OTP_ENABLED.subtext")} + +
+
+ + + +
+ )} + /> + { switch (type) { @@ -131,6 +131,50 @@ export default function UserSettingsPage() { title: t("users.auth_type"), cell: ({ row }) => <>{getType(row.original.auth_type)}, }, + { + accessorKey: "otp_enabled", + accessorFn: (row) => { + return row.otp_enabled + ? 0 + t("users.otp_enabled") + : t("users.otp_disabled") + }, + header: ({ column }) => ( + + ), + title: t("users.otp"), + cell: ({ row }) => ( + <> + {row.original.otp_enabled ? ( +
+ + + {t("users.otp_enabled")} + +
+ ) : ( +
+ + + {t("users.otp_disabled")} + +
+ )} + + ), + }, { id: "actions", cell: ({ row }) => ( @@ -248,15 +292,15 @@ function AuthLogDialog() { <> {row.original.created_at ? new Date(row.original.created_at).toLocaleDateString( - i18n.language, - { - day: "2-digit", - month: "long", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - } - ) + i18n.language, + { + day: "2-digit", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + } + ) : t("users.auth_log.unknown")} ), diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 01b327f..88f1427 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -16,7 +16,12 @@ export class AuthService { }) } - login = (email: string, password: string, newPassword?: string) => { + login = ( + email: string, + password: string, + newPassword?: string, + token?: string + ) => { if (newPassword) { return this.instance.post("/change_password", { email, @@ -28,6 +33,15 @@ export class AuthService { return this.instance.post("/login", { email, password, + token, + }) + } + + saveTwoFactorToken = (secret: string, username: string, password: string) => { + return this.instance.post("/setup_mfa", { + secret, + email: username, + password, }) } diff --git a/src/types/user.ts b/src/types/user.ts index f59b492..155d9ed 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -12,6 +12,7 @@ export interface IUser { auth_type: string username: string locale: string + otp_enabled: boolean permissions: ILimanPermissions }