finally building

This commit is contained in:
Om Lanke
2025-07-02 12:05:38 +05:30
parent ba6ee585dc
commit 449629ece2
40 changed files with 2253 additions and 3711 deletions

View 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>
);
}

View File

@@ -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';

View 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',
},
];

View 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>
);
}

View 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>
);
}

View File

@@ -1,2 +1,2 @@
import { handlers } from '@workspace/auth';
import { handlers } from '@/auth';
export const { GET, POST } = handlers;

View 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
View 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);

View File

@@ -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> }) {

View File

@@ -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,

View File

@@ -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).*)'],
};

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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"
}
}
}

View File

@@ -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"
]
}