← Tilbake

Roller og innlogging m/Supabase 🔐

Lær hvordan du lager et profesjonelt innloggingssystem med automatiske roller (admin/user).

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 anon key under Settings -> API.
  3. 1.3Viktig: Bytt ut e-posten i koden under før du kjører den i SQL Editor.
-- 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: Struktur

Lag index.html. Denne inneholder selve siden og det skjulte panelet. index.html, style.css og script.js skal ligge i samme mappe.

<!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/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">
      <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="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-ANON-PUBLIC-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');

openBtn.addEventListener('click', ()=> panel.classList.add('open'));
closeBtn.addEventListener('click', ()=> panel.classList.remove('open'));

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   = '';
    return;
  }

  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';
}

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

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 ✅ Sjekk e-posten din for bekreftelse!.';
});

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

supabase.auth.onAuthStateChange(() => refreshAuthUI());
refreshAuthUI();

Test & feilsøking