Roller og innlogging m/Supabase 🔐
Lær hvordan du lager et profesjonelt innloggingssystem med automatiske roller (admin/student).
Sentrale konsepter
Autentisering
Supabase Auth
Håndterer brukere, passord og sikker pålogging.
Automatisering
SQL Triggere
En funksjon i databasen som kjører "bak kulissene" ved registrering.
Tilgangsstyring
Roller
Skiller mellom vanlige brukere og administratorer.
Trinn 1 — Supabase: Tabeller & SQL-trigger
- 1.1Opprett prosjekt på supabase.com.
- 1.2Finn Project URL og
Publishable key (
sb_publishable_...) under Settings -> API Keys. - 1.3Kjør SQL-en under i Supabase SQL Editor.
-- Tabell for brukerprofiler (knyttet til auth.users)
create table if not exists public.profiles (
id uuid primary key references auth.users on delete cascade,
first_name text,
class text,
role text not null default 'student',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
is_blocked boolean not null default true,
email text
);
-- Slå på RLS (rad-nivå-sikkerhet)
alter table public.profiles enable row level security;
-- Gi bare nødvendig Data API-tilgang.
-- Anonyme brukere skal ikke ha tilgang til profiles.
-- Profiler opprettes kun av triggeren handle_new_user().
revoke all on table public.profiles from anon;
revoke all on table public.profiles from authenticated;
grant select, update on table public.profiles to authenticated;
-- Hjelpefunksjon: sjekk om innlogget bruker er admin
create or replace function public.is_admin()
returns boolean
language sql
security definer
set search_path = ''
stable
as $$
select exists (
select 1
from public.profiles p
where p.id = auth.uid()
and p.role = 'admin'
);
$$;
-- Trigger: opprett profil når en bruker registrerer seg.
-- Nye elever starter blokkert til lærer/admin godkjenner.
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
insert into public.profiles (
id,
first_name,
class,
email,
role,
is_blocked
)
values (
new.id,
new.raw_user_meta_data ->> 'display_name',
new.raw_user_meta_data ->> 'class',
lower(new.email),
'student',
true
)
on conflict (id) do nothing;
return new;
end;
$$;
drop trigger if exists on_auth_user_created on auth.users;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();
-- Rydd gamle policyer hvis de finnes
drop policy if exists "read-own-profile" on public.profiles;
drop policy if exists "admin-read-all-profiles" on public.profiles;
drop policy if exists profiles_insert_self on public.profiles;
drop policy if exists profiles_select_self_or_admin on public.profiles;
drop policy if exists profiles_update_admin on public.profiles;
drop policy if exists profiles_update_self on public.profiles;
-- Bruker kan lese egen profil. Admin kan lese alle.
create policy profiles_select_self_or_admin
on public.profiles
for select
to authenticated
using ((id = auth.uid()) or public.is_admin());
-- Bare admin kan oppdatere profiler, f.eks. godkjenne elever.
create policy profiles_update_admin
on public.profiles
for update
to authenticated
using (public.is_admin())
with check (true);
Personvern og RLS: Profiler kan inneholde personopplysninger som e-post, navn, klasse og
rolle. Derfor skal tilgang styres i databasen med RLS, ikke bare i JavaScript. Bruk testbrukere i
oppgaven, og ikke lagre sensitive opplysninger uten tydelig lovlig grunnlag.
Trinn 2 — index.html: Struktur
Lag index.html. Denne inneholder selve siden og det skjulte panelet.
index.html, style.css og script.js skal ligge i samme mappe.
NB: Kjør siden fra en lokal HTTP-server (f.eks. Live Server). Supabase-auth og modul-importer
fungerer ikke alltid når siden åpnes uten en HTTP-origin.
Dersom du ønsker at Supabase skal sende brukeren tilbake etter innlogging (redirect), legg til
din lokale adresse med port i Supabase (Settings → Auth) som en redirect-URL, for eksempel
http://localhost:5500.
<!doctype html>
<html lang="no">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Innlogging med Supabase</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<!-- NAVBAR -->
<nav class="navbar navbar-expand-md navbar-dark bg-dark border-bottom" aria-label="Hovednavigasjon">
<div class="container d-flex justify-content-between align-items-center">
<a class="navbar-brand fw-semibold text-white" href="index.html">learn-it.no</a>
<div>
<button id="authOpenBtn" class="btn btn-sm btn-outline-light">Logg inn</button>
<span id="authUserBadge" class="ms-2 small text-white-50"></span>
</div>
</div>
</nav>
<!-- HERO -->
<header class="py-5 bg-light border-bottom">
<div class="container">
<h1 class="h3 fw-bold">Supabase + HTML + JS: enkel innlogging</h1>
<p class="text-muted mb-0">Innlogging, registrering og roller (admin/student) uten rammeverk.</p>
</div>
</header>
<main class="container py-4">
<h2 class="h5">Velkommen</h2>
<p class="text-muted">Bruk knappen <em>Logg inn</em> oppe til høyre for å teste løsningen.</p>
</main>
<!-- INNLOGGINGSPANEL (skjult) -->
<aside id="authPanel" class="auth-panel" aria-hidden="true">
<header class="p-3 border-bottom d-flex justify-content-between align-items-center">
<strong>Min profil</strong>
<button id="authCloseBtn" class="btn btn-sm btn-outline-secondary">Lukk</button>
</header>
<div class="p-3">
<div id="authLoggedOut">
<div class="d-flex gap-2 mb-3">
<button class="btn btn-sm btn-outline-dark active" data-tab="login">Logg inn</button>
<button class="btn btn-sm btn-outline-dark" data-tab="signup">Opprett profil</button>
</div>
<form id="loginForm" data-panel="login">
<input class="form-control mb-2" type="email" id="loginEmail" placeholder="E-post" required />
<input class="form-control mb-3" type="password" id="loginPass" placeholder="Passord" required />
<button class="btn btn-dark w-100 mb-2" type="submit">Logg inn</button>
<div id="loginMsg" class="text-muted small"></div>
</form>
<form id="signupForm" data-panel="signup" style="display:none;">
<input class="form-control mb-2" type="text" id="signupName" placeholder="Brukernavn (valgfritt)" />
<input class="form-control mb-2" type="text" id="signupClass" placeholder="Klasse (f.eks. 1STA)" />
<input class="form-control mb-2" type="email" id="signupEmail" placeholder="E-post" required />
<input class="form-control mb-3" type="password" id="signupPass" placeholder="Passord (min. 6 tegn)" required />
<button class="btn btn-dark w-100 mb-2" type="submit">Opprett profil</button>
<div id="signupMsg" class="text-muted small mt-2"></div>
</form>
</div>
<div id="authLoggedIn" style="display:none;">
<p class="small text-muted mb-1" id="whoami"></p>
<div id="roleBadge" class="mb-2 small"></div>
<button id="logoutBtn" class="btn btn-outline-dark w-100">Logg ut</button>
</div>
</div>
</aside>
<script type="module" src="script.js"></script>
</body>
</html>
Trinn 3 — style.css: Design
/* Base */
html, body { height:100%; }
body { font-family: system-ui, sans-serif; background:#f7f9fc; color:#111; margin:0; }
/* Auth-panel */
.auth-panel{
position:fixed; top:0; right:0; height:100vh; width:340px; max-width:90vw;
background:#fff; border-left:1px solid #e9ecef; box-shadow:-10px 0 24px rgba(0,0,0,.08);
transform:translateX(100%); transition:transform .25s ease; z-index:1200;
}
.auth-panel.open{ transform:translateX(0); }
.card-hover{ transition:box-shadow .2s ease }
.card-hover:hover{ box-shadow:0 .5rem 1rem rgba(0,0,0,.08) }
Trinn 4 — script.js: Logikken
Husk: Bytt ut URL og API-nøkkel med dine egne.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
/** 1) Koble til Supabase (BYTT til dine nøkler) */
const supabase = createClient(
"https://DIN-PROSJEKT-URL.supabase.co",
"DIN-PUBLISHABLE-KEY"
);
const panel = document.getElementById('authPanel');
const openBtn = document.getElementById('authOpenBtn');
const closeBtn = document.getElementById('authCloseBtn');
const userBadge = document.getElementById('authUserBadge');
const loggedOut = document.getElementById('authLoggedOut');
const loggedIn = document.getElementById('authLoggedIn');
const whoami = document.getElementById('whoami');
const roleBadge = document.getElementById('roleBadge');
const tabButtons = document.querySelectorAll('[data-tab]');
const loginForm = document.getElementById('loginForm');
const signupForm = document.getElementById('signupForm');
const loginMsg = document.getElementById('loginMsg');
const signupMsg = document.getElementById('signupMsg');
function openPanel() {
panel.classList.add('open');
panel.setAttribute('aria-hidden', 'false');
}
function closePanel() {
panel.classList.remove('open');
panel.setAttribute('aria-hidden', 'true');
}
openBtn.addEventListener('click', openPanel);
closeBtn.addEventListener('click', closePanel);
tabButtons.forEach(btn=>{
btn.addEventListener('click', ()=>{
tabButtons.forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
const tab = btn.dataset.tab;
loginForm.style.display = tab === 'login' ? '' : 'none';
signupForm.style.display = tab === 'signup' ? '' : 'none';
});
});
async function refreshAuthUI() {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
loggedOut.style.display = '';
loggedIn.style.display = 'none';
userBadge.textContent = '';
whoami.textContent = '';
roleBadge.textContent = '';
return;
}
const { data: profile } = await supabase
.from('profiles')
.select('role, is_blocked')
.eq('id', user.id)
.maybeSingle();
const role = profile?.role ?? 'student';
const blockedText = profile?.is_blocked ? ' (venter på godkjenning)' : '';
loggedOut.style.display = 'none';
loggedIn.style.display = '';
whoami.textContent = `Innlogget som ${user.email}`;
roleBadge.textContent = `Rolle: ${role}${blockedText}`;
userBadge.textContent = (role === 'admin') ? 'Admin' : 'Innlogget';
}
loginForm.addEventListener('submit', async (e)=>{
e.preventDefault();
loginMsg.textContent = 'Logger inn...';
const email = document.getElementById('loginEmail').value.trim();
const pass = document.getElementById('loginPass').value;
const { error } = await supabase.auth.signInWithPassword({ email, password: pass });
loginMsg.textContent = error ? 'Feil: ' + error.message : 'Innlogget ✅';
if (!error) setTimeout(closePanel, 400);
});
signupForm.addEventListener('submit', async (e)=>{
e.preventDefault();
signupMsg.textContent = 'Oppretter...';
const name = document.getElementById('signupName').value.trim();
const className = document.getElementById('signupClass').value.trim();
const email = document.getElementById('signupEmail').value.trim().toLowerCase();
const pass = document.getElementById('signupPass').value;
const { error } = await supabase.auth.signUp({
email,
password: pass,
options: { data: { display_name: name, class: className } }
});
signupMsg.textContent = error
? 'Feil: ' + error.message
: 'Konto opprettet ✅ Sjekk e-posten din for bekreftelse.';
});
document.getElementById('logoutBtn').addEventListener('click', async ()=>{
await supabase.auth.signOut();
});
supabase.auth.onAuthStateChange(() => refreshAuthUI());
refreshAuthUI();
Test & feilsøking
- "permission denied": Husk å kjøre SQL-scriptet i Trinn 1.
- "Failed to fetch": Sjekk URL og API-nøkkel i script.js.
- Finner ikke profil: Sjekk Table Editor i Supabase for å se om raden ble laget.