Custom sign in/out page

This commit is contained in:
aronmal 2023-07-07 22:57:30 +02:00
parent 0a6fd88733
commit dbb9bd8476
Signed by: aronmal
GPG key ID: 816B7707426FC612
6 changed files with 171 additions and 147 deletions

View file

@ -48,6 +48,13 @@ const options: NextAuthOptions = {
return session return session
}, },
}, },
pages: {
signIn: "/login",
signOut: "/logout",
// error: '/auth/error', // Error code passed in query string as ?error=
// verifyRequest: '/auth/verify-request', // (used for check email message)
// newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
},
} }
export { options as authOptions } export { options as authOptions }

View file

@ -1,144 +1,142 @@
import { faWifiExclamation } from "@fortawesome/pro-duotone-svg-icons" import { faLeftLong } from "@fortawesome/pro-solid-svg-icons"
import {
faArrowLeft,
faCheck,
faSpinnerThird,
faXmark,
} from "@fortawesome/pro-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import classNames from "classnames" import { signIn, useSession } from "next-auth/react"
import { FormEvent, useState } from "react" import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import { toast } from "react-toastify"
enum ProcessStates { type SignInErrorTypes =
"waiting", | "Signin"
"loading", | "OAuthSignin"
"success", | "OAuthCallback"
"wrong", | "OAuthCreateAccount"
"error", | "EmailCreateAccount"
} | "Callback"
const messages: { [key in ProcessStates]: string } = { | "OAuthAccountNotLinked"
[ProcessStates.waiting]: "Enter Login Details", | "EmailSignin"
[ProcessStates.loading]: "Logging in...", | "CredentialsSignin"
[ProcessStates.success]: "Done!", | "SessionRequired"
[ProcessStates.wrong]: "Wrong username or password", | "default"
[ProcessStates.error]: "An error occurred!",
} const errors: Record<SignInErrorTypes, string> = {
const icons = { Signin: "Try signing in with a different account.",
[ProcessStates.loading]: faSpinnerThird, OAuthSignin: "Try signing in with a different account.",
[ProcessStates.success]: faCheck, OAuthCallback: "Try signing in with a different account.",
[ProcessStates.wrong]: faXmark, OAuthCreateAccount: "Try signing in with a different account.",
[ProcessStates.error]: faWifiExclamation, EmailCreateAccount: "Try signing in with a different account.",
} Callback: "Try signing in with a different account.",
const iconClasses = { OAuthAccountNotLinked:
[ProcessStates.loading]: "animate-spin", "To confirm your identity, sign in with the same account you used originally.",
[ProcessStates.success]: "text-green-500", EmailSignin: "The e-mail could not be sent.",
[ProcessStates.wrong]: "text-red-500", CredentialsSignin:
[ProcessStates.error]: "animate-pulse text-amber-500 !text-8xl", "Sign in failed. Check the details you provided are correct.",
SessionRequired: "Please sign in to access this page.",
default: "Unable to sign in.",
} }
function Login() { function Login() {
const [username, setUsername] = useState("") const [email, setEmail] = useState("")
const [password, setPassword] = useState("") const { status } = useSession()
const router = useRouter()
const [state, setState] = useState<ProcessStates>(ProcessStates.waiting) const errorType = router.query.error as SignInErrorTypes
const [error, setError] = useState("")
const elem = () => { useEffect(() => {
if (state === ProcessStates.waiting) if (!errorType) return
return ( toast.error(errors[errorType] ?? errors.default, { theme: "colored" })
<form onSubmit={login}> }, [errorType])
<div className="mb-4 text-lg">
<input
className="rounded-3xl border-none bg-blue-400 bg-opacity-50 px-6 py-2 text-center text-inherit placeholder-slate-200 shadow-lg outline-none backdrop-blur-md"
type="text"
name="name"
placeholder="Username or email"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="mb-4 text-lg"> useEffect(() => {
<input if (status === "authenticated") router.push("/")
className="rounded-3xl border-none bg-blue-400 bg-opacity-50 px-6 py-2 text-center text-inherit placeholder-slate-200 shadow-lg outline-none backdrop-blur-md" }, [router, status])
type="Password"
name="name"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="mt-8 flex justify-center text-lg text-black">
<button
type="submit"
className="rounded-3xl bg-blue-400 bg-opacity-50 px-10 py-2 text-white shadow-xl backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
>
Login
</button>
</div>
</form>
)
return ( function login(provider: "email" | "azure-ad") {
<> return () => {
<div className="flex flex-col items-center rounded-3xl bg-slate-800/50 px-16 py-8 shadow-lg drop-shadow-md"> signIn(provider, { email, callbackUrl: "/" })
<FontAwesomeIcon }
className={classNames("text-6xl", iconClasses[state])}
icon={icons[state]}
/>
</div>
<div className="mt-8 flex justify-center text-lg text-black">
<button
type="button"
className="rounded-3xl bg-blue-400 bg-opacity-50 px-10 py-2 text-white shadow-xl backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
onClick={() => {
setState(ProcessStates.waiting)
setError("")
}}
>
<FontAwesomeIcon className="-ml-4 mr-4" icon={faArrowLeft} />
Return
</button>
</div>
</>
)
}
async function login(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
setState(ProcessStates.loading)
await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ username, password }),
})
.then((res) => {
if (res.status === 200) setState(ProcessStates.success)
if (res.status === 401) setState(ProcessStates.wrong)
})
.catch((err: Error) => {
setState(ProcessStates.error)
setError(err.message)
})
} }
return ( return (
<div className="flex h-screen w-full items-center justify-center bg-gray-900 bg-[url('/images/wallpaper.jpg')] bg-cover bg-center bg-no-repeat"> <div className="flex h-screen w-full items-center justify-center bg-gray-900 bg-[url('/images/wallpaper.jpg')] bg-cover bg-center bg-no-repeat">
<div className="rounded-xl bg-gray-800 bg-opacity-50 px-16 py-10 shadow-lg backdrop-blur-md max-sm:px-8"> <div className="rounded-xl bg-gray-800 bg-opacity-60 px-16 py-10 text-white shadow-lg backdrop-blur-md max-sm:px-8">
<div className="text-white"> <div className="mb-8 flex flex-col items-center">
<div className="mb-8 flex flex-col items-center"> <img
<img className="rounded-full shadow-lg"
className="rounded-full shadow-lg" src="/logo512.png"
src="/logo512.png" width="150"
width="150" alt="Avatar"
alt="Avatar" />
/> <h1 className="mb-2 text-2xl">Leaky Ships</h1>
<h1 className="mb-2 text-2xl">Leaky Ships</h1> <span className="text-gray-300">Choose Login Method</span>
<span className="text-gray-300">
{error ? error : messages[state]}
</span>
</div>
{elem()}
</div> </div>
{errorType && <hr className="mb-8 border-gray-400" />}
<div className="flex flex-col">
<div className="flex flex-col">
<label htmlFor="email" className="mx-2 text-lg">
Email
</label>
<input
className="my-1 rounded-lg border-2 border-gray-500 bg-slate-800 bg-opacity-60 px-6 py-2 text-center text-inherit placeholder-slate-400 shadow-lg outline-none backdrop-blur-md focus-within:border-blue-500"
type="text"
name="email"
placeholder="user@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button
onClick={login("email")}
className="my-1 rounded-lg bg-blue-500 bg-opacity-75 px-10 py-3 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
>
Sign in with Email
</button>
</div>
<div className="flex flex-row items-center">
<hr className="w-full" />
<span className="mx-4 my-2">or</span>
<hr className="w-full" />
</div>
<div className="my-2 flex flex-col rounded-lg bg-gradient-to-tr from-[#fff8] via-[#fffd] to-[#fff8] p-4 shadow-lg drop-shadow-md">
<a
href="https://gbs-grafschaft.de/"
target="_blank"
rel="noreferrer"
>
<img
src="/images/logo-gbs.png"
loading="lazy"
alt="Gewerbliche Berufsbildende Schulen"
className="m-4 mt-2 w-60 justify-center"
/>
</a>
<button
onClick={login("azure-ad")}
className="flex w-full justify-evenly rounded-lg border border-gray-400 bg-slate-100 px-5 py-3 text-black drop-shadow-md duration-300 hover:bg-slate-200"
>
<img
src="/images/Microsoft_icon.svg"
loading="lazy"
height="24"
width="24"
alt="Microsoft_icon"
/>
<span>Sign in with Microsoft</span>
</button>
</div>
</div>
{errorType && <hr className="mt-8 border-gray-400" />}
{errorType && (
<div className="flex flex-col items-center">
<button
onClick={() => router.push("/")}
className="mt-10 rounded-lg border-2 border-gray-400 bg-gray-500 bg-opacity-75 px-16 py-2 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:border-blue-600"
>
<FontAwesomeIcon icon={faLeftLong} />
<span className="mx-4 font-bold">Return</span>
</button>
</div>
)}
</div> </div>
</div> </div>
) )

