← Tilbake

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. 1.1Opprett prosjekt på supabase.com.
  2. 1.2Finn Project URL og Publishable key (sb_publishable_...) under Settings -> API Keys.
  3. 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