ref: format
This commit is contained in:
parent
f54a8ccc0a
commit
b81a058a1c
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"semi": true
|
"semi": true
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
webapp:
|
webapp:
|
||||||
image: toogether/webapp
|
image: toogether/webapp
|
||||||
ports:
|
ports:
|
||||||
- "80:3000"
|
- "80:3000"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import { authOptions } from "@/authOptions";
|
import { authOptions } from "@/authOptions";
|
||||||
|
|
||||||
const handler = NextAuth(authOptions);
|
const handler = NextAuth(authOptions);
|
||||||
export { handler as GET, handler as POST };
|
export { handler as GET, handler as POST };
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
|
|
||||||
const LogoutPage = () => {
|
const LogoutPage = () => {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session) {
|
if (session) {
|
||||||
signOut();
|
signOut();
|
||||||
}
|
}
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LogoutPage;
|
export default LogoutPage;
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { NextUIProvider } from "@nextui-org/react";
|
import { NextUIProvider } from "@nextui-org/react";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
|
|
||||||
export default function AppWrapper({
|
export default function AppWrapper({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<NextUIProvider>
|
<NextUIProvider>
|
||||||
<NextThemesProvider attribute="class" defaultTheme="dark">
|
<NextThemesProvider attribute="class" defaultTheme="dark">
|
||||||
{children}
|
{children}
|
||||||
</NextThemesProvider>
|
</NextThemesProvider>
|
||||||
</NextUIProvider>
|
</NextUIProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,100 +1,108 @@
|
|||||||
'use client'
|
"use client";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownTrigger,
|
DropdownTrigger,
|
||||||
Navbar,
|
Navbar,
|
||||||
NavbarBrand,
|
NavbarBrand,
|
||||||
NavbarContent,
|
NavbarContent,
|
||||||
NavbarItem
|
NavbarItem,
|
||||||
} from '@nextui-org/react'
|
} from "@nextui-org/react";
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from "next-auth/react";
|
||||||
import { ThemeSwitcher } from '../ThemeSwitcher/ThemeSwitcher'
|
import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher";
|
||||||
import { axiosInstance } from '@/app/lib/axios'
|
import { axiosInstance } from "@/app/lib/axios";
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
const getInitials = (name: string) => {
|
const getInitials = (name: string) => {
|
||||||
if (!name) return ''
|
if (!name) return "";
|
||||||
|
|
||||||
const nameParts = name.split(' ')
|
const nameParts = name.split(" ");
|
||||||
if (nameParts.length === 1) {
|
if (nameParts.length === 1) {
|
||||||
return name
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstInitial = nameParts[0]?.[0] || ''
|
const firstInitial = nameParts[0]?.[0] || "";
|
||||||
const secondInitial = nameParts[1]?.[0] || nameParts[0]?.[1] || ''
|
const secondInitial = nameParts[1]?.[0] || nameParts[0]?.[1] || "";
|
||||||
|
|
||||||
return firstInitial + secondInitial
|
return firstInitial + secondInitial;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const [userProfile, setUserProfile] = useState<{
|
const [userProfile, setUserProfile] = useState<{
|
||||||
id: string
|
id: string;
|
||||||
username: string
|
username: string;
|
||||||
role: 'ADMIN' | 'STUDENT'
|
role: "ADMIN" | "STUDENT";
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
const initials = session?.user?.name ? getInitials(session.user.name) : ''
|
const initials = session?.user?.name ? getInitials(session.user.name) : "";
|
||||||
|
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
return await axiosInstance<{
|
return await axiosInstance<{
|
||||||
id: string
|
id: string;
|
||||||
username: string
|
username: string;
|
||||||
role: 'ADMIN' | 'STUDENT'
|
role: "ADMIN" | "STUDENT";
|
||||||
}>('/@me')
|
}>("/@me");
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUserProfile().then(r => {
|
fetchUserProfile().then((r) => {
|
||||||
setUserProfile(r.data)
|
setUserProfile(r.data);
|
||||||
})
|
});
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar className='mb-2'>
|
<Navbar className="mb-2">
|
||||||
<NavbarBrand>
|
<NavbarBrand>
|
||||||
<p className='font-bold text-inherit'>Toogether</p>
|
<p className="font-bold text-inherit">Toogether</p>
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
|
|
||||||
<NavbarContent as='div' justify='end'>
|
<NavbarContent as="div" justify="end">
|
||||||
<Dropdown placement='bottom-end'>
|
<Dropdown placement="bottom-end">
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Avatar
|
<Avatar
|
||||||
isBordered
|
isBordered
|
||||||
as='button'
|
as="button"
|
||||||
className='transition-transform'
|
className="transition-transform"
|
||||||
color='secondary'
|
color="secondary"
|
||||||
name={initials}
|
name={initials}
|
||||||
size='sm'
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label='Profile Actions' variant='flat'>
|
<DropdownMenu aria-label="Profile Actions" variant="flat">
|
||||||
<DropdownItem key='profile' className='h-14 gap-2'>
|
<DropdownItem key="profile" className="h-14 gap-2">
|
||||||
<p>Signed in as</p>
|
<p>Signed in as</p>
|
||||||
<p className='font-semibold'>{session?.user?.name}</p>
|
<p className="font-semibold">
|
||||||
</DropdownItem>
|
{session?.user?.name}
|
||||||
<DropdownItem key='settings'>Settings</DropdownItem>
|
</p>
|
||||||
<DropdownItem key='logout' color='danger' href='/auth/logout'>
|
</DropdownItem>
|
||||||
Logout
|
<DropdownItem key="settings">Settings</DropdownItem>
|
||||||
</DropdownItem>
|
<DropdownItem
|
||||||
</DropdownMenu>
|
key="logout"
|
||||||
</Dropdown>
|
color="danger"
|
||||||
<NavbarItem>
|
href="/auth/logout"
|
||||||
<ThemeSwitcher />
|
>
|
||||||
</NavbarItem>
|
Logout
|
||||||
{userProfile?.role === 'ADMIN' ? (
|
</DropdownItem>
|
||||||
<NavbarItem>
|
</DropdownMenu>
|
||||||
<Button onPress={() => router.push('/admin')}>🔧</Button>
|
</Dropdown>
|
||||||
</NavbarItem>
|
<NavbarItem>
|
||||||
) : null}
|
<ThemeSwitcher />
|
||||||
</NavbarContent>
|
</NavbarItem>
|
||||||
</Navbar>
|
{userProfile?.role === "ADMIN" ? (
|
||||||
)
|
<NavbarItem>
|
||||||
}
|
<Button onPress={() => router.push("/admin")}>
|
||||||
|
🔧
|
||||||
|
</Button>
|
||||||
|
</NavbarItem>
|
||||||
|
) : null}
|
||||||
|
</NavbarContent>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { parseDate, Time } from '@internationalized/date';
|
import { parseDate, Time } from "@internationalized/date";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -9,55 +9,55 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
DateInput,
|
DateInput,
|
||||||
Divider,
|
Divider,
|
||||||
TimeInput
|
TimeInput,
|
||||||
} from '@nextui-org/react';
|
} from "@nextui-org/react";
|
||||||
import { Room } from './Room';
|
import { Room } from "./Room";
|
||||||
import moment from 'moment';
|
import moment from "moment";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export const RoomCard = ({ id, name, date, Times, Presentator }: Room) => {
|
export const RoomCard = ({ id, name, date, Times, Presentator }: Room) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='w-[300px]'>
|
<Card className="w-[300px]">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<p className='text-md'>{name}</p>
|
<p className="text-md">{name}</p>
|
||||||
<p className='text-small text-default-500'>
|
<p className="text-small text-default-500">
|
||||||
{Presentator.username}
|
{Presentator.username}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<Divider />
|
<Divider />
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{Times.map(time => (
|
{Times.map((time) => (
|
||||||
<div className='flex flex-col gap-2' key={`${time.id}`}>
|
<div className="flex flex-col gap-2" key={`${time.id}`}>
|
||||||
<DateInput
|
<DateInput
|
||||||
isReadOnly
|
isReadOnly
|
||||||
label='Date'
|
label="Date"
|
||||||
value={parseDate(moment(date).format('YYYY-MM-DD'))}
|
value={parseDate(moment(date).format("YYYY-MM-DD"))}
|
||||||
/>
|
/>
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<TimeInput
|
<TimeInput
|
||||||
isReadOnly
|
isReadOnly
|
||||||
label='Start'
|
label="Start"
|
||||||
hourCycle={24}
|
hourCycle={24}
|
||||||
value={
|
value={
|
||||||
new Time(
|
new Time(
|
||||||
moment(time.startTime).hours(),
|
moment(time.startTime).hours(),
|
||||||
moment(time.startTime).minutes()
|
moment(time.startTime).minutes(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
<TimeInput
|
<TimeInput
|
||||||
isReadOnly
|
isReadOnly
|
||||||
label='End'
|
label="End"
|
||||||
hourCycle={24}
|
hourCycle={24}
|
||||||
value={
|
value={
|
||||||
new Time(
|
new Time(
|
||||||
moment(time.endTime).hours(),
|
moment(time.endTime).hours(),
|
||||||
moment(time.endTime).minutes()
|
moment(time.endTime).minutes(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -66,15 +66,13 @@ export const RoomCard = ({ id, name, date, Times, Presentator }: Room) => {
|
|||||||
))}
|
))}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
{moment(date).dayOfYear() === moment().dayOfYear() && (
|
{moment(date).dayOfYear() === moment().dayOfYear() && (
|
||||||
<div className='flex p-2'>
|
<div className="flex p-2">
|
||||||
<Button
|
<Button
|
||||||
className={
|
className={""}
|
||||||
''
|
color="primary"
|
||||||
}
|
radius="full"
|
||||||
color='primary'
|
size="sm"
|
||||||
radius='full'
|
variant={"flat"}
|
||||||
size='sm'
|
|
||||||
variant={'flat'}
|
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.push(`/room/${id}`);
|
router.push(`/room/${id}`);
|
||||||
}}
|
}}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from "react";
|
||||||
import { RoomCard } from './Card';
|
import { RoomCard } from "./Card";
|
||||||
import { Room } from './Room';
|
import { Room } from "./Room";
|
||||||
import { SkeletonRoomCard } from './SkeletonRoomCard';
|
import { SkeletonRoomCard } from "./SkeletonRoomCard";
|
||||||
|
|
||||||
export const RoomList = ({ rooms }: { rooms: Room[] }) => {
|
export const RoomList = ({ rooms }: { rooms: Room[] }) => {
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -18,7 +18,7 @@ export const RoomList = ({ rooms }: { rooms: Room[] }) => {
|
|||||||
const isEnd = goLeft
|
const isEnd = goLeft
|
||||||
? scrollContainer.scrollLeft === 0
|
? scrollContainer.scrollLeft === 0
|
||||||
: scrollContainer.scrollLeft + scrollContainer.clientWidth >=
|
: scrollContainer.scrollLeft + scrollContainer.clientWidth >=
|
||||||
scrollContainer.scrollWidth;
|
scrollContainer.scrollWidth;
|
||||||
if (isEnd) return;
|
if (isEnd) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -40,22 +40,22 @@ export const RoomList = ({ rooms }: { rooms: Room[] }) => {
|
|||||||
const scrollContainer = scrollContainerRef.current;
|
const scrollContainer = scrollContainerRef.current;
|
||||||
if (!scrollContainer) return;
|
if (!scrollContainer) return;
|
||||||
|
|
||||||
scrollContainer.addEventListener('wheel', handleWheel);
|
scrollContainer.addEventListener("wheel", handleWheel);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
scrollContainer.removeEventListener('wheel', handleWheel);
|
scrollContainer.removeEventListener("wheel", handleWheel);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
className='overflow-x-auto scrollbar-hide rounded-xl bg-default-100'
|
className="overflow-x-auto scrollbar-hide rounded-xl bg-default-100"
|
||||||
>
|
>
|
||||||
<ul className='flex'>
|
<ul className="flex">
|
||||||
{rooms?.length > 0 ? (
|
{rooms?.length > 0 ? (
|
||||||
rooms.map(room => (
|
rooms.map((room) => (
|
||||||
<li key={room.id} className='p-2'>
|
<li key={room.id} className="p-2">
|
||||||
<RoomCard
|
<RoomCard
|
||||||
id={room.id}
|
id={room.id}
|
||||||
name={room.name}
|
name={room.name}
|
||||||
@ -68,7 +68,7 @@ export const RoomList = ({ rooms }: { rooms: Room[] }) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<li key={i} className='p-2'>
|
<li key={i} className="p-2">
|
||||||
<SkeletonRoomCard />
|
<SkeletonRoomCard />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
4
src/app/components/Room/Room.d.ts
vendored
4
src/app/components/Room/Room.d.ts
vendored
@ -13,5 +13,5 @@ export interface Room {
|
|||||||
username: string;
|
username: string;
|
||||||
role: string;
|
role: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import { Card, Skeleton, Divider } from "@nextui-org/react"
|
import { Card, Skeleton, Divider } from "@nextui-org/react";
|
||||||
|
|
||||||
export const SkeletonRoomCard = () => {
|
export const SkeletonRoomCard = () => {
|
||||||
return (
|
return (
|
||||||
<Card className='w-[200px] space-y-5 p-4' radius='lg'>
|
<Card className="w-[200px] space-y-5 p-4" radius="lg">
|
||||||
<div className='flex flex-col gap-2'>
|
<div className="flex flex-col gap-2">
|
||||||
<Skeleton className='w-4/5 rounded-lg'>
|
<Skeleton className="w-4/5 rounded-lg">
|
||||||
<div className='h-3 w-4/5 rounded-lg bg-default-200' />
|
<div className="h-3 w-4/5 rounded-lg bg-default-200" />
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Skeleton className='w-2/5 rounded-lg'>
|
<Skeleton className="w-2/5 rounded-lg">
|
||||||
<div className='h-3 w-2/5 rounded-lg bg-default-300' />
|
<div className="h-3 w-2/5 rounded-lg bg-default-300" />
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Skeleton className='rounded-lg'>
|
<Skeleton className="rounded-lg">
|
||||||
<div className='h-12 rounded-lg bg-default-300' />
|
<div className="h-12 rounded-lg bg-default-300" />
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton className='rounded-lg w-1/2'>
|
<Skeleton className="rounded-lg w-1/2">
|
||||||
<div className='h-10 rounded-lg bg-default-300' />
|
<div className="h-10 rounded-lg bg-default-300" />
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
<Skeleton className='rounded-lg w-1/2'>
|
<Skeleton className="rounded-lg w-1/2">
|
||||||
<div className='h-10 rounded-lg bg-default-300' />
|
<div className="h-10 rounded-lg bg-default-300" />
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
'use client'
|
"use client";
|
||||||
import { Button } from '@nextui-org/react'
|
import { Button } from "@nextui-org/react";
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from "next-themes";
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const ThemeSwitcher = () => {
|
export const ThemeSwitcher = () => {
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false);
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (!mounted) return null
|
if (!mounted) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onPress={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
|
<Button onPress={() => setTheme(theme === "light" ? "dark" : "light")}>
|
||||||
{theme === 'light' ? '🌑' : '☀️'}
|
{theme === "light" ? "🌑" : "☀️"}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment";
|
||||||
import { getSession } from "next-auth/react";
|
import { getSession } from "next-auth/react";
|
||||||
|
|
||||||
moment.locale("fr");
|
moment.locale("fr");
|
||||||
|
|
||||||
let cachedAccessToken: string | null = null;
|
let cachedAccessToken: string | null = null;
|
||||||
let tokenExpirationAt: Moment | null = null;
|
let tokenExpirationAt: Moment | null = null;
|
||||||
|
|
||||||
export const axiosInstance = axios.create({
|
export const axiosInstance = axios.create({
|
||||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosInstance.interceptors.request.use(async (config) => {
|
axiosInstance.interceptors.request.use(async (config) => {
|
||||||
if (tokenExpirationAt && moment().isBefore(tokenExpirationAt)) {
|
if (tokenExpirationAt && moment().isBefore(tokenExpirationAt)) {
|
||||||
config.headers.Authorization = `Bearer ${cachedAccessToken}`;
|
config.headers.Authorization = `Bearer ${cachedAccessToken}`;
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error("User is not authenticated");
|
throw new Error("User is not authenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedAccessToken = session.accessToken;
|
cachedAccessToken = session.accessToken;
|
||||||
tokenExpirationAt = moment(session.accessTokenExpires);
|
tokenExpirationAt = moment(session.accessTokenExpires);
|
||||||
|
|
||||||
config.headers.Authorization = `Bearer ${cachedAccessToken}`;
|
config.headers.Authorization = `Bearer ${cachedAccessToken}`;
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export const UppercaseFirstLetter = (str: string) => {
|
export const UppercaseFirstLetter = (str: string) => {
|
||||||
return str.slice(0, 1).toLocaleUpperCase() + str.slice(1);
|
return str.slice(0, 1).toLocaleUpperCase() + str.slice(1);
|
||||||
}
|
};
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { Card, Divider, Skeleton } from '@nextui-org/react';
|
import { Card, Divider, Skeleton } from "@nextui-org/react";
|
||||||
import moment from 'moment';
|
import moment from "moment";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { Room } from './components/Room/Room';
|
import { Room } from "./components/Room/Room";
|
||||||
import { RoomList } from './components/Room/List';
|
import { RoomList } from "./components/Room/List";
|
||||||
import { Header } from './components/Header';
|
import { Header } from "./components/Header";
|
||||||
import { axiosInstance } from './lib/axios';
|
import { axiosInstance } from "./lib/axios";
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const [roomsLoading, setRoomsLoading] = useState(true);
|
const [roomsLoading, setRoomsLoading] = useState(true);
|
||||||
@ -16,30 +16,30 @@ const HomePage = () => {
|
|||||||
}>({
|
}>({
|
||||||
future: [],
|
future: [],
|
||||||
actual: [],
|
actual: [],
|
||||||
past: []
|
past: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.get<{ id: string; name: string; createdAt: string }[]>(
|
.get<
|
||||||
'/@me/class'
|
{ id: string; name: string; createdAt: string }[]
|
||||||
)
|
>("/@me/class")
|
||||||
.then(classResponse => {
|
.then((classResponse) => {
|
||||||
if (classResponse.data.length)
|
if (classResponse.data.length)
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.get<Room[]>(
|
.get<
|
||||||
`/@me/class/${classResponse.data[0].id}/rooms`
|
Room[]
|
||||||
)
|
>(`/@me/class/${classResponse.data[0].id}/rooms`)
|
||||||
.then(classes => {
|
.then((classes) => {
|
||||||
// Filter rooms by date, get future, actual and past rooms
|
// Filter rooms by date, get future, actual and past rooms
|
||||||
const future = classes.data.filter(room =>
|
const future = classes.data.filter((room) =>
|
||||||
moment(room.date).isAfter(moment(), 'day')
|
moment(room.date).isAfter(moment(), "day"),
|
||||||
);
|
);
|
||||||
const actual = classes.data.filter(room =>
|
const actual = classes.data.filter((room) =>
|
||||||
moment(room.date).isSame(moment(), 'day')
|
moment(room.date).isSame(moment(), "day"),
|
||||||
);
|
);
|
||||||
const past = classes.data.filter(room =>
|
const past = classes.data.filter((room) =>
|
||||||
moment(room.date).isBefore(moment())
|
moment(room.date).isBefore(moment()),
|
||||||
);
|
);
|
||||||
|
|
||||||
setRooms({ future, actual, past });
|
setRooms({ future, actual, past });
|
||||||
@ -51,17 +51,17 @@ const HomePage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<main className='flex flex-col gap-8 p-4'>
|
<main className="flex flex-col gap-8 p-4">
|
||||||
<section className='flex flex-col gap-2'>
|
<section className="flex flex-col gap-2">
|
||||||
<h2 className='font-semibold text-lg'>Upcoming</h2>
|
<h2 className="font-semibold text-lg">Upcoming</h2>
|
||||||
<RoomList rooms={rooms.future} />
|
<RoomList rooms={rooms.future} />
|
||||||
</section>
|
</section>
|
||||||
<section className='flex flex-col gap-2'>
|
<section className="flex flex-col gap-2">
|
||||||
<h2 className='font-semibold text-lg'>Current</h2>
|
<h2 className="font-semibold text-lg">Current</h2>
|
||||||
<RoomList rooms={rooms.actual} />
|
<RoomList rooms={rooms.actual} />
|
||||||
</section>
|
</section>
|
||||||
<section className='flex flex-col gap-2'>
|
<section className="flex flex-col gap-2">
|
||||||
<h2 className='font-semibold text-lg'>Past</h2>
|
<h2 className="font-semibold text-lg">Past</h2>
|
||||||
<RoomList rooms={rooms.past} />
|
<RoomList rooms={rooms.past} />
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
24
src/app/types/env.d.ts
vendored
24
src/app/types/env.d.ts
vendored
@ -1,12 +1,12 @@
|
|||||||
declare namespace NodeJS {
|
declare namespace NodeJS {
|
||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
OAUTH_PROVIDER_NAME: string;
|
OAUTH_PROVIDER_NAME: string;
|
||||||
OAUTH_CLIENT_ID: string;
|
OAUTH_CLIENT_ID: string;
|
||||||
OAUTH_CLIENT_SECRET: string;
|
OAUTH_CLIENT_SECRET: string;
|
||||||
OAUTH_AUTHORIZATION_URL: string;
|
OAUTH_AUTHORIZATION_URL: string;
|
||||||
OAUTH_TOKEN_URL: string;
|
OAUTH_TOKEN_URL: string;
|
||||||
OAUTH_USERINFO_URL: string;
|
OAUTH_USERINFO_URL: string;
|
||||||
OAUTH_ISSUER: string;
|
OAUTH_ISSUER: string;
|
||||||
OAUTH_JWKS_ENDPOINT: string;
|
OAUTH_JWKS_ENDPOINT: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
84
src/app/types/next-auth.d.ts
vendored
84
src/app/types/next-auth.d.ts
vendored
@ -1,42 +1,42 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { Moment } from "moment";
|
import { Moment } from "moment";
|
||||||
import NextAuth, { DefaultSession } from "next-auth";
|
import NextAuth, { DefaultSession } from "next-auth";
|
||||||
import { JWT } from "next-auth/jwt";
|
import { JWT } from "next-auth/jwt";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
sub: string;
|
sub: string;
|
||||||
name: string;
|
name: string;
|
||||||
preferred_username: string;
|
preferred_username: string;
|
||||||
given_name: string;
|
given_name: string;
|
||||||
family_name: string;
|
family_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "next-auth" {
|
declare module "next-auth" {
|
||||||
interface Session extends DefaultSession {
|
interface Session extends DefaultSession {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
accessTokenExpires: Moment;
|
accessTokenExpires: Moment;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
expires_at: number;
|
expires_at: number;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "next-auth/jwt" {
|
declare module "next-auth/jwt" {
|
||||||
interface JWT {
|
interface JWT {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessTokenExpires: Moment;
|
accessTokenExpires: Moment;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
user: User | AdapterUser;
|
user: User | AdapterUser;
|
||||||
iat: number;
|
iat: number;
|
||||||
exp: number;
|
exp: number;
|
||||||
jti: string;
|
jti: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,106 +1,110 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { AuthOptions, Session } from "next-auth";
|
import { AuthOptions, Session } from "next-auth";
|
||||||
import { JWT } from "next-auth/jwt";
|
import { JWT } from "next-auth/jwt";
|
||||||
|
|
||||||
moment.locale("fr");
|
moment.locale("fr");
|
||||||
|
|
||||||
export const authOptions: AuthOptions = {
|
export const authOptions: AuthOptions = {
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
id: "oauth",
|
id: "oauth",
|
||||||
name: process.env.OAUTH_PROVIDER_NAME,
|
name: process.env.OAUTH_PROVIDER_NAME,
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
clientId: process.env.OAUTH_CLIENT_ID,
|
clientId: process.env.OAUTH_CLIENT_ID,
|
||||||
clientSecret: process.env.OAUTH_CLIENT_SECRET,
|
clientSecret: process.env.OAUTH_CLIENT_SECRET,
|
||||||
authorization: {
|
authorization: {
|
||||||
url: process.env.OAUTH_AUTHORIZATION_URL,
|
url: process.env.OAUTH_AUTHORIZATION_URL,
|
||||||
params: {
|
params: {
|
||||||
scope: "openid profile offline_access",
|
scope: "openid profile offline_access",
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
checks: ["pkce", "state"],
|
checks: ["pkce", "state"],
|
||||||
idToken: true,
|
idToken: true,
|
||||||
token: process.env.OAUTH_TOKEN_URL,
|
token: process.env.OAUTH_TOKEN_URL,
|
||||||
userinfo: process.env.OAUTH_USERINFO_URL,
|
userinfo: process.env.OAUTH_USERINFO_URL,
|
||||||
issuer: process.env.OAUTH_ISSUER,
|
issuer: process.env.OAUTH_ISSUER,
|
||||||
jwks_endpoint: process.env.OAUTH_JWKS_ENDPOINT,
|
jwks_endpoint: process.env.OAUTH_JWKS_ENDPOINT,
|
||||||
profile(profile: Session["user"]) {
|
profile(profile: Session["user"]) {
|
||||||
return {
|
return {
|
||||||
id: profile.sub || profile.id,
|
id: profile.sub || profile.id,
|
||||||
name:
|
name:
|
||||||
profile.name ||
|
profile.name ||
|
||||||
profile.preferred_username ||
|
profile.preferred_username ||
|
||||||
`${profile.given_name} ${profile.family_name}`,
|
`${profile.given_name} ${profile.family_name}`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account, user }) {
|
async jwt({ token, account, user }) {
|
||||||
if (account && user) {
|
if (account && user) {
|
||||||
token.accessToken = account.access_token;
|
token.accessToken = account.access_token;
|
||||||
token.accessTokenExpires = moment(account.expires_at * 1000).subtract(5, "s");
|
token.accessTokenExpires = moment(
|
||||||
token.refreshToken = account.refresh_token;
|
account.expires_at * 1000,
|
||||||
token.user = user;
|
).subtract(5, "s");
|
||||||
return token;
|
token.refreshToken = account.refresh_token;
|
||||||
}
|
token.user = user;
|
||||||
|
return token;
|
||||||
if (moment().isBefore(moment(token.accessTokenExpires))) {
|
}
|
||||||
return token;
|
|
||||||
}
|
if (moment().isBefore(moment(token.accessTokenExpires))) {
|
||||||
|
return token;
|
||||||
return refreshAccessToken(token);
|
}
|
||||||
},
|
|
||||||
async session({ session, token }) {
|
return refreshAccessToken(token);
|
||||||
if (token) {
|
},
|
||||||
session.user = token.user;
|
async session({ session, token }) {
|
||||||
session.accessToken = token.accessToken;
|
if (token) {
|
||||||
session.accessTokenExpires = token.accessTokenExpires;
|
session.user = token.user;
|
||||||
session.error = token.error;
|
session.accessToken = token.accessToken;
|
||||||
}
|
session.accessTokenExpires = token.accessTokenExpires;
|
||||||
|
session.error = token.error;
|
||||||
return session;
|
}
|
||||||
},
|
|
||||||
},
|
return session;
|
||||||
pages: {
|
},
|
||||||
signIn: "/auth/login",
|
},
|
||||||
signOut: "/auth/logout",
|
pages: {
|
||||||
}
|
signIn: "/auth/login",
|
||||||
};
|
signOut: "/auth/logout",
|
||||||
|
},
|
||||||
const refreshAccessToken = async (token: JWT): Promise<JWT> => {
|
};
|
||||||
const response = await axios.post<{
|
|
||||||
access_token: string;
|
const refreshAccessToken = async (token: JWT): Promise<JWT> => {
|
||||||
expires_in: number;
|
const response = await axios.post<{
|
||||||
refresh_token: string;
|
access_token: string;
|
||||||
error_description?: string;
|
expires_in: number;
|
||||||
}>(
|
refresh_token: string;
|
||||||
process.env.OAUTH_TOKEN_URL,
|
error_description?: string;
|
||||||
{
|
}>(
|
||||||
grant_type: "refresh_token",
|
process.env.OAUTH_TOKEN_URL,
|
||||||
refresh_token: token.refreshToken,
|
{
|
||||||
client_id: process.env.OAUTH_CLIENT_ID,
|
grant_type: "refresh_token",
|
||||||
client_secret: process.env.OAUTH_CLIENT_SECRET,
|
refresh_token: token.refreshToken,
|
||||||
},
|
client_id: process.env.OAUTH_CLIENT_ID,
|
||||||
{
|
client_secret: process.env.OAUTH_CLIENT_SECRET,
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
{
|
||||||
},
|
headers: {
|
||||||
}
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
);
|
},
|
||||||
|
},
|
||||||
if (response.status != 200) {
|
);
|
||||||
throw new Error(
|
|
||||||
response.data.error_description || "Failed to refresh access token"
|
if (response.status != 200) {
|
||||||
);
|
throw new Error(
|
||||||
}
|
response.data.error_description || "Failed to refresh access token",
|
||||||
|
);
|
||||||
return {
|
}
|
||||||
...token,
|
|
||||||
accessToken: response.data.access_token,
|
return {
|
||||||
accessTokenExpires: moment().add(response.data.expires_in, "seconds").subtract(5, "s"),
|
...token,
|
||||||
refreshToken: response.data.refresh_token,
|
accessToken: response.data.access_token,
|
||||||
};
|
accessTokenExpires: moment()
|
||||||
};
|
.add(response.data.expires_in, "seconds")
|
||||||
|
.subtract(5, "s"),
|
||||||
|
refreshToken: response.data.refresh_token,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
export async function middleware(req: NextRequest) {
|
export async function middleware(req: NextRequest) {
|
||||||
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
|
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
|
||||||
const isAuth = !!token;
|
const isAuth = !!token;
|
||||||
|
|
||||||
const url = req.nextUrl.clone();
|
const url = req.nextUrl.clone();
|
||||||
|
|
||||||
if (isAuth && url.pathname === "/auth/login") {
|
if (isAuth && url.pathname === "/auth/login") {
|
||||||
url.pathname = "/";
|
url.pathname = "/";
|
||||||
return NextResponse.redirect(url);
|
return NextResponse.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuth && url.pathname !== "/auth/login") {
|
if (!isAuth && url.pathname !== "/auth/login") {
|
||||||
url.pathname = "/auth/login";
|
url.pathname = "/auth/login";
|
||||||
return NextResponse.redirect(url);
|
return NextResponse.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/((?!api|_next|static|favicon.ico).*)"],
|
matcher: ["/((?!api|_next|static|favicon.ico).*)"],
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user