333 lines
8.8 KiB
Markdown
333 lines
8.8 KiB
Markdown
# Les 18 — Huiswerk
|
||
## Multi-user Tasks app met RLS + social login + AUTH.md
|
||
|
||
**Vak:** AI-Assisted Development
|
||
**Deadline:** Voor eindopdracht-inlevering (4 weken)
|
||
**Inleveren:** GitHub repo + `AUTH.md` in root + Vercel productie URL
|
||
|
||
---
|
||
|
||
## Doel
|
||
|
||
Bouw je auth-demo uit naar productie-niveau: tasks per user met RLS, GitHub social login, en deploy. Eindopdracht-ready.
|
||
|
||
---
|
||
|
||
## Onderdeel A — Tasks tabel + RLS (verplicht)
|
||
|
||
### A1 — Schema
|
||
|
||
In Supabase SQL Editor:
|
||
|
||
```sql
|
||
create table tasks (
|
||
id bigserial primary key,
|
||
user_id uuid not null references auth.users(id) default auth.uid(),
|
||
text text not null,
|
||
done boolean default false,
|
||
created_at timestamp default now()
|
||
);
|
||
|
||
alter table tasks enable row level security;
|
||
|
||
create policy "select own tasks" on tasks
|
||
for select using (auth.uid() = user_id);
|
||
|
||
create policy "insert own tasks" on tasks
|
||
for insert with check (auth.uid() = user_id);
|
||
|
||
create policy "update own tasks" on tasks
|
||
for update using (auth.uid() = user_id);
|
||
|
||
create policy "delete own tasks" on tasks
|
||
for delete using (auth.uid() = user_id);
|
||
```
|
||
|
||
### A2 — Tasks UI
|
||
|
||
`app/page.tsx` — extend met tasks lijst + form:
|
||
|
||
```tsx
|
||
import { createClient } from "@/utils/supabase/server";
|
||
import { redirect } from "next/navigation";
|
||
import { revalidatePath } from "next/cache";
|
||
|
||
async function addTask(formData: FormData) {
|
||
"use server";
|
||
const supabase = await createClient();
|
||
await supabase.from("tasks").insert({ text: formData.get("text") as string });
|
||
revalidatePath("/");
|
||
}
|
||
|
||
async function toggleTask(id: number, done: boolean) {
|
||
"use server";
|
||
const supabase = await createClient();
|
||
await supabase.from("tasks").update({ done: !done }).eq("id", id);
|
||
revalidatePath("/");
|
||
}
|
||
|
||
async function deleteTask(id: number) {
|
||
"use server";
|
||
const supabase = await createClient();
|
||
await supabase.from("tasks").delete().eq("id", id);
|
||
revalidatePath("/");
|
||
}
|
||
|
||
export default async function Home() {
|
||
const supabase = await createClient();
|
||
const { data: { user } } = await supabase.auth.getUser();
|
||
if (!user) redirect("/login");
|
||
|
||
const { data: tasks } = await supabase
|
||
.from("tasks")
|
||
.select("*")
|
||
.order("created_at", { ascending: false });
|
||
|
||
return (
|
||
<main className="p-8 max-w-2xl mx-auto">
|
||
<h1 className="text-2xl mb-4">Tasks van {user.email}</h1>
|
||
|
||
<form action={addTask} className="flex gap-2 mb-6">
|
||
<input name="text" className="border p-2 flex-1" placeholder="Nieuwe taak..." />
|
||
<button className="bg-blue-600 text-white px-4 py-2 rounded">+</button>
|
||
</form>
|
||
|
||
<ul className="space-y-2">
|
||
{tasks?.map((t) => (
|
||
<li key={t.id} className="flex items-center gap-2 p-2 border rounded">
|
||
<form action={toggleTask.bind(null, t.id, t.done)}>
|
||
<button className={t.done ? "line-through" : ""}>{t.text}</button>
|
||
</form>
|
||
<form action={deleteTask.bind(null, t.id)} className="ml-auto">
|
||
<button className="text-red-500">×</button>
|
||
</form>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</main>
|
||
);
|
||
}
|
||
```
|
||
|
||
### Eisen
|
||
|
||
- [ ] `tasks` tabel met RLS aan
|
||
- [ ] 4 policies: select / insert / update / delete
|
||
- [ ] UI: add, toggle, delete tasks werkt
|
||
- [ ] In incognito met ander account: andere tasks zichtbaar (geen vermenging)
|
||
|
||
---
|
||
|
||
## Onderdeel B — Social login (GitHub) (verplicht)
|
||
|
||
### B1 — GitHub OAuth App
|
||
|
||
1. github.com → Settings → Developer settings → OAuth Apps → New
|
||
2. Application name: jouw app
|
||
3. Homepage URL: `http://localhost:3000` (dev) of `https://yourapp.vercel.app` (prod)
|
||
4. Callback URL: `https://your-project.supabase.co/auth/v1/callback`
|
||
5. Save, generate Client Secret
|
||
|
||
### B2 — Configure in Supabase
|
||
|
||
1. Dashboard → Auth → Providers → GitHub
|
||
2. Enable + plak Client ID + Secret
|
||
|
||
### B3 — Button toevoegen
|
||
|
||
In `app/login/page.tsx`:
|
||
|
||
```tsx
|
||
async function signInWithGitHub() {
|
||
const supabase = createClient();
|
||
await supabase.auth.signInWithOAuth({
|
||
provider: "github",
|
||
options: { redirectTo: `${location.origin}/auth/callback` },
|
||
});
|
||
}
|
||
|
||
// In render:
|
||
<button onClick={signInWithGitHub} className="border px-4 py-2 mt-3 rounded">
|
||
Of: Login met GitHub
|
||
</button>
|
||
```
|
||
|
||
### Eisen
|
||
|
||
- [ ] GitHub OAuth App aangemaakt
|
||
- [ ] Supabase configured
|
||
- [ ] Klik op GitHub button → redirect → ingelogd
|
||
- [ ] Tasks werken met GitHub-account
|
||
|
||
---
|
||
|
||
## Onderdeel C — Deploy naar Vercel (verplicht)
|
||
|
||
Combineert Les 14/15:
|
||
|
||
1. Push naar GitHub
|
||
2. Vercel: import repo
|
||
3. Add env vars: `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
||
4. Deploy
|
||
5. Update Supabase Auth → URL Configuration:
|
||
- **Site URL:** `https://yourapp.vercel.app`
|
||
- **Redirect URLs:** `https://yourapp.vercel.app/**`, `http://localhost:3000/**`
|
||
6. Update GitHub OAuth App callback URL to productie
|
||
|
||
### Eisen
|
||
|
||
- [ ] App live op Vercel
|
||
- [ ] Magic link werkt op productie URL
|
||
- [ ] GitHub login werkt op productie URL
|
||
- [ ] Tasks per user werken op productie
|
||
|
||
---
|
||
|
||
## Onderdeel D — `AUTH.md` documentatie (verplicht)
|
||
|
||
In repo-root.
|
||
|
||
### Sectie 1 — Auth methoden
|
||
|
||
Welke auth methoden ondersteund:
|
||
- Magic link (default)
|
||
- GitHub OAuth
|
||
- (eventuele extra's)
|
||
|
||
### Sectie 2 — RLS policies
|
||
|
||
Lijst je policies + waarom:
|
||
|
||
```markdown
|
||
**Table: tasks**
|
||
- `select own tasks` — users zien alleen eigen tasks
|
||
- `insert own tasks` — voorkomt inserten op andermans naam
|
||
- `update own tasks` — alleen eigen toggle
|
||
- `delete own tasks` — alleen eigen delete
|
||
```
|
||
|
||
### Sectie 3 — Test van multi-user isolation
|
||
|
||
Bewijs dat RLS werkt:
|
||
- Screenshot van Account A's tasks
|
||
- Screenshot van Account B's tasks (incognito)
|
||
- Bewijs: account A's tasks staan NIET in account B's lijst
|
||
|
||
### Sectie 4 — Deployment checklist
|
||
|
||
Wat je moest aanpassen voor productie:
|
||
- Site URL in Supabase
|
||
- Redirect URLs in Supabase
|
||
- GitHub OAuth callback URL
|
||
- Env vars in Vercel
|
||
|
||
### Sectie 5 — Eindopdracht-architectuur (~250 woorden)
|
||
|
||
Concrete plan voor je eigen eindopdracht:
|
||
- App naam + doel
|
||
- User-flows (signup, login, kernfeature)
|
||
- Welke tabellen + RLS policies
|
||
- AI feature(s) en hoe ze user-context gebruiken
|
||
- Externe APIs (uit Les 17)
|
||
- Tijd-plan (4-6 weken)
|
||
|
||
### Vorm
|
||
|
||
- Max 800 woorden
|
||
- Screenshots zijn waardevol
|
||
- Concrete plan, geen vaagheid
|
||
|
||
---
|
||
|
||
## Bonus (optioneel)
|
||
|
||
### Bonus 1 — Email + password naast magic link
|
||
|
||
Voeg klassieke signup/login toe. Combine met magic link.
|
||
|
||
### Bonus 2 — Profile pagina
|
||
|
||
`/profile` — user kan naam, avatar URL, etc. instellen. Aparte `profiles` tabel met RLS.
|
||
|
||
### Bonus 3 — Team feature
|
||
|
||
Multi-user binnen team. Aparte tabel `teams`, junction `team_members`. RLS policies die team-leden toelaten elkaars data te zien.
|
||
|
||
### Bonus 4 — AI integration met user context
|
||
|
||
Voeg een AI chat toe waar de user-tasks deel zijn van de context. AI weet wat jouw tasks zijn (door RLS), kan helpen prioriteren.
|
||
|
||
---
|
||
|
||
## Inleveren
|
||
|
||
1. **GitHub repo URL** in Brightspace
|
||
2. **Vercel productie URL** in Brightspace
|
||
3. **`AUTH.md`** in repo-root
|
||
4. **Test-account credentials** voor docent (email of GitHub)
|
||
5. **2-3 screenshots** van multi-user test
|
||
|
||
---
|
||
|
||
## Beoordeling
|
||
|
||
| Criterium | Punten |
|
||
|-----------|--------|
|
||
| A — Tasks + RLS werkt (4 policies) | 3 |
|
||
| B — GitHub OAuth werkt | 2 |
|
||
| C — Deployed op Vercel + auth werkt op prod | 2 |
|
||
| D — AUTH.md compleet (5 secties) | 2 |
|
||
| Multi-user isolation bewezen | 1 |
|
||
| **Totaal** | **10** |
|
||
|
||
Voldoende = 6+. Bonus telt mee.
|
||
|
||
---
|
||
|
||
## Tijd-indicatie
|
||
|
||
| Onderdeel | Tijd |
|
||
|-----------|------|
|
||
| A — Tasks + RLS | 50 min |
|
||
| B — GitHub OAuth | 25 min |
|
||
| C — Deploy | 20 min |
|
||
| D — AUTH.md | 35 min |
|
||
| **Totaal** | **~2 uur 10 min** |
|
||
|
||
---
|
||
|
||
## Veelvoorkomende valkuilen
|
||
|
||
| Probleem | Oplossing |
|
||
|----------|-----------|
|
||
| Tasks van andere users zichtbaar | RLS niet aan, of policy `using (true)` |
|
||
| Insert faalt met "row violates" | RLS check faalt — check `with check (auth.uid() = user_id)` + default |
|
||
| OAuth redirect 404 | Site URL niet bijgewerkt voor productie |
|
||
| Magic link werkt lokaal niet prod | Site URL in Supabase staat nog op localhost |
|
||
| Cookies werken niet | Middleware draait niet — check matcher en path |
|
||
| `auth.uid()` returnt null | User niet ingelogd — check session in Server Component |
|
||
|
||
---
|
||
|
||
## Tips
|
||
|
||
- **Test isolation in incognito** — alleen daar ben je écht "another user"
|
||
- **RLS error messages** kunnen vaag zijn — log policy + auth.uid() in Postgres function voor debug
|
||
- **Productie env vars** — vergeet niet je Vercel env vars te zetten
|
||
- **Eindopdracht-koppeling:** dit huiswerk is je startpunt — bouw eindopdracht hier op
|
||
- **Veel succes met de eindopdracht!**
|
||
|
||
---
|
||
|
||
## Voor je begint aan de eindopdracht
|
||
|
||
Als afsluiting:
|
||
|
||
1. Plan je eindopdracht concreet (Sectie 5 van AUTH.md is de basis)
|
||
2. Start een nieuwe repo van scratch óf bouw verder op deze
|
||
3. Push klein en vaak — niet één big commit aan eind
|
||
4. Vraag hulp tijdig — Slack, mail, office hours
|
||
5. Plan tijd in van min 8-10 uur/week voor 4-6 weken
|
||
|
||
**Bedankt voor jullie inzet deze 18 lessen. Veel succes met de eindopdracht!**
|