Jalankan SQL berikut di SQL Editor Supabase. Penting: nonaktifkan juga opsi "Confirm email" di Dashboard Supabase (Authentication > Sign In / Providers > Email) agar user bisa langsung login tanpa verifikasi email.
1. Setup Awal
-- Buat tabel users
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY, password TEXT NOT NULL,
name TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'staff',
password_hash TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO users (username, password, name, role) VALUES
('ADMIN','admin123','Administrator','admin'),
('FAZA','faza123','FAZA','staff'),('ZEFA','zefa123','ZEFA','staff'),
('AMIN','amin123','AMIN','staff'),('ERWIN','erwin123','ERWIN','staff'),
('PEBI','pebi123','PEBI','staff')
ON CONFLICT (username) DO NOTHING;
-- Kolom password_hash terisi otomatis (hash) saat user login pertama kali-- Buat tabel tickets (Tanpa kolom closed_at)
CREATE TABLE IF NOT EXISTS tickets (
id TEXT PRIMARY KEY, connote TEXT NOT NULL, pic TEXT,
complaint TEXT, reason TEXT, coding_destination TEXT,
status TEXT DEFAULT 'open', remark TEXT, bukti_foto TEXT,
created_by TEXT, updated_by TEXT,
reopen_count INTEGER DEFAULT 0, reopen_date TIMESTAMPTZ,
date_in_progress TIMESTAMPTZ, date_on_confirm TIMESTAMPTZ,
date_resolved TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Buat tabel app_settings untuk Backup Mode Global
CREATE TABLE IF NOT EXISTS app_settings (
id INT PRIMARY KEY DEFAULT 1,
backup_mode BOOLEAN DEFAULT FALSE
);
INSERT INTO app_settings (id, backup_mode) VALUES (1, FALSE) ON CONFLICT (id) DO NOTHING;
2. Migrasi & Pembersihan Kolom
-- Tambah kolom yang belum ada
ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS date_in_progress TIMESTAMPTZ;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS date_on_confirm TIMESTAMPTZ;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS date_resolved TIMESTAMPTZ;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS reopen_date TIMESTAMPTZ;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS reopen_count INTEGER DEFAULT 0;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS remark TEXT;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS bukti_foto TEXT;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS created_by TEXT;
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS updated_by TEXT;
-- Salin data dari resolved_at ke date_resolved (jika kolom masih ada)
UPDATE tickets SET date_resolved = COALESCE(date_resolved, resolved_at, updated_at)
WHERE status='resolved' AND date_resolved IS NULL;
-- Isi tanggal berdasarkan status lama yang kosong
UPDATE tickets SET date_in_progress = updated_at
WHERE status IN ('in_progress','on_confirm','resolved') AND date_in_progress IS NULL;
UPDATE tickets SET date_on_confirm = updated_at
WHERE status IN ('on_confirm','resolved') AND date_on_confirm IS NULL;
UPDATE tickets SET date_resolved = updated_at
WHERE status='resolved' AND date_resolved IS NULL;
UPDATE tickets SET reopen_count = 0 WHERE reopen_count IS NULL;
UPDATE tickets SET created_by = pic WHERE created_by IS NULL;
-- Ubah semua status closed menjadi resolved (Karena status closed sudah dihapus dari sistem)
UPDATE tickets SET status = 'resolved', date_resolved = COALESCE(date_resolved, closed_at, updated_at) WHERE status = 'closed';
-- HAPUS kolom yang tidak terpakai
ALTER TABLE tickets DROP COLUMN IF EXISTS resolved_at;
ALTER TABLE tickets DROP COLUMN IF EXISTS date_reopened;
ALTER TABLE tickets DROP COLUMN IF EXISTS closed_at;
3. Keamanan: RLS + Auth Gateway
-- Ekstensi & kolom pengikat akun
CREATE EXTENSION IF NOT EXISTS pgcrypto;
ALTER TABLE users ADD COLUMN IF NOT EXISTS auth_uid UUID;
-- Fungsi identitas
CREATE OR REPLACE FUNCTION public.is_app_user() RETURNS BOOLEAN
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public, extensions AS
$$ SELECT EXISTS(SELECT 1 FROM public.users u WHERE u.auth_uid = auth.uid()); $$;
CREATE OR REPLACE FUNCTION public.app_role() RETURNS TEXT
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public, extensions AS
$$ SELECT u.role FROM public.users u WHERE u.auth_uid = auth.uid() LIMIT 1; $$;
-- Login legacy (khusus user yang belum terikat akun auth)
CREATE OR REPLACE FUNCTION public.fn_legacy_login(p_username TEXT, p_password TEXT)
RETURNS TABLE (o_username TEXT, o_name TEXT, o_role TEXT)
LANGUAGE plpgsql SECURITY DEFINER STABLE SET search_path = public, extensions AS $$
DECLARE r users%ROWTYPE; v_ok BOOLEAN := FALSE; v_salt TEXT; v_hash TEXT;
BEGIN
SELECT * INTO r FROM users WHERE upper(username) = upper(p_username) LIMIT 1;
IF NOT FOUND THEN RETURN; END IF;
IF r.auth_uid IS NOT NULL THEN RETURN; END IF;
IF COALESCE(r.password,'') <> '' AND r.password = p_password THEN v_ok := TRUE; END IF;
IF NOT v_ok AND position('$' IN COALESCE(r.password_hash,'')) > 0 THEN
v_salt := split_part(r.password_hash,'$',1);
v_hash := split_part(r.password_hash,'$',2);
IF length(v_salt)=24 AND length(v_hash)=64 AND
encode(digest(v_salt || '$' || p_password,'sha256'),'hex') = v_hash THEN
v_ok := TRUE;
END IF;
END IF;
IF v_ok THEN RETURN QUERY SELECT r.username, r.name, r.role; END IF;
END; $$;
-- Pengikat profil ke akun auth (anti pembajakan username)
CREATE OR REPLACE FUNCTION public.fn_bind_auth(p_username TEXT, p_password TEXT, p_uid UUID)
RETURNS VOID
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, extensions AS $$
DECLARE r users%ROWTYPE; v_ok BOOLEAN := FALSE; v_salt TEXT; v_hash TEXT;
BEGIN
IF auth.uid() IS NULL OR p_uid IS NULL OR auth.uid() <> p_uid THEN RETURN; END IF;
SELECT * INTO r FROM users WHERE upper(username) = upper(p_username) LIMIT 1;
IF NOT FOUND THEN RETURN; END IF;
IF r.auth_uid IS NOT NULL THEN RETURN; END IF;
IF COALESCE(r.password,'') <> '' AND r.password = p_password THEN v_ok := TRUE; END IF;
IF NOT v_ok AND position('$' IN COALESCE(r.password_hash,'')) > 0 THEN
v_salt := split_part(r.password_hash,'$',1);
v_hash := split_part(r.password_hash,'$',2);
IF length(v_salt)=24 AND length(v_hash)=64 AND
encode(digest(v_salt || '$' || p_password,'sha256'),'hex') = v_hash THEN
v_ok := TRUE;
END IF;
END IF;
IF v_ok THEN
UPDATE users SET auth_uid = p_uid, password = '', password_hash = NULL
WHERE username = r.username;
END IF;
END; $$;
-- Cabut seluruh akses lama (anon tidak boleh apa-apa lagi)
DROP POLICY IF EXISTS "Allow anon access users" ON public.users;
DROP POLICY IF EXISTS "Allow anon access tickets" ON public.tickets;
DROP POLICY IF EXISTS "Allow anon access app_settings" ON public.app_settings;
REVOKE ALL ON public.users FROM anon, authenticated;
REVOKE ALL ON public.tickets FROM anon, authenticated;
REVOKE ALL ON public.app_settings FROM anon, authenticated;
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.tickets ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.app_settings ENABLE ROW LEVEL SECURITY;
-- Policy baru: hanya user yang login via Supabase Auth
CREATE POLICY "tickets_all_app_users" ON public.tickets
FOR ALL TO authenticated USING (is_app_user()) WITH CHECK (is_app_user());
CREATE POLICY "users_select_app_users" ON public.users
FOR SELECT TO authenticated USING (is_app_user());
CREATE POLICY "users_insert_admin" ON public.users
FOR INSERT TO authenticated WITH CHECK (app_role() = 'admin');
CREATE POLICY "users_update_self_or_admin" ON public.users
FOR UPDATE TO authenticated
USING (auth_uid = auth.uid() OR app_role() = 'admin')
WITH CHECK (auth_uid = auth.uid() OR app_role() = 'admin');
CREATE POLICY "users_delete_admin" ON public.users
FOR DELETE TO authenticated USING (app_role() = 'admin');
CREATE POLICY "settings_read_all" ON public.app_settings
FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "settings_update_admin" ON public.app_settings
FOR UPDATE TO authenticated USING (app_role() = 'admin')
WITH CHECK (app_role() = 'admin');
-- Grants (kolom password disembunyikan dari client biasa)
GRANT EXECUTE ON FUNCTION public.fn_legacy_login(TEXT,TEXT) TO anon, authenticated;
GRANT EXECUTE ON FUNCTION public.fn_bind_auth(TEXT,TEXT,UUID) TO authenticated;
GRANT EXECUTE ON FUNCTION public.is_app_user() TO authenticated;
GRANT EXECUTE ON FUNCTION public.app_role() TO authenticated;
GRANT SELECT ON public.app_settings TO anon;
GRANT SELECT, UPDATE ON public.app_settings TO authenticated;
GRANT ALL ON public.tickets TO authenticated;
GRANT SELECT (username, name, role, auth_uid) ON public.users TO authenticated;
GRANT INSERT (username, name, role, auth_uid, password) ON public.users TO authenticated;
GRANT UPDATE (username, name, role, auth_uid, password, password_hash) ON public.users TO authenticated;
GRANT DELETE ON public.users TO authenticated;
ConnoteTrackingCCC
Monitoring & Ticketing System
SYSTEM BACKUP MODE ACTIVE Sistem sedang dalam proses Backup. User non-admin tidak dapat login sementara.
ConnoteTracker
Selamat Datang, Admin
MODE BACKUP AKTIF: Akses user lain sementara diblokir.