Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Two factor authentication support #3

Merged
merged 2 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/locales/de/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
3 changes: 3 additions & 0 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
5 changes: 4 additions & 1 deletion public/locales/tr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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ı",
Expand Down
266 changes: 172 additions & 94 deletions src/components/ui/user-auth-form.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {}
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> { }

export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const router = useRouter()
Expand All @@ -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)
}
}

Expand All @@ -109,37 +128,42 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
</Alert>
)}

<div className="grid gap-1">
<Label className="sr-only" htmlFor="email">
Kullanıcı Adı
</Label>
<Input
id="email"
placeholder="[email protected]"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid gap-1">
<Label className="sr-only" htmlFor="password">
Şifre
</Label>
<Input
id="password"
placeholder="***********"
type="password"
autoCapitalize="none"
autoComplete="password"
autoCorrect="off"
disabled={isLoading}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{!otpSetup && !enableOtp && (
<>
<div className="grid gap-1">
<Label className="sr-only" htmlFor="email">
Kullanıcı Adı
</Label>
<Input
id="email"
placeholder="[email protected]"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid gap-1">
<Label className="sr-only" htmlFor="password">
Şifre
</Label>
<Input
id="password"
placeholder="***********"
type="password"
autoCapitalize="none"
autoComplete="password"
autoCorrect="off"
disabled={isLoading}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</>
)}

{forceChange && (
<>
<div className="mt-5 grid gap-1">
Expand Down Expand Up @@ -178,12 +202,66 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
</>
)}

<Button disabled={isLoading} className="mt-4">
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Giriş Yap
</Button>
{otpSetup && (
<div className="mt-5 flex grow flex-col items-center justify-center">
<div
dangerouslySetInnerHTML={{
__html: otpData?.image || "",
}}
style={{
borderRadius: "24px",
overflow: "hidden"
}}
></div>

<p className="mt-5 text-center">
Google Authenticator uygulamasına QR kodu tarattıktan sonra
kaydet düğmesine basınız.
</p>

<Button
disabled={isLoading}
className="mt-8 w-full"
onClick={() =>
saveTwoFactorToken(otpData?.secret || "", name, password)
}
>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Kurulumu Tamamla
</Button>
</div>
)}

{enableOtp && (
<>
<div className="grid gap-1">
<Label className="sr-only" htmlFor="otp">
İki Aşamalı Doğrulama Kodu
</Label>
<Input
id="otp"
placeholder="******"
autoCapitalize="none"
autoComplete="otp"
autoCorrect="off"
disabled={isLoading}
value={otp}
onChange={(e) => setOtp(e.target.value)}
/>
</div>
</>
)}

{!otpSetup && (
<Button disabled={isLoading} className="mt-4">
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Giriş Yap
</Button>
)}
</div>
</form>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/auth/useLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Loading
Loading