feat(front): routes secured under jwt session + small user component done

This commit is contained in:
2024-07-25 00:03:48 +02:00
parent fd81b532d0
commit 75e1fe4c65
25 changed files with 179 additions and 24 deletions

View File

@@ -1,3 +1,3 @@
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_URL=http://localhost:3016
# GEN AUTH_SECRET: $ openssl rand -base64 32
NEXTAUTH_SECRET=6lHRWUvCBtqlgTWc6aFn6s6PudYjuN6oUY+RrcEntTU=

View File

@@ -9,6 +9,16 @@ const nextConfig = {
prependData: `@import "@/styles/variables";`,
includePaths: [path.join("@", "styles")],
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "picsum.photos",
port: "",
pathname: "/**",
},
],
},
};
export default nextConfig;

View File

@@ -1 +1 @@
export { ApplicationLayout as default } from "@/modules/common/layouts/application/application.layout";
export { HomeLayout as default } from "@/modules/common/layouts/home";

View File

@@ -1,4 +1,3 @@
import { SignInWidget } from "@/modules/auth/components/sign-in/sign-in.widget";
import ThemeSwitcher from "@/modules/common/theme-switcher";
export default function Home() {
@@ -14,7 +13,6 @@ export default function Home() {
>
Main page
<ThemeSwitcher />
<SignInWidget />
</main>
);
}

View File

@@ -0,0 +1 @@
export { LogInView as default } from "@/modules/auth/views/log-in/log-in.view";

17
front/src/middleware.ts Normal file
View File

@@ -0,0 +1,17 @@
import { withAuth } from "next-auth/middleware";
import { authOptions } from "./modules/auth/configs/auth.options";
export default withAuth({
pages: authOptions.pages,
callbacks: {
authorized({ req, token }) {
if (token) return true;
const pathname = req.nextUrl.pathname;
return (
pathname.startsWith("/_next/") ||
pathname.startsWith("/favicon.ico") ||
pathname.startsWith("/assets/")
);
},
},
});

View File

