Within the first a part of this text sequence, we applied the backend with Appwrite, put in some dependencies, and arrange Allow to deal with authorization and role-based entry management.
Now let’s take a look at how we will combine the frontend with the backend for a completely practical EdTech SaaS software.
Frontend Integration: Implementing Authorization in Subsequent.js
Now that you’ve backend authorization in place utilizing Allow, combine it into your Subsequent.js frontend. The frontend ought to:
- Fetch consumer permissions from the backend to regulate what customers can see and do.
- Guarantee API requests respect role-based entry management (RBAC).
- Cover UI components for unauthorized customers (e.g., stop college students from seeing “Create Task”).
1. Establishing API calls with authorization
Since solely the backend enforces permissions, your frontend by no means decides entry straight—as an alternative, it:
- Sends requests to the backend
- Waits for the backend’s authorization response
- Shows information or UI components accordingly
To get began, you’ll have to have Node.js put in in your pc.
Then, comply with these steps, comply with the steps under:
npx create-next-app@newest frontend
cd frontend

2. Initialize shadcn
What you’ll observe after the creation of your Nextjs mission is that Tailwind CSS v4 is put in for you proper out of the field, which suggests you don’t have to do anything. As a result of we’re making use of a element library, we’re going to set up Shadcn UI.
To try this we have to run the init command to create a parts.json file within the root of the folder:
After initialization, you can begin including parts to your mission:
npx shadcn@newest add button card dialog enter label desk choose tabs
If requested, in case you ought to use drive due to the Nextjs 15 model compatibility with shadcn, hit enter to proceed.
3. Set up wanted packages
Set up the next packages:
npm i lucide-react zustand
npm i --save-dev axios
Now that we’ve put in all we have to construct our software, we will begin creating our different parts and routes.
To keep up UI consistency all through the appliance, paste this code into your world.css file (paste it under your tailwindcss import):
@layer base {
:root {
--background: 75 29% 95%;
--foreground: 0 0% 9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 9%;
--popover: 0 0% 99%;
--popover-foreground: 0 0% 9%;
--primary: 0 0% 0%;
--primary-foreground: 60 100% 100%;
--secondary: 75 31% 95%;
--secondary-foreground: 0 0% 9%;
--muted: 69 30% 95%;
--muted-foreground: 0 0% 45%;
--accent: 252 29% 97%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 189 0% 45%;
--input: 155 0% 45%;
--ring: 0 0% 0%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
physique {
@apply bg-background text-foreground;
}
}
physique {
font-family: Arial, Helvetica, sans-serif;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
physique {
@apply bg-background text-foreground;
}
}
4. Part information
Create the next element information and paste their corresponding code:
- AddAssignmentDialog.tsx file:
"use consumer"
import sort React from "react"
import { useState } from "react"
import { Button } from "@/parts/ui/button"
import { Enter } from "@/parts/ui/enter"
import { Label } from "@/parts/ui/label"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/parts/ui/dialog"
import { Task } from "@/sorts"
interface AddAssignmentDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onAddAssignment: (information: Task) => void
creatorEmail: string
}
export perform AddAssignmentDialog({ open, onOpenChange, onAddAssignment, creatorEmail }: AddAssignmentDialogProps) {
const [title, setTitle] = useState("")
const [subject, setSubject] = useState("")
const [teacher, setTeacher] = useState("")
const [className, setClassName] = useState("")
const [dueDate, setDueDate] = useState("")
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const newAssignment = { title, topic, instructor, className, dueDate, creatorEmail }
onAddAssignment(newAssignment)
console.log("New task:", { title, topic, class: className, dueDate, creatorEmail })
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Task</DialogTitle>
<DialogDescription>
Enter the main points of the new task right here. Click on save whenever you're achieved.
</DialogDescription>
</DialogHeader>
<type onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">
Title
</Label>
<Enter id="title" worth={title} onChange={(e) => setTitle(e.goal.worth)} className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="topic" className="text-right">
Topic
</Label>
<Enter id="topic" worth={topic} onChange={(e) => setSubject(e.goal.worth)} className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="instructor" className="text-right">
Trainer
</Label>
<Enter id="instructor" worth={instructor} onChange={(e) => setTeacher(e.goal.worth)} className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="class" className="text-right">
Class
</Label>
<Enter
id="class"
worth={className}
onChange={(e) => setClassName(e.goal.worth)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="dueDate" className="text-right">
Due Date
</Label>
<Enter
id="dueDate"
sort="date"
worth={dueDate}
onChange={(e) => setDueDate(e.goal.worth)}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button sort="submit">Save adjustments</Button>
</DialogFooter>
</type>
</DialogContent>
</Dialog>
)
}
This file defines a React element, AddAssignmentDialog, which renders a dialog type for including new assignments. It manages type state utilizing useState and submits the task information to a father or mother element by way of the onAddAssignment prop. The dialog contains enter fields for title, topic, instructor, class, and due date, and closes upon submission.
- AddStudentDialog.tsx file:
'use consumer'
import { useState } from 'react'
import { Button } from '@/parts/ui/button'
import { Enter } from '@/parts/ui/enter'
import { Label } from '@/parts/ui/label'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/parts/ui/dialog'
import {
Choose,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/parts/ui/choose"
import { Scholar } from '@/sorts'
interface AddStudentDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onAddStudent: (information: Scholar) => void
loading: boolean
creatorEmail: string
}
export perform AddStudentDialog({ open, onOpenChange, onAddStudent, loading, creatorEmail }: AddStudentDialogProps) {
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [className, setClassName] = useState('')
const [gender, setGender] = useState('')
const [age, setAge] = useState("")
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onAddStudent({
firstName,
lastName,
className,
gender,
age: Quantity(age),
creatorEmail
})
console.log('New pupil:', { firstName, lastName, className, gender, age })
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Scholar</DialogTitle>
<DialogDescription>
Enter the main points of the new pupil right here. Click on save whenever you're achieved.
</DialogDescription>
</DialogHeader>
<type onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="firstName" className="text-right">
First Title
</Label>
<Enter
id="firstName"
worth={firstName}
onChange={(e) => setFirstName(e.goal.worth)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="lastName" className="text-right">
Final Title
</Label>
<Enter
id="lastName"
worth={lastName}
onChange={(e) => setLastName(e.goal.worth)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="class" className="text-right">
Class
</Label>
<Enter
id="class"
worth={className}
onChange={(e) => setClassName(e.goal.worth)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="gender" className="text-right">
Gender
</Label>
<Choose onValueChange={setGender} worth={gender}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Choose gender" />
</SelectTrigger>
<SelectContent>
<SelectItem worth="boy">Boy</SelectItem>
<SelectItem worth="woman">Lady</SelectItem>
</SelectContent>
</Choose>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="age" className="text-right">
age
</Label>
<Enter
id="age"
sort="quantity"
step="0.1"
worth={age}
min={"4"}
max={"99"}
placeholder='enter a legitimate age'
onChange={(e) => setAge(e.goal.worth)}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button disabled={loading} sort="submit">{loading ? "Saving..." : "Save Adjustments"}</Button>
</DialogFooter>
</type>
</DialogContent>
</Dialog>
)
}
This file defines a React element, AddStudentDialog, which renders a dialog type for including new college students. It manages type state utilizing useState and submits the scholar information to a father or mother element by way of the onAddStudent prop. The dialog contains enter fields for first title, final title, class, gender (with a dropdown), and age, and handles loading states throughout submission.
- AssignmentsTable.tsx file:
import { Desk, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/parts/ui/desk"
import sort { AssignmentsTable } from "@/sorts"
export perform AssignmentsTables({ assignments }: { assignments: AssignmentsTable[] }) {
console.log("Assignments", assignments)
return (
<Desk>
<TableCaption>A listing of current assignments.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Topic</TableHead>
<TableHead>Class</TableHead>
<TableHead>Trainer</TableHead>
<TableHead>Due Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{assignments.map((task) => (
<TableRow key={task.$id}>
<TableCell>{task.title}</TableCell>
<TableCell>{task.topic}</TableCell>
<TableCell>{task.className}</TableCell>
<TableCell>{task.instructor}</TableCell>
<TableCell>{task.dueDate}</TableCell>
</TableRow>
))}
</TableBody>
</Desk>
)
}
This file defines a React element, AssignmentsTables, which renders a desk to show an inventory of assignments. It takes an array of assignments as props and maps by means of them to populate the desk rows with particulars like title, topic, class, instructor, and due date. The desk features a caption and headers for higher readability.
import sort React from "react"
interface AuthLayoutProps {
kids: React.ReactNode
title: string
description?: string
}
export perform AuthLayout({ kids, title, description }: AuthLayoutProps) {
return (
<div className="min-h-screen grid lg:grid-cols-2">
{}
<div className="flex items-center justify-center p-8">
<div className="mx-auto w-full max-w-sm space-y-6">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
{kids}
</div>
</div>
{}
<div className="hidden lg:block relative bg-black">
<div className="absolute inset-0 bg-[url('https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-xOOAKcDxPyvxlDygdNGtUvjEA6QHBO.png')] bg-cover bg-center opacity-50" />
<div className="relative h-full flex items-center justify-center text-white p-12">
<div className="space-y-6 max-w-lg">
<h2 className="text-4xl font-bold">Preserve Your Kids's Success</h2>
<p className="text-lg text-gray-200">
Join with academics, observe progress, and keep concerned in your kid's schooling journey.
</p>
</div>
</div>
</div>
</div>
)
}
This file defines a React element, AuthLayout, which gives a structure for authentication pages. It features a left aspect for types (with a title and non-obligatory description) and a proper aspect with a background picture and motivational textual content. The structure is responsive, hiding the picture on smaller screens.
import { E-book, BarChart, MessageCircle } from "lucide-react"
const options = [
{
name: "Comprehensive Dashboard",
description: "View student's overall academic performance, including average grades and progress over time.",
icon: BarChart,
},
{
name: "Easy Communication",
description: "Direct messaging system between school administrators and teachers for quick and efficient communication.",
icon: MessageCircle,
},
{
name: "Academic Tracking",
description:
"Monitor assignments, upcoming tests, and project deadlines to help your students stay on top of their studies.",
icon: Book,
},
]
export perform Options() {
return (
<div className="py-12 bg-white" id="options">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="lg:text-center">
<h2 className="text-base text-primary font-semibold tracking-wide uppercase">Options</h2>
<p className="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl">
All the pieces it's essential to keep related
</p>
<p className="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto">
Our platform gives a spread of options designed to reinforce communication between college directors and academics.
</p>
</div>
<div className="mt-10">
<dl className="space-y-10 md:space-y-0 md:grid md:grid-cols-3 md:gap-x-8 md:gap-y-10">
{options.map((characteristic) => (
<div key={characteristic.title} className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary text-white">
<characteristic.icon className="h-6 w-6" aria-hidden="true" />
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">{characteristic.title}</p>
</dt>
<dd className="mt-2 ml-16 text-base text-gray-500">{characteristic.description}</dd>
</div>
))}
</dl>
</div>
</div>
</div>
)
}
This file defines a React element, Options, which showcases key platform options in a visually interesting structure. It features a title, description, and a grid of characteristic playing cards, every with an icon, title, and detailed description. The element is designed to spotlight the platform’s capabilities for varsity directors and academics.
This file defines a React element, Footer, which shows a easy footer with social media icons (Fb and Twitter) and a copyright discover. The footer is centered and responsive, with social hyperlinks on the appropriate and the copyright textual content on the left for bigger screens.
This file defines a React element, Hero, which creates a visually participating hero part for an internet site. It features a daring headline, a descriptive paragraph, and two call-to-action buttons (“Get began” and “Study extra”). The structure includes a responsive design with a background form and a picture on the appropriate aspect for bigger screens.
This file defines a React element, MobileMenu, which creates a responsive cell navigation menu. It toggles visibility with a button and contains hyperlinks to options, about, and speak to sections, in addition to login and sign-up buttons. The menu is styled with a clear, fashionable design and closes when clicking the shut icon.
This file defines a React element, Navbar, which creates a responsive navigation bar with hyperlinks to options, about, and speak to sections. It contains login and sign-up buttons for bigger screens and integrates a MobileMenu element for smaller screens. The navbar is styled with a shadow and a centered structure.
- NotAuthorizedDialog.tsx file:
This file defines a React element, NotAuthorizedDialog, which shows a dialog when a consumer is just not approved to carry out an motion. It features a title and outline prompting the consumer to contact an administrator, and its visibility is managed by way of the open and onOpenChange props.
This file defines a React element, StudentsTables, which renders a desk to show an inventory of scholars. It takes an array of scholars as props and maps by means of them to populate the desk rows with particulars like first title, final title, class, gender, and age. The desk features a caption and headers for higher readability.
Confer with the GitHub code for the respective code of the parts talked about above.
State administration and kinds
Now for the following step, we’ll be creating the state and kinds we’ll be utilizing all through the appliance. Create the retailer and types folders within the root of the mission folder.
- Inside the shop folder, create the next information and paste the corresponding code:
import { create } from "zustand"
import { persist } from "zustand/middleware"
interface Consumer {
$id: string
firstName: string
lastName: string
e mail: string
}
interface AuthState null;
setToken: (token: string
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
consumer: null,
setUser: (consumer) => set({ consumer }),
token: null,
setToken: (token) => set({ token }),
logout: () => set({ consumer: null }),
}),
{
title: "auth-storage", // Persist state in localStorage
}
)
)
This file defines a Zustand retailer, useAuthStore, for managing authentication state. It contains consumer and token states, together with strategies to set the consumer, set the token, and log off. The state is endured in localStorage utilizing the persist middleware.
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface Profile {
firstName: string;
lastName: string;
e mail: string;
position: string;
userId: string;
$id: string;
$createdAt: string;
}
interface ProfileStore null;
setProfile: (profile: Profile) => void;
clearProfile: () => void;
export const useProfileStore = create<ProfileStore>()(
persist(
(set) => ({
profile: null,
setProfile: (profile) => set({ profile }),
clearProfile: () => set({ profile: null }),
}),
{
title: "profile-storage",
}
)
);
This file defines a Zustand retailer, useProfileStore, for managing consumer profile information. It features a profile state and strategies to set and clear the profile. The state is endured in localStorage utilizing the persist middleware.
- Inside the kinds folder, create the next file and paste the next code within the index.ts file:
export interface Task {
title: string;
topic: string;
className: string;
instructor: string;
dueDate: string;
creatorEmail: string;
}
export interface AssignmentsTable extends Task {
$id: string;
}
export interface Scholar {
firstName: string;
lastName: string;
gender: string;
className: string;
age: quantity;
creatorEmail: string;
}
export interface StudentsTable extends Scholar {
$id: string;
}
This file defines TypeScript interfaces for Task, AssignmentsTable, Scholar, and StudentsTable. It extends the bottom Task and Scholar interfaces with further properties like $id for database data, making certain constant typing throughout the appliance.
Routes
Now we get to see how the parts and retailer we simply created are getting used within the software.
Exchange the code within the app/web page.tsx file with the code under:
import { Navbar } from "@/parts/Navbar"
import { Hero } from "@/parts/Hero"
import { Options } from "@/parts/Options"
import { Footer } from "@/parts/Footer"
export default perform Residence() {
return (
<div className="min-h-screen flex flex-col">
<Navbar />
<foremost className="flex-grow">
<Hero />
<Options />
</foremost>
<Footer />
</div>
)
}
This file defines the principle residence web page element, which constructions the structure utilizing Navbar, Hero, Options, and Footer parts. It ensures a responsive design with a flex structure and full-page top.
Create the next folders within the app folder and paste this code of their respective web page.tsx information:
- Create a signup folder and paste this code in its web page.tsx file:
"use consumer"
import { useState } from "react"
import Hyperlink from "subsequent/hyperlink"
import { useRouter } from "subsequent/navigation"
import { Button } from "@/parts/ui/button"
import { Enter } from "@/parts/ui/enter"
import { Label } from "@/parts/ui/label"
import { AuthLayout } from "@/parts/auth-layout"
import { useAuthStore } from "@/retailer/auth"
export default perform SignupPage() {
const router = useRouter()
const { setUser, setToken } = useAuthStore()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
async perform onSubmit(e: React.FormEvent) {
e.preventDefault();
setIsLoading(true);
setError(null);
const formData = new FormData(e.currentTarget as HTMLFormElement);
const userData = {
title: `${formData.get("firstName")} ${formData.get("lastName")}`,
e mail: formData.get("e mail"),
password: formData.get("password"),
};
strive {
const response = await fetch("https://edtech-saas-backend.vercel.app/api/auth/signup", {
methodology: "POST",
headers: { "Content material-Kind": "software/json" },
physique: JSON.stringify(userData),
});
const consequence = await response.json();
if (!response.okay || !consequence.success) {
throw new Error("Signup failed. Please strive once more.");
}
console.log("Signup profitable:", consequence);
const [firstName, ...lastNameParts] = consequence.consumer.title.cut up(" ");
const lastName = lastNameParts.be part of(" ") || "";
setUser({
$id: consequence.consumer.$id,
firstName,
lastName,
e mail: consequence.consumer.e mail,
});
setToken(consequence.token);
console.log("Consumer:", consequence.consumer);
console.log("Token:", consequence.token)
router.push("/role-selection");
} catch (err) lastly {
setIsLoading(false);
}
}
return (
<AuthLayout title="Create an account" description="Enter your particulars to get began">
<type onSubmit={onSubmit} className="space-y-4">
<div className="grid gap-4 grid-cols-2">
<div className="space-y-2">
<Label htmlFor="firstName">First title</Label>
<Enter title="firstName" id="firstName" placeholder="John" disabled={isLoading} required />
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Final title</Label>
<Enter title="lastName" id="lastName" placeholder="Doe" disabled={isLoading} required />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="e mail">Electronic mail</Label>
<Enter title="e mail" id="e mail" placeholder="title@instance.com" sort="e mail" autoComplete="e mail" disabled={isLoading} required />
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Enter title="password" id="password" sort="password" disabled={isLoading} required />
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
<Button className="w-full" sort="submit" disabled={isLoading}>
{isLoading ? "Creating account..." : "Create account"}
</Button>
</type>
<div className="text-center text-sm">
<Hyperlink href="/login" className="underline underline-offset-4 hover:text-primary">
Have already got an account? Signal in
</Hyperlink>
</div>
</AuthLayout>
)
}
This file defines a SignupPage element for consumer registration, dealing with type submission with validation and error dealing with. It makes use of Zustand to retailer consumer information and a token upon profitable signup, then redirects to a job choice web page. The shape contains fields for first title, final title, e mail, and password, with a hyperlink to the login web page for current customers.
- Create a role-selection folder and paste this code in its web page.tsx file:
"use consumer"
import { useState } from "react"
import { useRouter } from "subsequent/navigation"
import { Button } from "@/parts/ui/button"
import { Card, CardContent } from "@/parts/ui/card"
import { GraduationCap, Customers } from "lucide-react"
import { useAuthStore } from "@/retailer/auth"
import { useProfileStore } from "@/retailer/profile"
const roles = [
{
id: "Admin",
title: "Admin",
description: "Manage teachers, classes, and more",
icon: GraduationCap,
},
{
id: "Teacher",
title: "Teacher",
description: "Access your class dashboard, manage grades, and communicate with students",
icon: GraduationCap,
},
{
id: "Student",
title: "Student",
description: "Monitor your progress and communicate with teachers",
icon: Users,
},
]
export default perform RoleSelectionPage() {
const { consumer, token } = useAuthStore()
const { setProfile } = useProfileStore()
console.log("Consumer:", consumer);
const router = useRouter()
const [selectedRole, setSelectedRole] = useState<string | null>(null)
console.log("Chosen Function:", selectedRole);
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
async perform onSubmit(e: React.FormEvent) {
e.preventDefault()
if (!selectedRole || !consumer) return
setIsLoading(true)
setError(null)
const formattedRole =
selectedRole.charAt(0).toUpperCase() + selectedRole.slice(1).toLowerCase();
const payload = {
firstName: consumer?.firstName,
lastName: consumer?.lastName,
e mail: consumer?.e mail,
position: formattedRole,
userId: consumer?.$id,
}
console.log("Payload", payload)
strive {
const response = await fetch("https://edtech-saas-backend.vercel.app/api/profile", {
methodology: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content material-Kind": "software/json"
},
physique: JSON.stringify(payload),
})
const information = await response.json()
if (!response.okay)
console.log("Profile Information", information)
setProfile({
firstName: information?.consumer?.firstName,
lastName: information?.consumer?.lastName,
e mail: information?.consumer?.e mail,
position: information?.consumer?.position,
userId: information?.consumer?.userId,
$id: information?.consumer?.$id,
$createdAt: information?.consumer?.$createdAt,
})
router.push("/dashboard")
} catch (err) {
const error = err as Error
setError(error.message)
console.error("Error:", error)
} lastly {
setIsLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
<div className="max-w-md w-full space-y-8">
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold">Choose your position</h1>
<p className="text-gray-500">Select your position to entry the suitable dashboard</p>
</div>
{error && <p className="text-red-500 text-center">{error}</p>}
<type onSubmit={onSubmit} className="space-y-4">
<div className="grid gap-4">
{roles.map((position) => {
const Icon = position.icon
return (
<Card
key={position.id}
className={`cursor-pointer transition-colors ${selectedRole === position.id ? "border-black" : ""}`}
onClick={() => setSelectedRole(position.title)}
>
<CardContent className="flex items-start gap-4 p-6">
<div className="rounded-full p-2 bg-gray-100">
<Icon className="h-6 w-6" />
</div>
<div className="space-y-1">
<h3 className="font-medium">{position.title}</h3>
<p className="text-sm text-gray-500">{position.description}</p>
</div>
</CardContent>
</Card>
)
})}
</div>
<Button className="w-full" sort="submit" disabled=>
{isLoading ? "Confirming..." : "Proceed"}
</Button>
</type>
</div>
</div>
)
}
This file defines a RoleSelectionPage element the place customers choose their position (Admin, Trainer, or Scholar) after signing up. It handles position choice, submits the information to create a profile, and redirects to the dashboard upon success. The UI contains playing cards for every position, a affirmation button, and error dealing with.
- Create a login folder and paste this code in its web page.tsx file:
"use consumer";
import { useState } from "react";
import Hyperlink from "subsequent/hyperlink";
import { useRouter } from "subsequent/navigation";
import { Button } from "@/parts/ui/button";
import { Enter } from "@/parts/ui/enter";
import { Label } from "@/parts/ui/label";
import { AuthLayout } from "@/parts/auth-layout";
import { useAuthStore } from "@/retailer/auth";
import { useProfileStore } from "@/retailer/profile";
export default perform LoginPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const { setUser, setToken } = useAuthStore()
const [formData, setFormData] = useState({ e mail: "", password: "" });
const [error, setError] = useState<string | null>(null)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.goal.worth });
};
async perform onSubmit(e: React.FormEvent) {
e.preventDefault();
setIsLoading(true);
setError(null);
console.log("FormData", formData);
strive {
const authResponse = await fetch("https://edtech-saas-backend.vercel.app/api/auth/login", {
methodology: "POST",
headers: {
"Content material-Kind": "software/json",
},
physique: JSON.stringify(formData),
});
if (!authResponse.okay) throw new Error("Invalid credentials");
const authData = await authResponse.json();
console.log("Auth Outcome:", authData);
const token = authData.token;
setToken(token);
setUser({
$id: authData.session.$id,
firstName: "",
lastName: "",
e mail: authData.session.providerUid,
});
const profileResponse = await fetch(`https://edtech-saas-backend.vercel.app/api/profile/${formData.e mail}`, {
methodology: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content material-Kind": "software/json",
},
});
if (!profileResponse.okay) throw new Error("Did not fetch consumer profile");
const profileData = await profileResponse.json();
console.log("Profile Information:", profileData);
if (profileData.profile) {
useProfileStore.getState().setProfile(profileData.profile);
router.push("/dashboard");
} else {
router.push("/role-selection");
}
} catch (err) "An error occurred");
lastly {
setIsLoading(false);
}
}
return (
<AuthLayout title="Welcome again" description="Enter your credentials to entry your account">
<type onSubmit={onSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="e mail">Electronic mail</Label>
<Enter
id="e mail"
title="e mail"
placeholder="title@instance.com"
sort="e mail"
autoCapitalize="none"
autoComplete="e mail"
autoCorrect="off"
disabled={isLoading}
required
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Enter
id="password"
title="password"
sort="password"
disabled={isLoading}
required
onChange={handleChange}
/>
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
<Button className="w-full" sort="submit" disabled={isLoading}>
{isLoading ? "Signing in..." : "Check in"}
</Button>
</type>
<div className="text-center text-sm">
<Hyperlink href="/signup" className="underline underline-offset-4 hover:text-primary">
Do not have an account? Join
</Hyperlink>
</div>
</AuthLayout>
);
}
This file defines a LoginPage element for consumer authentication, dealing with type submission with e mail and password. It makes use of Zustand to retailer consumer information and a token, fetches the consumer’s profile, and redirects to the dashboard or position choice web page primarily based on the profile standing. The shape contains error dealing with and a hyperlink to the signup web page for brand spanking new customers.
- Create a dashboard folder and paste this code in its web page.tsx file:
"use consumer";
import { useState, useEffect } from "react";
import { StudentsTables } from "@/parts/StudentsTable";
import { Button } from "@/parts/ui/button";
import { NotAuthorizedDialog } from "@/parts/NotAuthorizedDialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/parts/ui/tabs";
import { useAuthStore } from "@/retailer/auth";
import { useProfileStore } from "@/retailer/profile";
import { AddStudentDialog } from "@/parts/AddStudentDialog";
import { AddAssignmentDialog } from "@/parts/AddAssignmentDialog";
import {Task, AssignmentsTable, Scholar, StudentsTable } from "@/sorts";
import { AssignmentsTables } from "@/parts/AssignmentsTable";
import axios from "axios";
export default perform TeacherDashboard() {
const { token, logout } = useAuthStore();
const { profile, clearProfile } = useProfileStore();
const [isNotAuthorizedDialogOpen, setIsNotAuthorizedDialogOpen] = useState(false);
const [isAddStudentDialogOpen, setIsAddStudentDialogOpen] = useState(false);
const [isAddAssignmentDialogOpen, setIsAddAssignmentDialogOpen] = useState(false);
const [students, setStudents] = useState<StudentsTable[]>([]);
const [assignments, setAssignments] = useState<AssignmentsTable[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const API_URL_STUDENTS = "https://edtech-saas-backend.vercel.app/api/college students";
const API_URL_ASSIGNMENTS = "https://edtech-saas-backend.vercel.app/api/assignments/create";
async perform fetchData() {
setLoading(true);
setError("");
const headers = {
"Content material-Kind": "software/json",
Authorization: `Bearer ${token}`,
};
const e mail = profile?.e mail;
if (!e mail) {
setError("Electronic mail is required");
return;
}
const studentsUrl = `https://edtech-saas-backend.vercel.app/api/college students/${e mail}`;
const assignmentsUrl = `https://edtech-saas-backend.vercel.app/api/assignments/${e mail}`;
strive {
const studentsRes = await axios.get(studentsUrl, { headers });
console.log("College students Information:", studentsRes.information);
setStudents(studentsRes.information);
} catch (err) {
console.warn("Did not fetch college students information:", err);
setStudents([]);
}
strive {
const assignmentsRes = await axios.get(assignmentsUrl, { headers });
console.log("Assignments Information:", assignmentsRes.information);
setAssignments(assignmentsRes.information);
} catch (err) {
console.error("Error fetching assignments information:", err);
setError((err as Error).message);
} lastly {
setLoading(false);
}
}
useEffect(() => {
if (!token) return;
fetchData();
}, [token]);
const handleAddStudent = async (information: Omit<Scholar, 'creatorEmail'>) => {
setLoading(true);
setError("");
const payload = {
firstName: information.firstName,
lastName: information.lastName,
gender: information.gender,
className: information.className,
age: information.age,
creatorEmail: profile?.e mail,
};
console.log("College students payload:", payload);
strive {
const response = await fetch(API_URL_STUDENTS, {
methodology: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content material-Kind": "software/json",
},
physique: JSON.stringify(payload),
});
const consequence = await response.json();
console.log("Scholar Outcome", consequence);
if (response.standing === 403 && consequence.message === "Not approved") {
setIsAddStudentDialogOpen(false);
setIsNotAuthorizedDialogOpen(true);
return;
}
if (!response.okay) throw new Error(consequence.message || "Failed so as to add pupil");
setStudents((prevStudents: Scholar[]) => [...prevStudents, result]);
setIsAddStudentDialogOpen(false);
await fetchData();
} catch (err) {
if ((err as Error & { code?: quantity }).code === 403 && (err as Error).message === "Not approved") {
setIsAddStudentDialogOpen(false);
setIsNotAuthorizedDialogOpen(true);
return;
}
setError((err as Error).message);
console.error("Error:", err);
} lastly {
setLoading(false);
}
};
const handleAddAssignment = async (information: Task) => {
setLoading(true);
setError("");
const payload = {
title: information.title,
topic: information.topic,
className: information.className,
instructor: information.instructor,
dueDate: information.dueDate,
creatorEmail: profile?.e mail,
};
strive {
const response = await fetch(API_URL_ASSIGNMENTS, {
methodology: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content material-Kind": "software/json",
},
physique: JSON.stringify(payload),
});
const consequence = await response.json();
if (response.standing === 403 && consequence.message === "Not approved") {
setIsAddAssignmentDialogOpen(false);
setIsNotAuthorizedDialogOpen(true);
return;
}
if (!response.okay) throw new Error(consequence.message || "Failed so as to add task");
setAssignments((prevAssignments: Task[]) => [...prevAssignments, result]);
setIsAddAssignmentDialogOpen(false);
} catch (err) {
if ((err as Error & { code?: quantity }).code === 403 && (err as Error).message === "Not approved") {
setIsAddAssignmentDialogOpen(false);
setIsNotAuthorizedDialogOpen(true);
return;
}
setError((err as Error).message);
console.error("Error:", err);
} lastly {
setLoading(false);
}
};
const handleLogout = () => {
clearProfile();
logout();
window.location.href = "/login";
};
return (
<div className="container mx-auto p-4">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-2xl font-bold mb-2">Welcome {profile?.firstName}</h1>
<p className="text-gray-600 mb-6">
You might be logged in as {profile?.position === "Admin" ? "an" : "a"} {profile?.position}.
</p>
</div>
<Button variant="default" onClick={handleLogout}>Sign off</Button>
</div>
{profile?.position === 'Scholar'
? (
<div>
<AssignmentsTables assignments={assignments} />
</div>
)
: (
<Tabs defaultValue="college students" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger worth="college students">College students</TabsTrigger>
<TabsTrigger worth="assignments">Assignments</TabsTrigger>
</TabsList>
<TabsContent worth="college students">
<StudentsTables college students={college students} />
<Button onClick={() => setIsAddStudentDialogOpen(true)}>Add a Scholar</Button>
</TabsContent>
<TabsContent worth="assignments">
<AssignmentsTables assignments={assignments} />
<Button onClick={() => setIsAddAssignmentDialogOpen(true)}>Add Task</Button>
</TabsContent>
</Tabs>
)}
{error && <p className="text-red-500 mt-4">{error}</p>}
<NotAuthorizedDialog open={isNotAuthorizedDialogOpen} onOpenChange={setIsNotAuthorizedDialogOpen} />
<AddStudentDialog creatorEmail= "" loading={loading} open={isAddStudentDialogOpen} onOpenChange={setIsAddStudentDialogOpen} onAddStudent={handleAddStudent} />
<AddAssignmentDialog creatorEmail= "" open={isAddAssignmentDialogOpen} onOpenChange={setIsAddAssignmentDialogOpen} onAddAssignment={handleAddAssignment} />
</div>
);
}
This file defines a TeacherDashboard element that shows a dashboard for academics or admins, permitting them to handle college students and assignments. It contains tabs for switching between college students and assignments, buttons so as to add new entries, and handles authorization errors. The element fetches and shows information primarily based on the consumer’s position, with a logout choice and error dealing with.
After creating all of the information and parts above and utilizing them as I’ve proven you, your software ought to work whenever you run this command under:
The app might be out there at http://localhost:3000/.

