← Tilbake til oversikten

Roller og innlogging m/Supabase + HTML + JavaScript

I denne guiden lager vi et enkelt innloggingssystem i ren HTML/CSS/JavaScript som snakker med Supabase.

Du setter inn din egen e-post som admin i SQL-koden. Alle andre som registrerer seg blir automatisk user.

Trinn 1 — Supabase: prosjektnøkler, tabeller & regler


  1. 1.1Opprett (eller åpne) et prosjekt på supabase.com.
  2. 1.2Finn Project URL og anon public key i Project Settings → "API Keys" og Project Settings → "Data API". Disse bruker vi i script.js.
  3. 1.3Åpne SQL Editor i supabase og kjør skriptet under for å opprette profiles, slå på RLS (Row-level security: for å skjule passord)
  4. 1.4Sett din e-post som admin: Bytt ut teksten skriv inn din e-post her med din faktiske e-post før du kjører.
-- Tabell for brukerprofiler (knyttet til auth.users)
create table if not exists profiles (
  id uuid primary key references auth.users on delete cascade,
  role text not null default 'user',  -- standardrolle er "user"
  created_at timestamptz default now()
);

-- Slå på RLS (rad-nivå-sikkerhet)
alter table profiles enable row level security;

-- Policy: Bruker kan bare lese sin egen profil
drop policy if exists "read-own-profile" on profiles;
create policy "read-own-profile"
on profiles for select using (id = auth.uid());

-- Trigger: opprett profil og gi admin-rolle til DIN e-post
create or replace function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, role)
  values (
    new.id,
    case when lower(new.email) = lower('skriv inn din e-post her') then 'admin' else 'user' end
  );
  return new;
end;
$$ language plpgsql security definer;

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();

Trinn 2 — index.html: navbar, hero og innloggingspanel

Lag index.html. Vi beholder enkel, ryddig navigasjon og legger til en «Logg inn»-knapp + sidepanel (for innlogging/opprett bruker).

<!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-black 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">Læringsportalen</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/user) 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">
      <!-- Ikke innlogget -->
      <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>

        <!-- Logg inn -->
        <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>

        <!-- Opprett bruker -->
        <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="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 class="text-muted small">Admin settes automatisk til e-posten du oppga i SQL-koden.</div>
          <div id="signupMsg" class="text-muted small mt-2"></div>
        </form>
      </div>

      <!-- Innlogget -->
      <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>

Hvorfor panel? Elevene holder fokus på siden og slipper å navigere bort. Panelet fungerer på mobil og desktop.

Trinn 3 — style.css: enkel, videreførbar stil

Lag style.css (du kan starte med lite – bygg videre senere):

/* Base */
html, body { height:100%; }
body { font-family: system-ui,-apple-system,"Segoe UI",Roboto,Arial,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); }

/* Små hjelpere (optional) */
.card-hover{ transition:box-shadow .2s ease }
.card-hover:hover{ box-shadow:0 .5rem 1rem rgba(0,0,0,.08) }

Har du allerede en stilfil? Legg bare til panel-klassene.

Trinn 4 — script.js: koble HTML ←→ Supabase

Lag script.js og bytt ut dine egne API-nøkler (rød markering under). Finn nøkler i Project Settings → API.

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",   // ← Project URL
  "DIN-ANON-PUBLIC-KEY"                      // ← anon public key
);

/** 2) Finn elementer */
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');

/** 3) Åpne/lukk panel */
openBtn.addEventListener('click', ()=> panel.classList.add('open'));
closeBtn.addEventListener('click', ()=> panel.classList.remove('open'));

/** 4) Bytt mellom "Logg inn" og "Opprett profil" */
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';
  });
});

/** 5) Oppdater UI (bruker + rolle) */
async function refreshAuthUI() {
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    loggedOut.style.display = '';
    loggedIn.style.display  = 'none';
    userBadge.textContent   = '';
    return;
  }

  // Les rolle fra "profiles" (RLS: bruker kan bare lese sin egen rad)
  const { data: profile } = await supabase
    .from('profiles')
    .select('role')
    .eq('id', user.id)
    .maybeSingle();

  const role = profile?.role ?? 'user';
  loggedOut.style.display = 'none';
  loggedIn.style.display  = '';
  whoami.textContent = \`Innlogget som \${user.email}\`;
  roleBadge.textContent = \`Rolle: \${role}\`;
  userBadge.textContent = (role === 'admin') ? 'Admin' : 'Innlogget';
}

/** 6) Logg inn */
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 ✅';
  await refreshAuthUI();
  if (!error) setTimeout(()=>panel.classList.remove('open'), 400);
});

/** 7) Opprett bruker */
signupForm.addEventListener('submit', async (e)=>{
  e.preventDefault();
  signupMsg.textContent = 'Oppretter...';
  const name  = document.getElementById('signupName').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 } }
  });

  signupMsg.textContent = error
    ? 'Feil: ' + error.message
    : 'Konto opprettet ✅ Du kan logge inn nå.';
});

/** 8) Logg ut */
document.getElementById('logoutBtn').addEventListener('click', async ()=>{
  await supabase.auth.signOut();
  await refreshAuthUI();
});

/** 9) Hold UI i sync */
supabase.auth.onAuthStateChange(() => refreshAuthUI());
refreshAuthUI();
Forklaring: All inn/ut/registrering går via supabase.auth.*. Rollen leses fra tabellen profiles (RLS sikrer at du bare kan lese din egen rad). Triggeren fra Trinn 1 gir admin til din e-post, og user til alle andre.

Test & feilsøking

  • AÅpne index.html med Live Server (VS Code) eller last opp til GitHub Pages.
  • BKlikk Logg innOpprett profil → registrer e-post og passord → logg inn.
  • CSjekk i Supabase → Table Editor → profiles at rollen er admin for din e-post, ellers user.

  • “permission denied for table profiles” → Kjør SQL-skriptet (Trinn 1) på nytt, spesielt policyen.
  • “Failed to fetch” → Sjekk at Project URL og anon key er korrekte (se rød markering i script.js).
  • Ingenting skjer → Åpne nettleser-konsollen (F12) og se feilmelding. Sjekk at script.js lastes riktig.