@@ -1,5 +1,6 @@
.container {
max-width: 20em;
height: fit-content;
display: flex;
flex-direction: column;

View File

@@ -1,18 +1,18 @@
"use client";
import { FormEvent, useState } from "react";
import styles from "./sign-in.module.scss";
import { FormEvent, useEffect, useState } from "react";
import styles from "./log-in.module.scss";
import { signIn, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
type SingInWidgetsProps = {
type LogInWidgetsProps = {
afterSuccess?: Function;
};
export const SignInWidget: React.FC<SingInWidgetsProps> = ({
afterSuccess,
}) => {
export const LogInWidget: React.FC<LogInWidgetsProps> = ({ afterSuccess }) => {
const { data } = useSession();
const router = useRouter();
const [loginStatus, setLoginStatus] = useState<
"idle" | "check" | "confirm" | "error"
@@ -41,9 +41,15 @@ export const SignInWidget: React.FC<SingInWidgetsProps> = ({
}
};
useEffect(() => {
if (loginStatus === "confirm") {
router.push("/");
}
}, [loginStatus]);
return (
<div className={styles.container}>
<h1 className={styles["heading"]}>Sign in</h1>
<h1 className={styles["heading"]}>Log in</h1>
<form className={styles.form} onSubmit={submit}>
<div className={`${styles["form__group"]}`}>
<label htmlFor="username" className={styles["form__label"]}>

View File

@@ -0,0 +1 @@
export { UserDropdown } from "./user-dropdown.component";

View File

@@ -0,0 +1,14 @@
import { UserFrame } from "../user-frame";
import styles from "./user-dropdown.module.scss";
export const UserDropdown = () => {
return (
<div className={styles.container}>
<div className={styles["avatar__container"]}>
<UserFrame />
</div>
<div></div>
<button>Log out</button>
</div>
);
};

View File

@@ -0,0 +1,8 @@
.container {
.avatar__container {
width: 6rem;
height: 6rem;
font-size: 3rem;
}
}

View File

@@ -0,0 +1 @@
export { UserFrame } from "./user-frame.component";

View File

@@ -0,0 +1,39 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../configs/auth.options";
import Image from "next/image";
import styles from "./user-frame.module.scss";
export async function UserFrame() {
const session = await getServerSession(authOptions);
return (
<div className={styles.frame}>
<span className={styles.initials}>
{getInitialsFromName(session?.user?.name || "")}
</span>
<Image
className={styles["profile__picture"]}
src={session?.user?.image || ""}
alt="user profile picture"
fill={true}
sizes="(max-width: 768px) 10rem, 6rem"
priority={true}
/>
</div>
);
}
const getInitialsFromName = (name: string): string => {
if (name.length < 1) return "00";
// TODO: this can be done in one line probably
const words = name.split(" ");
let initials = "";
words.forEach((word) => (initials += word[0]));
if (initials.length < 2) {
initials += name[1];
}
return initials.slice(0, 2);
};

View File

@@ -0,0 +1,23 @@
.frame {
position: relative;
font-size: inherit;
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
border: 1px solid $color-grey-medium;
box-sizing: border-box;
& > span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: inherit;
text-transform: uppercase;
color: $color-grey-medium;
}
}

View File

@@ -6,7 +6,7 @@ export const authOptions: AuthOptions = {
strategy: "jwt",
},
pages: {
signIn: "/auth/sign-in",
signIn: "/auth/log-in",
},
providers: [
CredentialsProvider({
@@ -50,7 +50,7 @@ export const authOptions: AuthOptions = {
const user: User = {
id: token_payload.username,
roles: token_payload.roles,
image: "https://randomuser.me/api/portraits/women/92.jpg",
image: "https://picsum.photos/200/300",
name: token_payload.username,
apiSession: {
accessToken: response_body.access_token,

View File

@@ -0,0 +1,7 @@
.container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,12 @@
import { LogInWidget } from "../../components/log-in/log-in.widget";
import styles from "./log-in.module.scss";
type LogInWidgetsProps = {};
export const LogInView: React.FC<LogInWidgetsProps> = ({}) => {
return (
<div className={styles.container}>
<LogInWidget />
</div>
);
};

View File

@@ -0,0 +1,5 @@
import { UserDropdown } from "@/modules/auth/components/user-dropdown";
export const Header = () => {
return <UserDropdown />;
};

View File

View File

@@ -0,0 +1 @@
export { Header } from "./header.component";

View File

@@ -1,8 +0,0 @@
import { PropsWithChildren } from "react";
import { ApplicationProvider } from "../../providers/application.provider";
export const ApplicationLayout: React.FC<PropsWithChildren> = ({
children,
}) => {
return <ApplicationProvider>{children}</ApplicationProvider>;
};

View File

@@ -0,0 +1,13 @@
import { PropsWithChildren } from "react";
import styles from "./home.module.scss";
import { Header } from "../../components/header";
export const HomeLayout: React.FC<PropsWithChildren> = ({ children }) => {
return (
<div className={styles.container}>
<Header />
{children}
</div>
);
};

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
export { HomeLayout } from "./home.layout";

View File

@@ -1,9 +1,11 @@
import type { Metadata } from "next";
import "@/styles/main.scss";
import { ApplicationProvider } from "../../providers/application.provider";
export const metadata: Metadata = {
title: "Personal finance - Home",
description: "Manage your money blablabla",
title: "Full stack archetype",
description:
"This is a full stack archetype supported in NextJS, NestJS and mysql",
};
export function RootLayout({
@@ -13,7 +15,9 @@ export function RootLayout({
}>) {
return (
<html lang="en">
<body>{children}</body>
<body>
<ApplicationProvider>{children}</ApplicationProvider>
</body>
</html>
);
}