finally building
This commit is contained in:
36
apps/admin/app/(main)/layout.tsx
Normal file
36
apps/admin/app/(main)/layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
} from '@workspace/ui/components/navigation-menu';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<header className="flex h-16 items-center justify-between border-b bg-background px-4 md:px-6">
|
||||
<nav>
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/">Home</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/students">Students</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</nav>
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import Login from '@/components/login';
|
||||
import Studs from '@/components/studs';
|
||||
import { db, admins } from '@workspace/db';
|
||||
import { auth, signIn, signOut } from '@workspace/auth';
|
||||
import { auth, signIn, signOut } from '@/auth';
|
||||
|
||||
async function getStudents() {
|
||||
'use server';
|
||||
25
apps/admin/app/(main)/students/columns.tsx
Normal file
25
apps/admin/app/(main)/students/columns.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { createSelectSchema, students } from '@workspace/db';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
const studentSelectSchema = createSelectSchema(students);
|
||||
export type Student = z.infer<typeof studentSelectSchema>;
|
||||
|
||||
export const columns: ColumnDef<Student>[] = [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'firstName',
|
||||
header: 'First Name',
|
||||
},
|
||||
{
|
||||
accessorKey: 'lastName',
|
||||
header: 'Last Name',
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
},
|
||||
];
|
||||
66
apps/admin/app/(main)/students/data-table.tsx
Normal file
66
apps/admin/app/(main)/students/data-table.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@workspace/ui/components/table';
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
apps/admin/app/(main)/students/page.tsx
Normal file
18
apps/admin/app/(main)/students/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { columns, Student } from './columns';
|
||||
import { DataTable } from './data-table';
|
||||
import { db, students } from '@workspace/db';
|
||||
|
||||
async function getData(): Promise<Student[]> {
|
||||
const data = db.select().from(students);
|
||||
return data;
|
||||
}
|
||||
|
||||
export default async function DemoPage() {
|
||||
const data = await getData();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10">
|
||||
<DataTable columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
import { handlers } from '@workspace/auth';
|
||||
import { handlers } from '@/auth';
|
||||
export const { GET, POST } = handlers;
|
||||
|
||||
29
apps/admin/app/login/page.tsx
Normal file
29
apps/admin/app/login/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { signIn } from '@/auth';
|
||||
|
||||
async function logIn() {
|
||||
'use server';
|
||||
await signIn('google', { redirectTo: '/' });
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-svh">
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<form action={logIn}>
|
||||
<Button type="submit" variant="outline" className="w-full h-12">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/0 via-primary/10 to-primary/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 ease-out pointer-events-none" />
|
||||
<img
|
||||
src="https://static.cdnlogo.com/logos/g/35/google-icon.svg"
|
||||
alt="Google logo"
|
||||
className="w-5 h-5 transition-transform duration-200"
|
||||
/>
|
||||
<span className="relative z-10 font-medium transition-colors duration-200 group-hover:text-foreground">
|
||||
Sign in with Google
|
||||
</span>
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
apps/admin/auth.ts
Normal file
87
apps/admin/auth.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import NextAuth, { type DefaultSession } from 'next-auth';
|
||||
import type { NextAuthConfig } from 'next-auth';
|
||||
import Google from "next-auth/providers/google";
|
||||
import { db, admins, students } from '@workspace/db';
|
||||
import { eq } from '@workspace/db/drizzle';
|
||||
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
role?: 'ADMIN' | 'USER';
|
||||
adminId?: number;
|
||||
studentId?: number;
|
||||
[key: string]: any;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
|
||||
interface JWT {
|
||||
role?: 'ADMIN' | 'USER';
|
||||
adminId?: number;
|
||||
studentId?: number;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'next/server' {
|
||||
interface NextRequest {
|
||||
auth: import('next-auth').Session | null;
|
||||
}
|
||||
}
|
||||
|
||||
const authConfig: NextAuthConfig = {
|
||||
providers: [Google],
|
||||
callbacks: {
|
||||
async jwt({ token, account, user, profile }) {
|
||||
// Only check DB on first sign in
|
||||
if (account && user && user.email) {
|
||||
const admin = await db.select().from(admins).where(eq(admins.email, user.email)).limit(1);
|
||||
if (admin.length > 0 && admin[0]) {
|
||||
token.role = 'ADMIN';
|
||||
token.adminId = admin[0].id;
|
||||
} else {
|
||||
token.role = 'USER';
|
||||
const student = await db
|
||||
.select()
|
||||
.from(students)
|
||||
.where(eq(students.email, user.email))
|
||||
.limit(1);
|
||||
if (student.length > 0 && student[0]) {
|
||||
token.studentId = student[0].id;
|
||||
} else {
|
||||
const nameParts = user.name?.split(' ') ?? [];
|
||||
const firstName = nameParts[0] || '';
|
||||
const lastName = nameParts.slice(1).join(' ') || '';
|
||||
const newStudent = await db
|
||||
.insert(students)
|
||||
.values({
|
||||
email: user.email,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
profilePicture: user.image,
|
||||
})
|
||||
.returning({ id: students.id });
|
||||
if (newStudent[0]) {
|
||||
token.studentId = newStudent[0].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (token?.role) {
|
||||
session.user.role = token.role as 'ADMIN' | 'USER';
|
||||
}
|
||||
if (token?.adminId) {
|
||||
session.user.adminId = token.adminId as number;
|
||||
}
|
||||
if (token?.studentId) {
|
||||
session.user.studentId = token.studentId as number;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Note: TypeScript warnings about inferred types are expected with NextAuth v5 beta
|
||||
// These warnings don't affect functionality
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth(authConfig);
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
import { signIn } from '@workspace/auth';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
|
||||
export default function Login({ action }: { action: () => Promise<void> }) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { auth } from '@workspace/auth';
|
||||
import { auth } from '@/auth';
|
||||
|
||||
export default async function Studs({
|
||||
action,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { auth } from '@workspace/auth';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { NextResponse, NextRequest } from 'next/server';
|
||||
|
||||
export default auth((req: any) => {
|
||||
// If the user is unauthenticated or an admin, allow the request.
|
||||
if (!req.auth || req.auth.user?.role === 'ADMIN') {
|
||||
export default auth((req: NextRequest) => {
|
||||
if (!req.auth) {
|
||||
return NextResponse.redirect(new URL('/login', req.url));
|
||||
}
|
||||
|
||||
if (req.auth.user?.role === 'ADMIN') {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
@@ -11,8 +14,8 @@ export default auth((req: any) => {
|
||||
const studentUrl = process.env.STUDENT_URL ?? 'http://localhost:3000';
|
||||
|
||||
return NextResponse.redirect(new URL(studentUrl, req.url));
|
||||
}) as any;
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|login).*)'],
|
||||
};
|
||||
|
||||
19
apps/admin/next-auth.d.ts
vendored
19
apps/admin/next-auth.d.ts
vendored
@@ -1,19 +0,0 @@
|
||||
import 'next-auth';
|
||||
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
role?: 'ADMIN' | 'USER';
|
||||
adminId?: number;
|
||||
studentId?: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
declare module 'next-auth/jwt' {
|
||||
interface JWT {
|
||||
role?: 'ADMIN' | 'USER';
|
||||
adminId?: number;
|
||||
studentId?: number;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ['@workspace/ui', '@workspace/db', '@workspace/auth'],
|
||||
transpilePackages: ['@workspace/ui', '@workspace/db'],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -12,24 +12,25 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@workspace/auth": "workspace:*",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@workspace/db": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"framer-motion": "^12.19.1",
|
||||
"framer-motion": "^12.22.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"next": "^15.2.3",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"next": "^15.3.4",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/node": "^20.19.4",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@workspace/eslint-config": "workspace:^",
|
||||
"@workspace/typescript-config": "workspace:*",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,18 @@
|
||||
"extends": "@workspace/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@workspace/ui/*": ["../../packages/ui/src/*"],
|
||||
"@workspace/db/*": ["../../packages/db/src/*"],
|
||||
"@workspace/auth/*": ["../../packages/auth/src/*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
],
|
||||
"@workspace/ui/*": [
|
||||
"../../packages/ui/src/*"
|
||||
],
|
||||
"@workspace/db": [
|
||||
"../../packages/db/index.ts"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
@@ -14,6 +21,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "next.config.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"next-env.d.ts",
|
||||
"next.config.ts",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user