No Primeira parte desta série de artigosImplementamos o back-end com o AppWrite, instalamos algumas dependências e configuramos a licença para lidar com a autorização e controle de acesso baseado em funções.
Agora, vamos ver como podemos integrar o front -end ao back -end para um aplicativo EDTECH SAAS totalmente funcional.
Integração do front -end: implementando a autorização no próximo.js
Agora que você tem autorização de back -end usando a licença, integra -a ao seu próximo.js frontend. O front -end deve:
- Pegue permissões de usuário no back -end para controlar o que os usuários podem ver e fazer.
- Garanta que as solicitações da API respeitem o controle de acesso baseado em funções (RBAC).
- Ocultar elementos da interface do usuário para usuários não autorizados (por exemplo, impedem que os alunos vejam “Criar atribuição”).
1. Configurando chamadas de API com autorização
Como apenas o back -end aplica as permissões, seu front -end nunca decide o acesso diretamente – em vez de ele:
- Envia solicitações para o back -end
- Aguarda a resposta de autorização do back -end
- Exibe dados ou elementos da interface do usuário de acordo
Para começar, você precisará instalar o Node.js no seu computador.
Em seguida, siga estas etapas, siga as etapas abaixo:
npx create-next-app@latest frontend
cd frontend

2. Inicialize Shadcn
O que você observará após a criação do seu projeto NextJS é que Tailwind CSS v4 está instalado para você imediatamente, o que significa que você não precisa fazer mais nada. Como estamos usando uma biblioteca de componentes, vamos instalar Onion shadcn.
Para fazer isso, precisamos executar o comando init para criar um arquivo components.json na raiz da pasta:
Após a inicialização, você pode começar a adicionar componentes ao seu projeto:
npx shadcn@latest add button card dialog input label table select tabs
Se solicitado, se você deve usar a força devido à compatibilidade da versão NextJS 15 com Shadcn, pressione Enter para continuar.
3. Instale os pacotes necessários
Instale os seguintes pacotes:
npm i lucide-react zustand
npm i --save-dev axios
Agora que instalamos tudo o que precisamos para criar nosso aplicativo, podemos começar a criar nossos outros componentes e rotas.
Para manter a consistência da interface do usuário ao longo do aplicativo, cole este código no seu arquivo global.css (cole -o abaixo do seu trocador de importação):
@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;
}
body {
@apply bg-background text-foreground;
}
}
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
4. Arquivos de componentes
Crie os seguintes arquivos de componentes e cole seu código correspondente:
- Addassignmentdialog.tsx arquivo:
"use client"
import type React from "react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Assignment } from "@/types"
interface AddAssignmentDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onAddAssignment: (data: Assignment) => void
creatorEmail: string
}
export function 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, subject, teacher, className, dueDate, creatorEmail }
onAddAssignment(newAssignment)
console.log("New assignment:", { title, subject, class: className, dueDate, creatorEmail })
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Assignment</DialogTitle>
<DialogDescription>
Enter the details of the new assignment here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<form 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>
<Input id="title" value={title} onChange={(e) => setTitle(e.target.value)} className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="subject" className="text-right">
Subject
</Label>
<Input id="subject" value={subject} onChange={(e) => setSubject(e.target.value)} className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="teacher" className="text-right">
Teacher
</Label>
<Input id="teacher" value={teacher} onChange={(e) => setTeacher(e.target.value)} className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="class" className="text-right">
Class
</Label>
<Input
id="class"
value={className}
onChange={(e) => setClassName(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="dueDate" className="text-right">
Due Date
</Label>
<Input
id="dueDate"
type="date"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
Este arquivo define um componente React, AddassignmentDialog, que renderiza um formulário de diálogo para adicionar novas atribuições. Ele gerencia o estado do estado usando o uso de usestate e envia os dados de atribuição a um componente pai por meio do suporte do OnadDassignment. A caixa de diálogo inclui campos de entrada para título, assunto, professor, aula e data de vencimento e fecha mediante envio.
- AddStudentDialog.tsx arquivo:
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Student } from '@/types'
interface AddStudentDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onAddStudent: (data: Student) => void
loading: boolean
creatorEmail: string
}
export function 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: Number(age),
creatorEmail
})
console.log('New student:', { firstName, lastName, className, gender, age })
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Student</DialogTitle>
<DialogDescription>
Enter the details of the new student here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<form 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 Name
</Label>
<Input
id="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="lastName" className="text-right">
Last Name
</Label>
<Input
id="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="class" className="text-right">
Class
</Label>
<Input
id="class"
value={className}
onChange={(e) => setClassName(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="gender" className="text-right">
Gender
</Label>
<Select onValueChange={setGender} value={gender}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select gender" />
</SelectTrigger>
<SelectContent>
<SelectItem value="boy">Boy</SelectItem>
<SelectItem value="girl">Girl</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="age" className="text-right">
age
</Label>
<Input
id="age"
type="number"
step="0.1"
value={age}
min={"4"}
max={"99"}
placeholder='enter a valid age'
onChange={(e) => setAge(e.target.value)}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button disabled={loading} type="submit">{loading ? "Saving..." : "Save Changes"}</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
Este arquivo define um componente React, AddStudentDialog, que torna um formulário de diálogo para adicionar novos alunos. Ele gerencia o estado do estado usando o uso de usestate e envia os dados do aluno para um componente pai por meio do suporte OnaddStudent. A caixa de diálogo inclui campos de entrada para o primeiro nome, sobrenome, classe, gênero (com suspensão) e idade e lida com os estados de carregamento durante o envio.
- Atributmentstable.tsx arquivo:
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import type { AssignmentsTable } from "@/types"
export function AssignmentsTables({ assignments }: { assignments: AssignmentsTable() }) {
console.log("Assignments", assignments)
return (
<Table>
<TableCaption>A list of recent assignments.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Class</TableHead>
<TableHead>Teacher</TableHead>
<TableHead>Due Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{assignments.map((assignment) => (
<TableRow key={assignment.$id}>
<TableCell>{assignment.title}</TableCell>
<TableCell>{assignment.subject}</TableCell>
<TableCell>{assignment.className}</TableCell>
<TableCell>{assignment.teacher}</TableCell>
<TableCell>{assignment.dueDate}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
Este arquivo define um componente React, atribuintes, que renderiza uma tabela para exibir uma lista de atribuições. É preciso uma variedade de tarefas como adereços e mapas através delas para preencher as linhas de tabela com detalhes como título, assunto, classe, professor e data de vencimento. A tabela inclui uma legenda e cabeçalhos para uma melhor legibilidade.
import type React from "react"
interface AuthLayoutProps {
children: React.ReactNode
title: string
description?: string
}
export function AuthLayout({ children, 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>
{children}
</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">Keep Your Children's Success</h2>
<p className="text-lg text-gray-200">
Connect with teachers, track progress, and stay involved in your child's education journey.
</p>
</div>
</div>
</div>
</div>
)
}
Este arquivo define um componente React, authlayout, que fornece um layout para páginas de autenticação. Ele inclui um lado esquerdo para formas (com título e descrição opcional) e um lado direito com uma imagem de fundo e texto motivacional. O layout é responsivo, escondendo a imagem em telas menores.
import { Book, BarChart, MessageCircle } from "lucide-react"
const features = (
{
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 function Features() {
return (
<div className="py-12 bg-white" id="features">
<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">Features</h2>
<p className="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl">
Everything you need to stay connected
</p>
<p className="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto">
Our platform offers a range of features designed to enhance communication between school administrators and teachers.
</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">
{features.map((feature) => (
<div key={feature.name} className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary text-white">
<feature.icon className="h-6 w-6" aria-hidden="true" />
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">{feature.name}</p>
</dt>
<dd className="mt-2 ml-16 text-base text-gray-500">{feature.description}</dd>
</div>
))}
</dl>
</div>
</div>
</div>
)
}
Este arquivo define um componente React, recursos, que mostra os principais recursos da plataforma em um layout visualmente atraente. Inclui um título, descrição e uma grade de cartões, cada um com um ícone, nome e descrição detalhada. O componente foi projetado para destacar os recursos da plataforma para administradores e professores da escola.
Este arquivo define um componente React, rodapé, que exibe um rodapé simples com ícones de mídia social (Facebook e Twitter) e um aviso de direitos autorais. O rodapé é centralizado e receptivo, com links sociais à direita e o texto de direitos autorais à esquerda para telas maiores.
Este arquivo define um componente React, Hero, que cria uma seção de herói visualmente envolvente para um site. Inclui uma manchete em negrito, um parágrafo descritivo e dois botões de frase de chamariz (“Comece” e “Saiba mais”). O layout apresenta um design responsivo com uma forma de fundo e uma imagem no lado direito para telas maiores.
Este arquivo define um componente React, MobileMenu, que cria um menu de navegação móvel responsivo. Ele alterna a visibilidade com um botão e inclui links para os recursos, sobre seções de contato, além de botões de login e inscrição. O menu é estilizado com um design limpo e moderno e fecha ao clicar no ícone Fechar.
Este arquivo define um componente React, NavBar, que cria uma barra de navegação responsiva com links para recursos, sobre seções de contato. Inclui botões de login e inscrição para telas maiores e integra um componente mobilemenu para telas menores. A barra naval é estilizada com uma sombra e um layout centrado.
- Notauthorizeddialog.tsx arquivo:
Este arquivo define um componente React, NotauthorizedDialog, que exibe uma caixa de diálogo quando um usuário não está autorizado a executar uma ação. Inclui um título e uma descrição que levam ao usuário a entrar em contato com um administrador, e sua visibilidade é controlada através dos adereços abertos e on -openizadores.
Este arquivo define um componente React, StudentStables, que renderiza uma tabela para exibir uma lista de alunos. É preciso uma variedade de estudantes como adereços e mapas através deles para preencher as linhas de mesa com detalhes como primeiro nome, sobrenome, classe, sexo e idade. A tabela inclui uma legenda e cabeçalhos para uma melhor legibilidade.
Consulte o código GitHub para o respectivo código dos componentes mencionados acima.
Gerenciamento e tipos estaduais
Agora, para a próxima etapa, criaremos o estado e os tipos que usaremos em todo o aplicativo. Crie o loja e tYpes pastas na raiz da pasta do projeto.
- Dentro da pasta Store, crie os seguintes arquivos e cole o código correspondente:
import { create } from "zustand"
import { persist } from "zustand/middleware"
interface User {
$id: string
firstName: string
lastName: string
email: string
}
interface AuthState {
user: User | null
setUser: (user: User | null) => void
token: string | null;
setToken: (token: string | null) => void;
logout: () => void
}
export const useAuthStore = create()(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
token: null,
setToken: (token) => set({ token }),
logout: () => set({ user: null }),
}),
{
name: "auth-storage", // Persist state in localStorage
}
)
)
Este arquivo define uma loja Zustand, UseAuthStore, para gerenciar o estado de autenticação. Inclui estados de usuário e token, juntamente com os métodos para definir o usuário, definir o token e fazer logon. O estado é persistido em LocalSorage usando o middleware persistente.
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface Profile {
firstName: string;
lastName: string;
email: string;
role: string;
userId: string;
$id: string;
$createdAt: string;
}
interface ProfileStore {
profile: Profile | null;
setProfile: (profile: Profile) => void;
clearProfile: () => void;
}
export const useProfileStore = create<ProfileStore>()(
persist(
(set) => ({
profile: null,
setProfile: (profile) => set({ profile }),
clearProfile: () => set({ profile: null }),
}),
{
name: "profile-storage",
}
)
);
Este arquivo define uma loja Zustand, useProfilestore, para gerenciar dados do perfil do usuário. Inclui um estado de perfil e métodos para definir e limpar o perfil. O estado é persistido em LocalSorage usando o middleware persistente.
- Dentro da pasta Tipos, crie o seguinte arquivo e cole o seguinte código no index.ts arquivo:
export interface Assignment {
title: string;
subject: string;
className: string;
teacher: string;
dueDate: string;
creatorEmail: string;
}
export interface AssignmentsTable extends Assignment {
$id: string;
}
export interface Student {
firstName: string;
lastName: string;
gender: string;
className: string;
age: number;
creatorEmail: string;
}
export interface StudentsTable extends Student {
$id: string;
}
Este arquivo define as interfaces do TypeScript para atribuição, atribuições, estudante e estudante. Ele estende a atribuição básica e as interfaces dos alunos com propriedades adicionais como $ id para registros de banco de dados, garantindo uma digitação consistente em todo o aplicativo.
Rotas
Agora, podemos ver como os componentes e a loja que acabamos de criar estão sendo usados no aplicativo.
Substitua o código no arquivo app/página.tsx pelo código abaixo:
import { Navbar } from "@/components/Navbar"
import { Hero } from "@/components/Hero"
import { Features } from "@/components/Features"
import { Footer } from "@/components/Footer"
export default function Home() {
return (
<div className="min-h-screen flex flex-col">
<Navbar />
<main className="flex-grow">
<Hero />
<Features />
</main>
<Footer />
</div>
)
}
Este arquivo define o componente principal da página inicial, que estrutura o layout usando componentes de navbar, herói, recursos e rodapé. Ele garante um design responsivo com um layout flexível e altura de página inteira.
Crie as seguintes pastas na pasta do aplicativo e cole este código em seus respectivos arquivos Page.TSX:
- Crie uma pasta de inscrição e cole este código em seu arquivo Page.TSX:
"use client"
import { useState } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { AuthLayout } from "@/components/auth-layout"
import { useAuthStore } from "@/store/auth"
export default function SignupPage() {
const router = useRouter()
const { setUser, setToken } = useAuthStore()
const (isLoading, setIsLoading) = useState(false)
const (error, setError) = useState<string | null>(null)
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setIsLoading(true);
setError(null);
const formData = new FormData(e.currentTarget as HTMLFormElement);
const userData = {
name: `${formData.get("firstName")} ${formData.get("lastName")}`,
email: formData.get("email"),
password: formData.get("password"),
};
try {
const response = await fetch("https://edtech-saas-backend.vercel.app/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData),
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error("Signup failed. Please try again.");
}
console.log("Signup successful:", result);
const (firstName, ...lastNameParts) = result.user.name.split(" ");
const lastName = lastNameParts.join(" ") || "";
setUser({
$id: result.user.$id,
firstName,
lastName,
email: result.user.email,
});
setToken(result.token);
console.log("User:", result.user);
console.log("Token:", result.token)
router.push("/role-selection");
} catch (err) {
const error = err as Error;
setError(error.message || "An error occurred");
console.error("Error:", error);
} finally {
setIsLoading(false);
}
}
return (
<AuthLayout title="Create an account" description="Enter your details to get started">
<form onSubmit={onSubmit} className="space-y-4">
<div className="grid gap-4 grid-cols-2">
<div className="space-y-2">
<Label htmlFor="firstName">First name</Label>
<Input name="firstName" id="firstName" placeholder="John" disabled={isLoading} required />
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last name</Label>
<Input name="lastName" id="lastName" placeholder="Doe" disabled={isLoading} required />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input name="email" id="email" placeholder="name@example.com" type="email" autoComplete="email" disabled={isLoading} required />
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input name="password" id="password" type="password" disabled={isLoading} required />
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
<Button className="w-full" type="submit" disabled={isLoading}>
{isLoading ? "Creating account..." : "Create account"}
</Button>
</form>
<div className="text-center text-sm">
<Link href="/login" className="underline underline-offset-4 hover:text-primary">
Already have an account? Sign in
</Link>
</div>
</AuthLayout>
)
}
Este arquivo define um componente significativo para o registro do usuário, manuseio de envio do formulário com validação e manuseio de erros. Ele usa o Zustand para armazenar dados do usuário e um token após a inscrição bem -sucedida, depois redireciona para uma página de seleção de função. O formulário inclui campos para o primeiro nome, sobrenome, email e senha, com um link para a página de login para usuários existentes.
- Crie uma pasta de seleção de função e cole este código em seu arquivo Page.TSX:
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { GraduationCap, Users } from "lucide-react"
import { useAuthStore } from "@/store/auth"
import { useProfileStore } from "@/store/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 function RoleSelectionPage() {
const { user, token } = useAuthStore()
const { setProfile } = useProfileStore()
console.log("User:", user);
const router = useRouter()
const (selectedRole, setSelectedRole) = useState<string | null>(null)
console.log("Selected Role:", selectedRole);
const (isLoading, setIsLoading) = useState(false)
const (error, setError) = useState<string | null>(null)
async function onSubmit(e: React.FormEvent) {
e.preventDefault()
if (!selectedRole || !user) return
setIsLoading(true)
setError(null)
const formattedRole =
selectedRole.charAt(0).toUpperCase() + selectedRole.slice(1).toLowerCase();
const payload = {
firstName: user?.firstName,
lastName: user?.lastName,
email: user?.email,
role: formattedRole,
userId: user?.$id,
}
console.log("Payload", payload)
try {
const response = await fetch("https://edtech-saas-backend.vercel.app/api/profile", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || "Failed to create profile")
}
console.log("Profile Data", data)
setProfile({
firstName: data?.user?.firstName,
lastName: data?.user?.lastName,
email: data?.user?.email,
role: data?.user?.role,
userId: data?.user?.userId,
$id: data?.user?.$id,
$createdAt: data?.user?.$createdAt,
})
router.push("/dashboard")
} catch (err) {
const error = err as Error
setError(error.message)
console.error("Error:", error)
} finally {
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">Select your role</h1>
<p className="text-gray-500">Choose your role to access the appropriate dashboard</p>
</div>
{error && <p className="text-red-500 text-center">{error}</p>}
<form onSubmit={onSubmit} className="space-y-4">
<div className="grid gap-4">
{roles.map((role) => {
const Icon = role.icon
return (
<Card
key={role.id}
className={`cursor-pointer transition-colors ${selectedRole === role.id ? "border-black" : ""}`}
onClick={() => setSelectedRole(role.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">{role.title}</h3>
<p className="text-sm text-gray-500">{role.description}</p>
</div>
</CardContent>
</Card>
)
})}
</div>
<Button className="w-full" type="submit" disabled={!selectedRole || isLoading}>
{isLoading ? "Confirming..." : "Continue"}
</Button>
</form>
</div>
</div>
)
}
Este arquivo define um componente RolesElectionPage, onde os usuários selecionam sua função (administrador, professor ou aluno) após se inscrever. Ele lida com a seleção de função, envia os dados para criar um perfil e redireciona para o painel após o sucesso. A interface do usuário inclui cartões para cada função, um botão de confirmação e manuseio de erros.
- Crie uma pasta de login e cole este código em seu arquivo Page.TSX:
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AuthLayout } from "@/components/auth-layout";
import { useAuthStore } from "@/store/auth";
import { useProfileStore } from "@/store/profile";
export default function LoginPage() {
const router = useRouter();
const (isLoading, setIsLoading) = useState(false);
const { setUser, setToken } = useAuthStore()
const (formData, setFormData) = useState({ email: "", password: "" });
const (error, setError) = useState<string | null>(null)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, (e.target.name): e.target.value });
};
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setIsLoading(true);
setError(null);
console.log("FormData", formData);
try {
const authResponse = await fetch("https://edtech-saas-backend.vercel.app/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
if (!authResponse.ok) throw new Error("Invalid credentials");
const authData = await authResponse.json();
console.log("Auth Result:", authData);
const token = authData.token;
setToken(token);
setUser({
$id: authData.session.$id,
firstName: "",
lastName: "",
email: authData.session.providerUid,
});
const profileResponse = await fetch(`https://edtech-saas-backend.vercel.app/api/profile/${formData.email}`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
if (!profileResponse.ok) throw new Error("Failed to fetch user profile");
const profileData = await profileResponse.json();
console.log("Profile Data:", profileData);
if (profileData.profile) {
useProfileStore.getState().setProfile(profileData.profile);
router.push("/dashboard");
} else {
router.push("/role-selection");
}
} catch (err) {
const error = err as Error;
setError(error.message || "An error occurred");
} finally {
setIsLoading(false);
}
}
return (
<AuthLayout title="Welcome back" description="Enter your credentials to access your account">
<form onSubmit={onSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
placeholder="name@example.com"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading}
required
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
disabled={isLoading}
required
onChange={handleChange}
/>
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
<Button className="w-full" type="submit" disabled={isLoading}>
{isLoading ? "Signing in..." : "Sign in"}
</Button>
</form>
<div className="text-center text-sm">
<Link href="/signup" className="underline underline-offset-4 hover:text-primary">
Don't have an account? Sign up
</Link>
</div>
</AuthLayout>
);
}
Este arquivo define um componente de loginpage para autenticação do usuário, manipulando o envio do formulário com email e senha. Ele usa o Zustand para armazenar dados do usuário e um token, buscar o perfil do usuário e redirecionar para o painel ou a página de seleção de função com base no status do perfil. O formulário inclui manuseio de erros e um link para a página de inscrição para novos usuários.
- Crie uma pasta de painel e cole este código em seu arquivo página.tsx:
"use client";
import { useState, useEffect } from "react";
import { StudentsTables } from "@/components/StudentsTable";
import { Button } from "@/components/ui/button";
import { NotAuthorizedDialog } from "@/components/NotAuthorizedDialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useAuthStore } from "@/store/auth";
import { useProfileStore } from "@/store/profile";
import { AddStudentDialog } from "@/components/AddStudentDialog";
import { AddAssignmentDialog } from "@/components/AddAssignmentDialog";
import {Assignment, AssignmentsTable, Student, StudentsTable } from "@/types";
import { AssignmentsTables } from "@/components/AssignmentsTable";
import axios from "axios";
export default function 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/students";
const API_URL_ASSIGNMENTS = "https://edtech-saas-backend.vercel.app/api/assignments/create";
async function fetchData() {
setLoading(true);
setError("");
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
const email = profile?.email;
if (!email) {
setError("Email is required");
return;
}
const studentsUrl = `https://edtech-saas-backend.vercel.app/api/students/${email}`;
const assignmentsUrl = `https://edtech-saas-backend.vercel.app/api/assignments/${email}`;
try {
const studentsRes = await axios.get(studentsUrl, { headers });
console.log("Students Data:", studentsRes.data);
setStudents(studentsRes.data);
} catch (err) {
console.warn("Failed to fetch students data:", err);
setStudents(());
}
try {
const assignmentsRes = await axios.get(assignmentsUrl, { headers });
console.log("Assignments Data:", assignmentsRes.data);
setAssignments(assignmentsRes.data);
} catch (err) {
console.error("Error fetching assignments data:", err);
setError((err as Error).message);
} finally {
setLoading(false);
}
}
useEffect(() => {
if (!token) return;
fetchData();
}, (token));
const handleAddStudent = async (data: Omit<Student, 'creatorEmail'>) => {
setLoading(true);
setError("");
const payload = {
firstName: data.firstName,
lastName: data.lastName,
gender: data.gender,
className: data.className,
age: data.age,
creatorEmail: profile?.email,
};
console.log("Students payload:", payload);
try {
const response = await fetch(API_URL_STUDENTS, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const result = await response.json();
console.log("Student Result", result);
if (response.status === 403 && result.message === "Not authorized") {
setIsAddStudentDialogOpen(false);
setIsNotAuthorizedDialogOpen(true);
return;
}
if (!response.ok) throw new Error(result.message || "Failed to add student");
setStudents((prevStudents: Student()) => (...prevStudents, result));
setIsAddStudentDialogOpen(false);
await fetchData();
} catch (err) {
if ((err as Error & { code?: number }).code === 403 && (err as Error).message === "Not authorized") {
setIsAddStudentDialogOpen(false);
setIsNotAuthorizedDialogOpen(true);
return;
}
setError((err as Error).message);
console.error("Error:", err);
} finally {
setLoading(false);
}
};
const handleAddAssignment = async (data: Assignment) => {
setLoading(true);
setError("");
const payload = {
title: data.title,
subject: data.subject,
className: data.className,
teacher: data.teacher,
dueDate: data.dueDate,
creatorEmail: profile?.email,
};
try {
const response = await fetch(API_URL_ASSIGNMENTS, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const result = await response.json();
if (response.status === 403 && result.message === "Not authorized") {
setIsAddAssignmentDialogOpen(false);
setIsNotAuthorizedDialogOpen(true);
return;
}
if (!response.ok) throw new Error(result.message || "Failed to add assignment");
setAssignments((prevAssignments: Assignment()) => (...prevAssignments, result));
setIsAddAssignmentDialogOpen(false);
} catch (err) {
if ((err as Error & { code?: number }).code === 403 && (err as Error).message === "Not authorized") {
setIsAddAssignmentDialogOpen(false);
setIsNotAuthorizedDialogOpen(true);
return;
}
setError((err as Error).message);
console.error("Error:", err);
} finally {
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 are logged in as {profile?.role === "Admin" ? "an" : "a"} {profile?.role}.
</p>
</div>
<Button variant="default" onClick={handleLogout}>Log out</Button>
</div>
{profile?.role === 'Student'
? (
<div>
<AssignmentsTables assignments={assignments} />
</div>
)
: (
<Tabs defaultValue="students" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="students">Students</TabsTrigger>
<TabsTrigger value="assignments">Assignments</TabsTrigger>
</TabsList>
<TabsContent value="students">
<StudentsTables students={students} />
<Button onClick={() => setIsAddStudentDialogOpen(true)}>Add a Student</Button>
</TabsContent>
<TabsContent value="assignments">
<AssignmentsTables assignments={assignments} />
<Button onClick={() => setIsAddAssignmentDialogOpen(true)}>Add Assignment</Button>
</TabsContent>
</Tabs>
)}
{error && <p className="text-red-500 mt-4">{error}</p>}
<NotAuthorizedDialog open={isNotAuthorizedDialogOpen} onOpenChange={setIsNotAuthorizedDialogOpen} />
<AddStudentDialog creatorEmail={profile?.email || ""} loading={loading} open={isAddStudentDialogOpen} onOpenChange={setIsAddStudentDialogOpen} onAddStudent={handleAddStudent} />
<AddAssignmentDialog creatorEmail={profile?.email || ""} open={isAddAssignmentDialogOpen} onOpenChange={setIsAddAssignmentDialogOpen} onAddAssignment={handleAddAssignment} />
</div>
);
}
Este arquivo define um componente da ProfessorDashboard que exibe um painel para professores ou administradores, permitindo que eles gerenciem alunos e tarefas. Ele inclui guias para alternar entre alunos e tarefas, botões para adicionar novas entradas e lidar com erros de autorização. O componente busca e exibe dados com base na função do usuário, com uma opção de logout e manuseio de erros.
Depois de criar todos os arquivos e componentes acima e usá -los como eu mostrei, seu aplicativo deve funcionar quando você executar este comando abaixo:
O aplicativo estará disponível em http: // localhost: 3000/.

Teste o aplicativo agora criando uma escola, inscrevendo -se e fazendo login como administrador, professor ou aluno e executando algumas ações.

Construindo um aplicativo de SaaS de EDTech com vários inquilinos com o Next.js, AppWrite e Permitir Forneceu várias idéias sobre autorização, segurança e escalabilidade. Aqui estão os principais tumores:
- Controle de acesso simplificado baseado em funções (RBAC): com permissão, definir e fazer cumprir as funções de administrador, professor e aluno foi direto. Em vez de permissões de codificação, eu poderia gerenciá -las dinamicamente através da interface do usuário da licença.
- As políticas com reconhecimento de inquilinos da Permit garantiram que as escolas (inquilinos) permanecessem isolados um do outro. Isso foi importante para a segurança dos dados em um aplicativo SaaS de vários inquilinos.
- Em vez de escrever e gerenciar a lógica de permissão personalizada em dezenas de rotas de API, o Controle de Acesso de Permissão tratado de uma maneira centralizada para reduzir a complexidade e facilitar as atualizações futuras.
- Como todas as verificações de autorização foram aplicadas no back -end, o front -end exibiu apenas elementos da interface do usuário com base em permissões, garantindo uma experiência suave do usuário.
- A implementação de autenticação personalizada do zero poderia levar semanas. Mas, usando o AppWrite para autenticação e permissão para autorização, pude me concentrar na criação de recursos principais em vez de reinventar o controle de acesso.
Conclusão
A integração da licença com o Next.js & AppWrite me permitiu simplificar a autorização no meu aplicativo de saas EDTech com vários inquilinos. Ao descarregar a lógica de permissão complexa para permitir, pude me concentrar nos recursos de criação, não gerenciando o controle de acesso manualmente.
Se você estiver criando um aplicativo SaaS com permissões complexas e multi-cinema, a licença é uma ótima ferramenta para otimizar seu fluxo de trabalho.
Acesse o repositório do GitHub do projeto acabado para o back -end aqui e o front -end aqui.