feat(device): OAuth2 device-pairing flow for HA add-on (code/approve/token/tunnel + /activate)

This commit is contained in:
Gerhard Scheikl
2026-06-01 10:35:41 +02:00
parent 2256f8359b
commit 2c2afa458e
9 changed files with 863 additions and 1 deletions
@@ -0,0 +1,91 @@
-- 0003_device_pairing.sql
-- Additive, idempotent migration for the OAuth2 device-authorization-grant
-- style "device pairing" flow used by the Home Assistant add-on.
-- Safe to run multiple times.
-- 1. Device pairings ------------------------------------------------------
-- One row per device-pairing attempt. The opaque device_code is only ever
-- stored as a SHA-256 hash; the short, human-typed user_code is stored in the
-- clear so it can be looked up when the user approves on the web.
create table if not exists public.device_pairings (
id bigint generated always as identity primary key,
device_code_hash text not null,
user_code text not null,
user_id uuid,
subdomain citext,
status text not null default 'pending',
interval_seconds int not null default 5,
created_at timestamptz not null default now(),
expires_at timestamptz not null,
approved_at timestamptz,
claimed_at timestamptz,
last_polled_at timestamptz
);
create unique index if not exists device_pairings_device_code_hash_uidx
on public.device_pairings (device_code_hash);
create unique index if not exists device_pairings_user_code_uidx
on public.device_pairings (user_code);
create index if not exists device_pairings_expires_at_idx
on public.device_pairings (expires_at);
-- Cascade-delete pairings when the owning auth user is removed.
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'device_pairings_user_id_fkey'
and conrelid = 'public.device_pairings'::regclass
) then
alter table public.device_pairings
add constraint device_pairings_user_id_fkey
foreign key (user_id) references auth.users (id) on delete cascade;
end if;
end $$;
-- RLS enabled with NO policies: service-role only.
alter table public.device_pairings enable row level security;
comment on table public.device_pairings is
'Device-authorization-grant pairings (device_code SHA-256 hashed). RLS enabled with no policies: service-role only.';
-- 2. Device tokens --------------------------------------------------------
-- Long-lived bearer tokens minted to a paired device. Only the SHA-256 hash
-- is stored; the plaintext is returned to the device exactly once at mint.
create table if not exists public.device_tokens (
id bigint generated always as identity primary key,
token_hash text not null,
user_id uuid not null,
label text,
created_at timestamptz not null default now(),
last_used_at timestamptz,
revoked_at timestamptz
);
create unique index if not exists device_tokens_token_hash_uidx
on public.device_tokens (token_hash);
create index if not exists device_tokens_user_id_idx
on public.device_tokens (user_id);
-- Cascade-delete device tokens when the owning auth user is removed.
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'device_tokens_user_id_fkey'
and conrelid = 'public.device_tokens'::regclass
) then
alter table public.device_tokens
add constraint device_tokens_user_id_fkey
foreign key (user_id) references auth.users (id) on delete cascade;
end if;
end $$;
-- RLS enabled with NO policies: service-role only.
alter table public.device_tokens enable row level security;
comment on table public.device_tokens is
'Device bearer tokens (SHA-256 hashed). RLS enabled with no policies: service-role only.';