View file

@ -1,15 +1,14 @@
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons" import { signOut, useSession } from "next-auth/react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import React, { useEffect } from "react" import React, { useEffect } from "react"
function Logout() { function Logout() {
const { status } = useSession()
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => {
const timeout = setTimeout(() => router.push("/dev"), 2000) if (status === "unauthenticated") router.push("/signin")
return () => clearTimeout(timeout) }, [router, status])
}, [router])
return ( return (
<div className="flex h-screen w-full items-center justify-center bg-gray-900 bg-[url('/images/wallpaper.jpg')] bg-cover bg-center bg-no-repeat"> <div className="flex h-screen w-full items-center justify-center bg-gray-900 bg-[url('/images/wallpaper.jpg')] bg-cover bg-center bg-no-repeat">
@ -23,12 +22,17 @@ function Logout() {
alt="Avatar" alt="Avatar"
/> />
<h1 className="mb-2 text-2xl">Leaky Ships</h1> <h1 className="mb-2 text-2xl">Leaky Ships</h1>
<span className="text-gray-300">Logging out...</span> <span className="text-gray-300">Signout</span>
</div>
<div className="flex flex-col justify-start gap-4">
<span>Are you sure you want to sign out?</span>
<button
onClick={() => signOut({ callbackUrl: "/" })}
className="rounded-lg bg-blue-500 bg-opacity-75 px-10 py-3 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
>
Sign out
</button>
</div> </div>
<FontAwesomeIcon
className="mx-24 my-16 animate-spin text-6xl"
icon={faSpinnerThird}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -138,16 +138,30 @@ export default function Start() {
<Logo /> <Logo />
<BurgerMenu /> <BurgerMenu />
<div className="flex flex-col items-center rounded-xl border-4 border-black bg-grayish px-4 py-6 shadow-lg sm:mx-8 sm:p-12 md:w-full"> <div className="flex flex-col items-center rounded-xl border-4 border-black bg-grayish px-4 py-6 shadow-lg sm:mx-8 sm:p-12 md:w-full">
<button <div className="flex w-full justify-between">
className="-mt-2 w-20 self-start rounded-xl border-b-4 border-shield-gray bg-voidDark text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-5xl" <button
onClick={() => className="-mt-2 h-14 w-20 self-start rounded-xl border-b-4 border-shield-gray bg-voidDark text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-5xl"
setTimeout(() => { onClick={() =>
router.push("/") setTimeout(() => {
}, 200) router.push("/")
} }, 200)
> }
<FontAwesomeIcon icon={faLeftLong} /> >
</button> <FontAwesomeIcon icon={faLeftLong} />
</button>
{!session?.user.id && (
<button
className="-mt-2 h-14 w-20 self-start rounded-xl border-b-4 border-orange-500 bg-yellow-500 text-2xl active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-4xl"
onClick={() =>
setTimeout(() => {
router.push("/signin")
}, 200)
}
>
Login
</button>
)}
</div>
<div className="flex flex-col items-center gap-6 sm:gap-12"> <div className="flex flex-col items-center gap-6 sm:gap-12">
<OptionButton <OptionButton
callback={() => gameFetch()} callback={() => gameFetch()}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><path fill="#f35325" d="M0 0h10v10H0z"/><path fill="#81bc06" d="M11 0h10v10H11z"/><path fill="#05a6f0" d="M0 11h10v10H0z"/><path fill="#ffba08" d="M11 11h10v10H11z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB