Author: Adam Evyčędo <git@apiote.xyz>
return user info, create and validate tickets
accounts/subscriptions.go | 129 ++++++++++++++++++++++++++++++++++ accounts/users.go | 8 ++ config-example.toml | 4 + config/config.go | 6 + db/migrations/20240529_users.sql | 6 + db/subscriptions.go | 106 +++++++++++++++++++++++++++ db/users.go | 56 +------------- go.mod | 2 go.sum | 6 + main.go | 57 ++++++++++++++ server/router.go | 19 ++-- server/users.go | 117 ++++++++++++++++++++++++++---
diff --git a/accounts/subscriptions.go b/accounts/subscriptions.go new file mode 100644 index 0000000000000000000000000000000000000000..b86a68074f32909329269e02ba928e1283e4827d --- /dev/null +++ b/accounts/subscriptions.go @@ -0,0 +1,129 @@ +package accounts + +import ( + "bytes" + "crypto" + "crypto/ed25519" + "encoding/base64" + "errors" + "fmt" + "time" + + "git.sr.ht/~sircmpwn/go-bare" + "github.com/google/uuid" +) + +type Seat string + +func ParseSeat(s string) (Seat, error) { + switch s { + case string(SEAT_BACK): + return SEAT_BACK, nil + case string(SEAT_WINDOW): + return SEAT_WINDOW, nil + case string(SEAT_FRONT): + return SEAT_FRONT, nil + case string(SEAT_DRIVER): + return SEAT_DRIVER, nil + case string(SEAT_PILOT): + return SEAT_PILOT, nil + case string(SEAT_GARAGE): + return SEAT_GARAGE, nil + default: + return SEAT_BACK, errors.New("No such seat: " + s) + } +} + +func (s Seat) String() string { + return string(s) +} + +const ( + SEAT_BACK Seat = "back" + SEAT_WINDOW Seat = "window" + SEAT_FRONT Seat = "front" + SEAT_DRIVER Seat = "driver" + SEAT_PILOT Seat = "pilot" + SEAT_GARAGE Seat = "garage" +) + +type Subscription struct { + ID string + Plan Seat + ValidSince time.Time + Validity int // months + Creator string // user ID + Signature []byte + // TODO payment ID +} + +type SubscriptionTicket struct { + ID string + Plan string + Validity int // months + Creator string // user ID + Signature []byte +} + +func (s SubscriptionTicket) Marshal() ([]byte, error) { + bytes, err := bare.Marshal(&s) + return bytes, err +} + +func ParseSubscription(s string) (SubscriptionTicket, error) { + t := SubscriptionTicket{} + bytes, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return SubscriptionTicket{}, fmt.Errorf("while decoding base64: %w", err) + } + err = bare.Unmarshal(bytes, &t) + if err != nil { + return t, fmt.Errorf("while unmarshalling: %w", err) + } + return t, nil +} + +func CreateSubscription(plan Seat, validity int, keySeed []byte) error { + subscription := SubscriptionTicket{ + ID: uuid.NewString(), + Plan: plan.String(), + Validity: validity, + Creator: "cli", + } + + signed, err := subscription.Marshal() + if err != nil { + return fmt.Errorf("while marshalling signed subscription: %w", err) + } + + fmt.Println(base64.StdEncoding.EncodeToString(signed)) + + return nil +} + +func sign(subscription SubscriptionTicket, keySeed []byte) (SubscriptionTicket, error) { + privateKey := ed25519.NewKeyFromSeed(keySeed) + + unsigned, err := subscription.Marshal() + if err != nil { + return subscription, fmt.Errorf("while marshalling unsigned subscription: %w", err) + } + + signature, err := privateKey.Sign(nil, unsigned, crypto.Hash(0)) + if err != nil { + return subscription, fmt.Errorf("while signing subscription: %w", err) + } + subscription.Signature = signature + + return subscription, nil +} + +func CheckSignature(s SubscriptionTicket, keySeed []byte) (bool, error) { + signature := s.Signature + s.Signature = []byte{} + signed, err := sign(s, keySeed) + if err != nil { + return false, err + } + return bytes.Compare(signature, signed.Signature) == 0, nil +} diff --git a/accounts/users.go b/accounts/users.go new file mode 100644 index 0000000000000000000000000000000000000000..1862b25a8e2fa36533466ea0d6e2b3d4cd942e7f --- /dev/null +++ b/accounts/users.go @@ -0,0 +1,8 @@ +package accounts + +type User struct { + Name string + MatrixHandle string + Email string + Subscriptions []Subscription +} diff --git a/config/config.go b/config/config.go index 767b65f95141d42b86367808213d7097d68eb947..6b61a788746fe0d92de15945b5beca0e101e242b 100644 --- a/config/config.go +++ b/config/config.go @@ -42,6 +42,11 @@ Db string Params string } +type ServerConfig struct { + Host string + Port int +} + func (c DatabaseConfig) Uri() string { return fmt.Sprintf("%s://%s:%s@%s:%d/%s?%s", c.Schema, c.Username, c.Password, c.Host, c.Port, c.Db, c.Params) } @@ -49,6 +54,7 @@ type Config struct { Matrix MatrixConfig Database DatabaseConfig + Server ServerConfig } func Load(path string) (config Config, err error) { diff --git a/config-example.toml b/config-example.toml index f3bf014c11cf39e0fcf65568d7136fe48c5ef21b..d6093372ab57e74c06125b871c4192f58f78883a 100644 --- a/config-example.toml +++ b/config-example.toml @@ -23,3 +23,7 @@ host = '127.0.0.1' port = 5432 db = ampelmaennchen params = 'sslmode=disable' + +[server] +host = 'localhost' +port = 43637 diff --git a/db/migrations/20240529_users.sql b/db/migrations/20240529_users.sql index 6cce2bb69fd0beba68cf42caea9aeebc04e2297a..ca119e0e8af1b1bd3bd85094c5a29109478ddc77 100644 --- a/db/migrations/20240529_users.sql +++ b/db/migrations/20240529_users.sql @@ -1,2 +1,4 @@ -create table users(id text primary key, matrix_handle text, name text) -create table subscriptions(user_id text references users(id), valid_since timestamp with time zone, valid_till timestamp with time zone, plan text) +create table users(id text primary key, matrix_handle text, name text); +create type seat as enum('back', 'window', 'front', 'driver', 'pilot', 'garage'); +create table subscriptions(id text primary key, subscribed_user text references users(id), valid_since timestamp with time zone, validity integer, plan seat, signature bytea, creator text references users(id)); +create table keys(seed bytea) diff --git a/db/subscriptions.go b/db/subscriptions.go new file mode 100644 index 0000000000000000000000000000000000000000..5dbd52706988a2a873c7a4f18967b347bd052cc1 --- /dev/null +++ b/db/subscriptions.go @@ -0,0 +1,106 @@ +package db + +import ( + "apiote.xyz/p/ampelmaennchen/accounts" + + "database/sql" + "errors" + "fmt" + "time" +) + +func GetValidSubscription(userID string) (accounts.Subscription, error) { + subscription := accounts.Subscription{} + row := db.QueryRow("select plan, valid_since, validity from subscriptions where subscribed_user = $1 and valid_since <= now() and valid_since + valididy >= now()", userID) + err := row.Scan(&subscription.Plan, &subscription.ValidSince, &subscription.Validity) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return accounts.Subscription{ + Plan: accounts.SEAT_BACK, + ValidSince: time.Now(), + Validity: 100 * 12, + }, nil + } + return subscription, fmt.Errorf("while selecting subscription: %w", err) + } + return subscription, nil +} + +func GetSubscriptions(userID string) ([]accounts.Subscription, error) { + subscriptions := []accounts.Subscription{} + c, err := db.Query("select plan, valid_since, validity from subscriptions where subscribed_user = $1") + if err != nil { + return subscriptions, fmt.Errorf("while selecting subscriptions: %w", err) + } + for c.Next() { + var ( + rawPlan string + plan accounts.Seat + validSince time.Time + validity int + ) + err = c.Scan(&rawPlan, &validSince, &validity) + if err != nil { + return subscriptions, fmt.Errorf("while scanning subscription: %w", err) + } + plan, err = accounts.ParseSeat(rawPlan) + if err != nil { + return subscriptions, fmt.Errorf("while parsing subscription plan: %w", err) + } + subscriptions = append(subscriptions, accounts.Subscription{ + Plan: plan, + ValidSince: validSince, + Validity: validity, + }) + } + if len(subscriptions) == 0 { + return []accounts.Subscription{ + {Plan: accounts.SEAT_BACK, ValidSince: time.Now(), Validity: 100 * 12}, + }, nil + } else { + return subscriptions, nil + } +} + +func HasSubscriptionKey() (bool, error) { + var seed []byte + row := db.QueryRow("select seed from keys") + err := row.Scan(&seed) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, err + } + return true, nil +} + +func GetSubscriptionKey() ([]byte, error) { + var seed []byte + row := db.QueryRow("select seed from keys") + err := row.Scan(&seed) + return seed, err +} + +func SaveSubscriptionKey(seed []byte) error { + _, err := db.Exec("insert into keys values($1)", seed) + return err +} + +func IsTicketUnused(ticket accounts.SubscriptionTicket) (bool, error) { + var subscribedUser *string + row := db.QueryRow("select subscribed_user from subscriptions") + err := row.Scan(&subscribedUser) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return true, nil + } + return false, err + } + return subscribedUser == nil, nil +} + +func ValidateTicket(ticket accounts.SubscriptionTicket, userID string) error { + _, err := db.Exec("insert into subscriptions values($1, $2, $3, $4, $5, $6, $7) on conflict(id) do update set subscribed_user = $2, valid_since = $3", ticket.ID, userID, time.Now(), ticket.Validity, ticket.Plan, ticket.Signature, ticket.Creator) + return err +} diff --git a/db/users.go b/db/users.go index 832c0ab8f11164d94493e18256c31960eced07c9..85d85aec49df42851dcfb7d5e1c6ca585bd7ea7b 100644 --- a/db/users.go +++ b/db/users.go @@ -1,34 +1,15 @@ package db import ( + "apiote.xyz/p/ampelmaennchen/accounts" + "database/sql" "errors" "fmt" - "time" ) -const ( - SEAT_BACK = "back seat" - // ... - SEAT_GARAGE = "garage" -) - -type Subscription struct { - Plan string - ValidSince string // RFC3339 - ValidTill string // RFC3339 - // TODO payment ID -} - -type User struct { - Name string - MatrixHandle string - Email string - Subscriptions []Subscription -} - -func GetUser(userID string) (User, error) { - user := User{} +func GetUser(userID string) (accounts.User, error) { + user := accounts.User{} row := db.QueryRow("select name, matrix_handle from users where id = $1", userID) err := row.Scan(&user.Name, &user.MatrixHandle) if err != nil { @@ -39,32 +20,3 @@ return user, fmt.Errorf("while selecting user: %w", err) } return user, nil } - -func GetValidSubscription(userID string) (Subscription, error) { - subscription := Subscription{} - var ( - validSince time.Time - validTill time.Time - ) - row := db.QueryRow("select plan, valid_since, valid_till from subscriptions where user_id = $1 and valid_since <= now() and valid_till >= now()", userID) - err := row.Scan(&subscription.Plan, &validSince, &validTill) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return Subscription{ - Plan: SEAT_BACK, - ValidTill: time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC).Format(time.RFC3339), - }, nil - } - return subscription, fmt.Errorf("while selecting user: %w", err) - } - subscription.ValidSince = validSince.Format(time.RFC3339) - subscription.ValidTill = validTill.Format(time.RFC3339) - return subscription, nil -} - -func GetSubscriptions(userID string) ([]Subscription, error) { - // TODO actually get subscriptions - return []Subscription{ - {Plan: SEAT_BACK, ValidTill: time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC).Format(time.RFC3339)}, - }, nil -} diff --git a/go.mod b/go.mod index 2fb8b8b961025393244c601ce14411613b506a1a..411b27c9d61c3ae6f00942d520e99c7b3e5234d8 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ require ( apiote.xyz/p/gott/v2 v2.0.3 git.sr.ht/~sircmpwn/getopt v1.0.0 + git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 github.com/BurntSushi/toml v1.3.2 github.com/gabriel-vasile/mimetype v1.4.4 + github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.5.5 go.mongodb.org/mongo-driver/v2 v2.0.0-beta1 maunium.net/go/mautrix v0.18.1 diff --git a/go.sum b/go.sum index 709fb0b67d4a3ce263ac113db94c5ed4983db032..bc5e521a87f3fb3b94bffe36bb5b9be62b53e43b 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,10 @@ apiote.xyz/p/gott/v2 v2.0.3 h1:CUFo0OAau20eRCPq/D11F9hQjKMB2cwLz6dZupgs7ME= apiote.xyz/p/gott/v2 v2.0.3/go.mod h1:H87aFMqvof1DWBzJuxzLaQRby4+PrIvFRwMfTTB6lK8= +git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= git.sr.ht/~sircmpwn/getopt v1.0.0 h1:/pRHjO6/OCbBF4puqD98n6xtPEgE//oq5U8NXjP7ROc= git.sr.ht/~sircmpwn/getopt v1.0.0/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 h1:Ahny8Ud1LjVMMAlt8utUFKhhxJtwBAualvsbc/Sk7cE= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -14,6 +17,8 @@ github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -45,6 +50,7 @@ github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= diff --git a/main.go b/main.go index 48e42106f1a16942649907238bef5742666a3c61..59814501c62c6de942dcd29f53fded8a272abc21 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,13 @@ package main import ( + "apiote.xyz/p/ampelmaennchen/accounts" "apiote.xyz/p/ampelmaennchen/config" "apiote.xyz/p/ampelmaennchen/db" "apiote.xyz/p/ampelmaennchen/matrix" "apiote.xyz/p/ampelmaennchen/server" + "crypto/ed25519" "fmt" "log" "os" @@ -16,10 +18,11 @@ var ( recreateRegistration bool command string + configPath string = "./config.toml" ) func parseArgs() { - opts, optind, err := getopt.Getopts(os.Args, "r") + opts, optind, err := getopt.Getopts(os.Args, "rc:") if err != nil { log.Fatalf("while getopts: %v", err) } @@ -27,6 +30,8 @@ for _, opt := range opts { switch opt.Option { case 'r': recreateRegistration = true + case 'c': + configPath = opt.Value } } for i, arg := range os.Args[optind:] { @@ -38,7 +43,7 @@ } func main() { parseArgs() - cfg, err := config.Load("./config.toml") + cfg, err := config.Load(configPath) if err != nil { log.Fatalf("while loading config: %v", err) } @@ -88,5 +93,51 @@ log.Println(err) err = matrix.CreateHeadOfficeRoom(as, cfg.Matrix, spaceID) log.Println(err) - server.Route("localhost:43637") + hasKeys, err := db.HasSubscriptionKey() + if err != nil { + log.Println(err) + os.Exit(1) + } + if !hasKeys { + _, private, err := ed25519.GenerateKey(nil) + if err != nil { + log.Println(err) + os.Exit(1) + } + err = db.SaveSubscriptionKey(private.Seed()) + if err != nil { + log.Println(err) + os.Exit(1) + } + } + + switch command { + case "serve": + server.Route(cfg.Server.Host, cfg.Server.Port) + case "csub": + var ( + planRaw string + plan accounts.Seat + validity int + err error + ) + fmt.Printf("Plan (back, window, front, driver, pilot, garage): ") + fmt.Scanf("%s", &planRaw) + if plan, err = accounts.ParseSeat(planRaw); err != nil { + log.Printf("error parsing plan: %v\n", err) + os.Exit(1) + } + fmt.Printf("Validity (in months): ") + fmt.Scanf("%d", &validity) + keySeed, err := db.GetSubscriptionKey() + if err != nil { + log.Printf("while marshalling unsigned subscription: %w", err) + os.Exit(1) + } + err = accounts.CreateSubscription(plan, validity, keySeed) + if err != nil { + log.Printf("while creating subscription: %v\n", err) + os.Exit(1) + } + } } diff --git a/server/router.go b/server/router.go index 067a9b9a8974b4491e4a1f6b41282e0cf6d04329..35593315bf40539185f356ae202d3e211dc6f250 100644 --- a/server/router.go +++ b/server/router.go @@ -1,24 +1,25 @@ package server import ( + "fmt" "log/slog" "net/http" "os" ) -func Route(listenAddress string) *http.Server { +func Route(host string, port int) { + listenAddress := fmt.Sprintf("%s:%d", host, port) srv := &http.Server{Addr: listenAddress} http.HandleFunc("GET /users/{userID}", func(w http.ResponseWriter, r *http.Request) { getUser(r.PathValue("userID"), w, r) }) + http.HandleFunc("POST /users/{userID}/subscriptions/", func(w http.ResponseWriter, r *http.Request) { + validateTicket(r.PathValue("userID"), w, r) + }) - go func() { - if err := srv.ListenAndServe(); err != http.ErrServerClosed { - slog.Error("ListenAndServe error", slog.Any("error", err)) - os.Exit(1) - } - }() - - return srv + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + slog.Error("ListenAndServe error", slog.Any("error", err)) + os.Exit(1) + } } diff --git a/server/users.go b/server/users.go index 7601fafcd59c8c59401c5ca9c6ade65d6e3f29c2..b1f82e19de720ebc83d091fab2835b73f55e8a57 100644 --- a/server/users.go +++ b/server/users.go @@ -1,6 +1,9 @@ package server import ( + "fmt" + + "apiote.xyz/p/ampelmaennchen/accounts" "apiote.xyz/p/ampelmaennchen/db" "encoding/json" @@ -19,14 +22,24 @@ Sub string `json:"sub"` Email string `json:"email"` } -func getUser(userID string, w http.ResponseWriter, r *http.Request) { +type userInfoError struct { + status int + cause error +} + +func (e userInfoError) Error() string { + return e.cause.Error() +} + +func getUserInfo(authorization string) (userInfo, error) { client := http.Client{} issuer := "https://oauth-bimba.apiote.xyz" response, err := client.Get(issuer + "/.well-known/openid-configuration") if err != nil || response.StatusCode != 200 { - w.WriteHeader(http.StatusInternalServerError) - log.Printf("while getting OIDC configuration: %v", err) - return + return userInfo{}, userInfoError{ + status: http.StatusInternalServerError, + cause: fmt.Errorf("while getting OIDC configuration: %w", err), + } } oidcConfig := oidcConfig{} decoder := json.NewDecoder(response.Body) @@ -34,36 +47,48 @@ decoder.Decode(&oidcConfig) userinfoRequest, err := http.NewRequest(http.MethodGet, oidcConfig.UserinfoEndpoint, nil) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - log.Printf("while creating request to userinfo: %v", err) - return + return userInfo{}, userInfoError{ + status: http.StatusInternalServerError, + cause: fmt.Errorf("while creating request to userinfo: %w", err), + } } - userinfoRequest.Header.Add("authorization", r.Header.Get("authorization")) + userinfoRequest.Header.Add("authorization", authorization) response, err = client.Do(userinfoRequest) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - log.Printf("while performing request to userinfo: %v", err) - return + return userInfo{}, userInfoError{ + status: http.StatusInternalServerError, + cause: fmt.Errorf("while performing request to userinfo: %w", err), + } } if response.StatusCode == http.StatusForbidden || response.StatusCode == http.StatusUnauthorized { - w.WriteHeader(response.StatusCode) - log.Printf("%d from userinfo", response.StatusCode) - return + return userInfo{}, userInfoError{ + status: response.StatusCode, + cause: fmt.Errorf("%d from userinfo", response.StatusCode), + } } userInfo := userInfo{} decoder = json.NewDecoder(response.Body) decoder.Decode(&userInfo) + return userInfo, nil +} +func getUser(userID string, w http.ResponseWriter, r *http.Request) { + userInfo, err := getUserInfo(r.Header.Get("authorization")) + if err != nil { + w.WriteHeader(err.(userInfoError).status) + log.Printf("while getting user info: %v", err) + return + } currentSubscription, err := db.GetValidSubscription(userInfo.Sub) if err != nil { w.WriteHeader(http.StatusInternalServerError) log.Printf("while getting logged-in user subscription from db: %v", err) return } - if !(currentSubscription.Plan == db.SEAT_GARAGE || userInfo.Sub == userID) { + if !(currentSubscription.Plan == accounts.SEAT_GARAGE || userInfo.Sub == userID) { w.WriteHeader(http.StatusForbidden) return } @@ -90,3 +115,65 @@ return } w.Write(marshalledUser) } + +func validateTicket(userID string, w http.ResponseWriter, r *http.Request) { + userInfo, err := getUserInfo(r.Header.Get("authorization")) + if err != nil { + w.WriteHeader(err.(userInfoError).status) + log.Printf("while getting user info: %v", err) + return + } + if !(userInfo.Sub == userID) { + w.WriteHeader(http.StatusForbidden) + log.Printf("user mismatch") + return + } + r.ParseForm() + + ticket, err := accounts.ParseSubscription(r.Form.Get("ticket")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + log.Printf("while parsing subscription: %v", err) + return + } + + isTicketSlotFree, err := db.IsTicketUnused(ticket) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Printf("while checking free ticket slot: %v", err) + return + } + + if !isTicketSlotFree { + w.WriteHeader(http.StatusForbidden) + log.Printf("ticket used") + return + } + + seed, err := db.GetSubscriptionKey() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Printf("while getting seed: %v", err) + return + } + + ticketSignedOK, err := accounts.CheckSignature(ticket, seed) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Printf("while checking ticket signature: %v", err) + return + } + + if !ticketSignedOK { + w.WriteHeader(http.StatusBadRequest) + log.Printf("ticket signature incorrect") + return + } + + err = db.ValidateTicket(ticket, userID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Printf("while validating ticket: %v", err) + return + } +}