Check out the appliance now by creating a faculty, signing up and logging in as an admin, instructor or pupil, and performing some actions.

Constructing a multi-tenant EdTech SaaS software with Subsequent.js, Appwrite, and Allow supplied a number of insights into authorization, safety, and scalability. Listed here are the important thing takeaways:
- Simplified Function-Based mostly Entry Management (RBAC): With Allow, defining and implementing admin, instructor, and pupil roles was easy. As an alternative of hardcoding permissions, I may dynamically handle them by way of the Allow UI.
- Allow’s tenant-aware insurance policies ensured that faculties (tenants) remained remoted from each other. This was essential for information safety in a multi-tenant SaaS app.
- As an alternative of writing and managing customized permission logic throughout dozens of API routes, Allow dealt with entry management in a centralized technique to scale back complexity and make future updates simpler.
- Since all authorization checks had been enforced on the backend, the frontend solely displayed UI components primarily based on permissions, making certain a clean consumer expertise.
- Implementing customized authentication from scratch may have taken weeks. However utilizing Appwrite for authentication and Allow for authorization, I used to be in a position to give attention to constructing core options as an alternative of reinventing entry management.
Conclusion
Integrating Allow with Subsequent.js & Appwrite enabled me to simplify authorization in my multi-tenant Edtech SaaS software. By offloading complicated permission logic to Allow, I used to be in a position to give attention to constructing options, not managing entry management manually.
Should you’re constructing a SaaS app with complicated permissions & multi-tenancy, Allow is a superb instrument to make use of to streamline your workflow.
Entry the GitHub repo of the completed mission for the backend right here and the frontend right here.