diff --git a/db/sqlc/migrations/000001_init_schema.down.sql b/db/sqlc/migrations/000001_init_schema.down.sql index 2629106..e43c023 100644 --- a/db/sqlc/migrations/000001_init_schema.down.sql +++ b/db/sqlc/migrations/000001_init_schema.down.sql @@ -1,4 +1,10 @@ DROP TABLE IF EXISTS public.verification; +DROP TABLE IF EXISTS public.jwks; DROP TABLE IF EXISTS public.session; DROP TABLE IF EXISTS public.account; -DROP TABLE IF EXISTS public."user"; \ No newline at end of file +DROP TABLE IF EXISTS public."user"; + +DROP TABLE IF EXISTS drizzle.__drizzle_migrations; +DROP SCHEMA IF EXISTS drizzle; + +DROP EXTENSION IF EXISTS pgcrypto; diff --git a/db/sqlc/migrations/000001_init_schema.up.sql b/db/sqlc/migrations/000001_init_schema.up.sql index fe65b81..1c8d24b 100644 --- a/db/sqlc/migrations/000001_init_schema.up.sql +++ b/db/sqlc/migrations/000001_init_schema.up.sql @@ -1,52 +1,88 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto; -CREATE TABLE IF NOT EXISTS public."user" ( - id text NOT NULL, - ssh_identifier text UNIQUE NOT NULL DEFAULT substr(encode(gen_random_bytes(16), 'hex'), 1, 32), - name text NOT NULL, - email text NOT NULL, - email_verified boolean DEFAULT false NOT NULL, - image text, - created_at timestamp without time zone DEFAULT now() NOT NULL, - updated_at timestamp without time zone DEFAULT now() NOT NULL, - CONSTRAINT user_pkey PRIMARY KEY (id) +CREATE TABLE public."user" ( + id TEXT PRIMARY KEY, + ssh_identifier TEXT NOT NULL DEFAULT substr(encode(gen_random_bytes(16), 'hex'), 1, 32), + name TEXT NOT NULL, + email TEXT NOT NULL, + email_verified BOOLEAN NOT NULL DEFAULT false, + image TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now() ); -CREATE TABLE IF NOT EXISTS public.account ( - id text NOT NULL, - account_id text NOT NULL, - provider_id text NOT NULL, - user_id text NOT NULL, - access_token text, - refresh_token text, - id_token text, - access_token_expires_at timestamp without time zone, - refresh_token_expires_at timestamp without time zone, - scope text, - password text, - created_at timestamp without time zone DEFAULT now() NOT NULL, - updated_at timestamp without time zone NOT NULL, - CONSTRAINT account_pkey PRIMARY KEY (id) +CREATE UNIQUE INDEX user_email_unique + ON public."user"(email); + +CREATE UNIQUE INDEX user_ssh_identifier_unique + ON public."user"(ssh_identifier); + +CREATE TABLE public.account ( + id TEXT PRIMARY KEY, + account_id TEXT NOT NULL, + provider_id TEXT NOT NULL, + user_id TEXT NOT NULL, + access_token TEXT, + refresh_token TEXT, + id_token TEXT, + access_token_expires_at TIMESTAMP WITHOUT TIME ZONE, + refresh_token_expires_at TIMESTAMP WITHOUT TIME ZONE, + scope TEXT, + password TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL ); -CREATE TABLE IF NOT EXISTS public.session ( - id text NOT NULL, - expires_at timestamp without time zone NOT NULL, - token text NOT NULL, - created_at timestamp without time zone DEFAULT now() NOT NULL, - updated_at timestamp without time zone NOT NULL, - ip_address text, - user_agent text, - user_id text NOT NULL, - CONSTRAINT session_pkey PRIMARY KEY (id) +CREATE INDEX account_userId_idx + ON public.account(user_id); + +ALTER TABLE public.account + ADD CONSTRAINT account_user_id_user_id_fk + FOREIGN KEY (user_id) + REFERENCES public."user"(id) + ON DELETE CASCADE; + + +CREATE TABLE public.session ( + id TEXT PRIMARY KEY, + expires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + token TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + ip_address TEXT, + user_agent TEXT, + user_id TEXT NOT NULL ); -CREATE TABLE IF NOT EXISTS public.verification ( - id text NOT NULL, - identifier text NOT NULL, - value text NOT NULL, - expires_at timestamp without time zone NOT NULL, - created_at timestamp without time zone DEFAULT now() NOT NULL, - updated_at timestamp without time zone DEFAULT now() NOT NULL, - CONSTRAINT verification_pkey PRIMARY KEY (id) -); \ No newline at end of file +CREATE UNIQUE INDEX session_token_unique + ON public.session(token); + +CREATE INDEX session_userId_idx + ON public.session(user_id); + +ALTER TABLE public.session + ADD CONSTRAINT session_user_id_user_id_fk + FOREIGN KEY (user_id) + REFERENCES public."user"(id) + ON DELETE CASCADE; + + +CREATE TABLE public.jwks ( + id TEXT PRIMARY KEY, + public_key TEXT NOT NULL, + private_key TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + expires_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE public.verification ( + id TEXT PRIMARY KEY, + identifier TEXT NOT NULL, + value TEXT NOT NULL, + expires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now() +); + +CREATE INDEX verification_identifier_idx + ON public.verification(identifier); diff --git a/go.mod b/go.mod index 3f1afef..9416d7a 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,33 @@ module git.fossy.my.id/bagas/tunnel-please-controller go 1.25.5 require ( - git.fossy.my.id/bagas/tunnel-please-grpc v1.0.0 - github.com/google/uuid v1.6.0 + git.fossy.my.id/bagas/tunnel-please-grpc v1.2.0 github.com/jackc/pgx/v5 v5.8.0 github.com/joho/godotenv v1.5.1 + github.com/lestrrat-go/httprc/v3 v3.0.1 + github.com/lestrrat-go/jwx/v3 v3.0.12 google.golang.org/grpc v1.78.0 - google.golang.org/protobuf v1.36.11 ) require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/dsig v1.0.0 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect + github.com/segmentio/asm v1.2.1 // indirect + github.com/valyala/fastjson v1.6.4 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/protobuf v1.36.11 // indirect ) + replace git.fossy.my.id/bagas/tunnel-please-grpc => ../tunnel-please-grpc diff --git a/go.sum b/go.sum index 644d8e0..4b6944c 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,14 @@ -git.fossy.my.id/bagas/tunnel-please-grpc v1.0.0 h1:G9U7YmfMBeTK8ASbqdBCmkrRsCNU3e6RUzGaTmg1NB0= -git.fossy.my.id/bagas/tunnel-please-grpc v1.0.0/go.mod h1:fG+VkArdkceGB0bNA7IFQus9GetLAwdF5Oi4jdMlXtY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -23,13 +25,35 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= +github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI= +github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk= +github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg= +github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= +github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -42,18 +66,20 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..5e5ae08 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,12 @@ +package config + +import "os" + +func Getenv(key, defaultValue string) string { + val := os.Getenv(key) + if val == "" { + val = defaultValue + } + + return val +} diff --git a/main.go b/main.go index b3a2cf4..0e8d308 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,8 @@ import ( "git.fossy.my.id/bagas/tunnel-please-controller/server" "github.com/jackc/pgx/v5" "github.com/joho/godotenv" + "github.com/lestrrat-go/httprc/v3" + "github.com/lestrrat-go/jwx/v3/jwk" ) func main() { @@ -55,7 +57,12 @@ func main() { }(connect, ctx) repo := repository.New(connect) - s := server.New(repo, authToken) + client := httprc.NewClient() + jwkCache, err := jwk.NewCache(ctx, client) + if err != nil { + log.Printf("failed to initialize jwk cache : %s", err) + } + s := server.New(repo, authToken, jwkCache) log.Printf("Listening controller on %s", controllerAddr) log.Printf("Listening api on %s", apiAddr) diff --git a/server/server.go b/server/server.go index 4a46653..d49e31d 100644 --- a/server/server.go +++ b/server/server.go @@ -13,7 +13,10 @@ import ( "time" "git.fossy.my.id/bagas/tunnel-please-controller/db/sqlc/repository" + "git.fossy.my.id/bagas/tunnel-please-controller/internal/config" proto "git.fossy.my.id/bagas/tunnel-please-grpc/gen" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/health" @@ -39,17 +42,19 @@ type Server struct { Subscribers map[string]*Subscriber mu *sync.RWMutex authToken string + jwkCache *jwk.Cache proto.UnimplementedEventServiceServer proto.UnimplementedSlugChangeServer proto.UnimplementedUserServiceServer } -func New(database *repository.Queries, authToken string) *Server { +func New(database *repository.Queries, authToken string, jwkCache *jwk.Cache) *Server { return &Server{ Database: database, Subscribers: make(map[string]*Subscriber), mu: new(sync.RWMutex), authToken: authToken, + jwkCache: jwkCache, } } @@ -348,14 +353,63 @@ func (s *Server) StartAPI(ctx context.Context, Addr string) error { IdleTimeout: 60 * time.Second, } + jwkURL := config.Getenv("JWKS_URL", "") + if jwkURL != "" { + if err := s.jwkCache.Register(ctx, jwkURL); err != nil { + return fmt.Errorf("failed to register jwk cache: %w", err) + } + } + handler.HandleFunc("/api/sessions", func(writer http.ResponseWriter, request *http.Request) { - identity := request.URL.Query().Get("identity") + writeError := func(status int, msg string) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(status) + _ = json.NewEncoder(writer).Encode(map[string]string{"error": msg}) + } + + var token jwt.Token + var err error + if jwkURL != "" { + keyset, err := s.jwkCache.Lookup(request.Context(), jwkURL) + if err != nil { + log.Printf("jwks lookup failed: %v", err) + writeError(http.StatusBadGateway, "unable to fetch jwks") + return + } + + token, err = jwt.ParseRequest(request, jwt.WithKeySet(keyset)) + if err != nil { + log.Printf("jwt parse failed: %v", err) + writeError(http.StatusUnauthorized, "invalid or expired token") + return + } + } else { + token, err = jwt.ParseRequest(request, jwt.WithVerify(false)) + if err != nil { + log.Printf("jwt parse failed (no verification): %v", err) + writeError(http.StatusBadRequest, "invalid token") + return + } + } + + var email string + err = token.Get("email", &email) + if err != nil { + log.Printf("email claim not found: %v", err) + writeError(http.StatusBadRequest, "missing email claim in token") + return + } + if email == "" { + writeError(http.StatusBadRequest, "empty email claim in token") + return + } + results := s.broadcastAndCollect(request.Context(), func(ctx context.Context, subscriber *Subscriber) (interface{}, bool) { receive, err := s.sendAndReceive(ctx, subscriber, &proto.Events{ Type: proto.EventType_GET_SESSIONS, Payload: &proto.Events_GetSessionsEvent{ GetSessionsEvent: &proto.GetSessionsEvent{ - Identity: identity, + Identity: email, }, }, }, defaultSubscriberResponseWait) @@ -382,18 +436,20 @@ func (s *Server) StartAPI(ctx context.Context, Addr string) error { if len(flatten) == 0 { _, err := writer.Write([]byte("[]")) if err != nil { - return + log.Printf("write empty sessions response failed: %v", err) } return } marshal, err := json.Marshal(flatten) if err != nil { - http.Error(writer, "failed to marshal sessions", http.StatusInternalServerError) + log.Printf("marshal sessions failed: %v", err) + writeError(http.StatusInternalServerError, "failed to marshal sessions") return } _, err = writer.Write(marshal) if err != nil { + log.Printf("write sessions response failed: %v", err) return } })