diff --git a/api/cmd/main.go b/api/cmd/main.go index b4258c2..3aa83de 100644 --- a/api/cmd/main.go +++ b/api/cmd/main.go @@ -8,7 +8,6 @@ import ( "ivpn.net/email/api/internal/repository" "ivpn.net/email/api/internal/service" "ivpn.net/email/api/internal/transport/api" - "ivpn.net/email/api/internal/utils" ) func Run() error { @@ -17,8 +16,6 @@ func Run() error { return err } - utils.NewLogger(cfg.API) - db, err := repository.NewDB(cfg.DB) if err != nil { return err diff --git a/api/docs/docs.go b/api/docs/docs.go index cfb9b01..47f083d 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -922,6 +922,46 @@ const docTemplate = `{ } } }, + "/rotatepasession": { + "put": { + "description": "Rotate pre-auth session ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "subscription" + ], + "summary": "Rotate pre-auth session ID", + "parameters": [ + { + "description": "Rotate pre-auth session request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.RotatePASessionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.SuccessRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorRes" + } + } + } + } + }, "/settings": { "get": { "security": [ @@ -1033,6 +1073,51 @@ const docTemplate = `{ } } }, + "/sub/session": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add pre-auth session", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "subscription" + ], + "summary": "Add pre-auth session", + "parameters": [ + { + "description": "Pre-auth session request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.PASessionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.SuccessRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorRes" + } + } + } + } + }, "/subscription/update": { "put": { "security": [ @@ -1715,6 +1800,25 @@ const docTemplate = `{ } } }, + "api.PASessionReq": { + "type": "object", + "required": [ + "id", + "preauth_id", + "token" + ], + "properties": { + "id": { + "type": "string" + }, + "preauth_id": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, "api.RecipientReq": { "type": "object", "required": [ @@ -1749,6 +1853,17 @@ const docTemplate = `{ } } }, + "api.RotatePASessionReq": { + "type": "object", + "required": [ + "sessionid" + ], + "properties": { + "sessionid": { + "type": "string" + } + } + }, "api.SettingsReq": { "type": "object", "required": [ @@ -1776,20 +1891,12 @@ const docTemplate = `{ "type": "object", "required": [ "email", - "preauthid", - "preauthtokenhash", "subid" ], "properties": { "email": { "type": "string" }, - "preauthid": { - "type": "string" - }, - "preauthtokenhash": { - "type": "string" - }, "subid": { "type": "string" } @@ -1799,8 +1906,6 @@ const docTemplate = `{ "type": "object", "required": [ "email", - "preauthid", - "preauthtokenhash", "subid" ], "properties": { @@ -1810,12 +1915,6 @@ const docTemplate = `{ "password": { "type": "string" }, - "preauthid": { - "type": "string" - }, - "preauthtokenhash": { - "type": "string" - }, "subid": { "type": "string" } @@ -1825,20 +1924,12 @@ const docTemplate = `{ "type": "object", "required": [ "id", - "preauthid", - "preauthtokenhash", "subid" ], "properties": { "id": { "type": "string" }, - "preauthid": { - "type": "string" - }, - "preauthtokenhash": { - "type": "string" - }, "subid": { "type": "string" } diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 62e6409..6a54d6c 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -911,6 +911,46 @@ } } }, + "/rotatepasession": { + "put": { + "description": "Rotate pre-auth session ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "subscription" + ], + "summary": "Rotate pre-auth session ID", + "parameters": [ + { + "description": "Rotate pre-auth session request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.RotatePASessionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.SuccessRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorRes" + } + } + } + } + }, "/settings": { "get": { "security": [ @@ -1022,6 +1062,51 @@ } } }, + "/sub/session": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add pre-auth session", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "subscription" + ], + "summary": "Add pre-auth session", + "parameters": [ + { + "description": "Pre-auth session request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.PASessionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.SuccessRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorRes" + } + } + } + } + }, "/subscription/update": { "put": { "security": [ @@ -1704,6 +1789,25 @@ } } }, + "api.PASessionReq": { + "type": "object", + "required": [ + "id", + "preauth_id", + "token" + ], + "properties": { + "id": { + "type": "string" + }, + "preauth_id": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, "api.RecipientReq": { "type": "object", "required": [ @@ -1738,6 +1842,17 @@ } } }, + "api.RotatePASessionReq": { + "type": "object", + "required": [ + "sessionid" + ], + "properties": { + "sessionid": { + "type": "string" + } + } + }, "api.SettingsReq": { "type": "object", "required": [ @@ -1765,20 +1880,12 @@ "type": "object", "required": [ "email", - "preauthid", - "preauthtokenhash", "subid" ], "properties": { "email": { "type": "string" }, - "preauthid": { - "type": "string" - }, - "preauthtokenhash": { - "type": "string" - }, "subid": { "type": "string" } @@ -1788,8 +1895,6 @@ "type": "object", "required": [ "email", - "preauthid", - "preauthtokenhash", "subid" ], "properties": { @@ -1799,12 +1904,6 @@ "password": { "type": "string" }, - "preauthid": { - "type": "string" - }, - "preauthtokenhash": { - "type": "string" - }, "subid": { "type": "string" } @@ -1814,20 +1913,12 @@ "type": "object", "required": [ "id", - "preauthid", - "preauthtokenhash", "subid" ], "properties": { "id": { "type": "string" }, - "preauthid": { - "type": "string" - }, - "preauthtokenhash": { - "type": "string" - }, "subid": { "type": "string" } diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 6c3af6a..e3417e5 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -52,6 +52,19 @@ definitions: error: type: string type: object + api.PASessionReq: + properties: + id: + type: string + preauth_id: + type: string + token: + type: string + required: + - id + - preauth_id + - token + type: object api.RecipientReq: properties: id: @@ -74,6 +87,13 @@ definitions: required: - otp type: object + api.RotatePASessionReq: + properties: + sessionid: + type: string + required: + - sessionid + type: object api.SettingsReq: properties: alias_format: @@ -93,16 +113,10 @@ definitions: properties: email: type: string - preauthid: - type: string - preauthtokenhash: - type: string subid: type: string required: - email - - preauthid - - preauthtokenhash - subid type: object api.SignupUserReq: @@ -111,32 +125,20 @@ definitions: type: string password: type: string - preauthid: - type: string - preauthtokenhash: - type: string subid: type: string required: - email - - preauthid - - preauthtokenhash - subid type: object api.SubscriptionReq: properties: id: type: string - preauthid: - type: string - preauthtokenhash: - type: string subid: type: string required: - id - - preauthid - - preauthtokenhash - subid type: object api.SuccessRes: @@ -902,6 +904,32 @@ paths: summary: Reset password tags: - user + /rotatepasession: + put: + consumes: + - application/json + description: Rotate pre-auth session ID + parameters: + - description: Rotate pre-auth session request + in: body + name: body + required: true + schema: + $ref: '#/definitions/api.RotatePASessionReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.SuccessRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.ErrorRes' + summary: Rotate pre-auth session ID + tags: + - subscription /settings: get: consumes: @@ -971,6 +999,34 @@ paths: summary: Get subscription tags: - subscription + /sub/session: + post: + consumes: + - application/json + description: Add pre-auth session + parameters: + - description: Pre-auth session request + in: body + name: body + required: true + schema: + $ref: '#/definitions/api.PASessionReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.SuccessRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.ErrorRes' + security: + - ApiKeyAuth: [] + summary: Add pre-auth session + tags: + - subscription /subscription/update: put: consumes: diff --git a/api/internal/middleware/auth/auth.go b/api/internal/middleware/auth/auth.go index 3ab9237..b0448ee 100644 --- a/api/internal/middleware/auth/auth.go +++ b/api/internal/middleware/auth/auth.go @@ -24,6 +24,7 @@ const ( AUTH_COOKIE = "auth" AUTHN_COOKIE = "authn" AUTHN_TEMP_COOKIE = "authntemp" + PA_SESSION_COOKIE = "pasession" USER_ID = "user_id" ) @@ -134,6 +135,17 @@ func NewCookieTempAuthn(token string, path string, cfg config.APIConfig) *fiber. } } +func NewCookiePASession(id string) *fiber.Cookie { + return &fiber.Cookie{ + Name: PA_SESSION_COOKIE, + Value: id, + HTTPOnly: true, + Secure: true, + MaxAge: 900, // 15 minutes + Expires: time.Now().Add(15 * time.Minute), + } +} + func NewWebAuthn(cfg config.APIConfig) *webauthn.WebAuthn { var webAuthn *webauthn.WebAuthn config := &webauthn.Config{ diff --git a/api/internal/model/pa_session.go b/api/internal/model/pa_session.go new file mode 100644 index 0000000..e71f25d --- /dev/null +++ b/api/internal/model/pa_session.go @@ -0,0 +1,7 @@ +package model + +type PASession struct { + ID string `json:"id"` + Token string `json:"token"` + PreauthId string `json:"preauth_id"` +} diff --git a/api/internal/service/subscription.go b/api/internal/service/subscription.go index c1c6891..415e586 100644 --- a/api/internal/service/subscription.go +++ b/api/internal/service/subscription.go @@ -2,8 +2,12 @@ package service import ( "context" + "crypto/sha256" + "encoding/base64" + "encoding/json" "errors" "log" + "time" "github.com/go-sql-driver/mysql" "github.com/google/uuid" @@ -16,6 +20,8 @@ var ( ErrPostSubscription = errors.New("Unable to create subscription.") ErrUpdateSubscription = errors.New("Unable to update subscription.") ErrDeleteSubscription = errors.New("Unable to delete subscription.") + ErrPANotFound = errors.New("Pre-auth entry not found.") + ErrPASessionNotFound = errors.New("Pre-auth session not found.") ) type SubscriptionStore interface { @@ -71,15 +77,26 @@ func (s *Service) AddSubscription(ctx context.Context, subscription model.Subscr return nil } -func (s *Service) UpdateSubscription(ctx context.Context, sub model.Subscription, subID string, preauthID string, preauthTokenHash string) error { - preauth, err := s.Http.GetPreauth(preauthID) +func (s *Service) UpdateSubscription(ctx context.Context, sub model.Subscription, subID string, sessionId string) error { + paSession, err := s.GetPASession(ctx, sessionId) if err != nil { - log.Printf("error creating user: %s", err.Error()) - return ErrInvalidSubscription + log.Printf("error updating subscription: %s", err.Error()) + return ErrPASessionNotFound + } + + preauthId := paSession.PreauthId + token := paSession.Token + tokenHash := sha256.Sum256([]byte(token)) + tokenHashStr := base64.StdEncoding.EncodeToString(tokenHash[:]) + + preauth, err := s.Http.GetPreauth(preauthId) + if err != nil { + log.Printf("error updating subscription: %s", err.Error()) + return ErrPANotFound } - if preauth.TokenHash != preauthTokenHash { - log.Printf("error creating user: Token hash does not match") + if preauth.TokenHash != tokenHashStr { + log.Printf("error updating subscription: Token hash does not match") return ErrTokenHashMismatch } @@ -88,7 +105,7 @@ func (s *Service) UpdateSubscription(ctx context.Context, sub model.Subscription sub.Tier = preauth.Tier sub.TokenHash = preauth.TokenHash - if sub.ID == "" { + if sub.ID == "" || sub.UserID == "" { log.Printf("error updating subscription: Subscription ID is required") return ErrInvalidSubscription } @@ -101,7 +118,7 @@ func (s *Service) UpdateSubscription(ctx context.Context, sub model.Subscription err = s.Http.SignupWebhook(subID) if err != nil { - log.Printf("error creating user: %s", err.Error()) + log.Printf("error updating subscription: %s", err.Error()) return ErrSignupWebhook } @@ -117,3 +134,67 @@ func (s *Service) DeleteSubscription(ctx context.Context, userID string) error { return nil } + +func (s *Service) AddPASession(ctx context.Context, paSession model.PASession) error { + data, err := json.Marshal(paSession) + if err != nil { + log.Println("failed to marshal pre-auth session to JSON:", err) + return err + } + + err = s.Cache.Set(ctx, "pasession_"+paSession.ID, string(data), 15*time.Minute) + if err != nil { + log.Println("failed to set pre-auth session in Redis:", err) + return err + } + + return nil +} + +func (s *Service) GetPASession(ctx context.Context, id string) (model.PASession, error) { + data, err := s.Cache.Get(ctx, "pasession_"+id) + if err != nil { + log.Println("failed to get pre-auth session from Redis:", err) + return model.PASession{}, err + } + + var paSession model.PASession + err = json.Unmarshal([]byte(data), &paSession) + if err != nil { + log.Println("failed to unmarshal pre-auth session JSON:", err) + return model.PASession{}, err + } + + return paSession, nil +} + +func (s *Service) RotatePASessionId(ctx context.Context, id string) (string, error) { + paSession, err := s.GetPASession(ctx, id) + if err != nil { + log.Println("failed to get pre-auth session for rotation:", err) + return "", err + } + + newID := uuid.New().String() + paSession.ID = newID + + data, err := json.Marshal(paSession) + if err != nil { + log.Println("failed to marshal rotated pre-auth session to JSON:", err) + return "", err + } + + err = s.Cache.Set(ctx, "pasession_"+newID, string(data), 15*time.Minute) + if err != nil { + log.Println("failed to set rotated pre-auth session in Redis:", err) + return "", err + } + + err = s.Cache.Del(ctx, "pasession_"+id) + if err != nil { + log.Println("failed to delete old pre-auth session from Redis:", err) + return "", err + } + + return newID, nil +} diff --git a/api/internal/service/user.go b/api/internal/service/user.go index 4a969f2..0d361fe 100644 --- a/api/internal/service/user.go +++ b/api/internal/service/user.go @@ -2,7 +2,9 @@ package service import ( "context" + "crypto/sha256" "encoding/base32" + "encoding/base64" "errors" "log" "strings" @@ -91,7 +93,7 @@ func (s *Service) GetUserByEmail(ctx context.Context, email string) (model.User, return user, nil } -func (s *Service) GetUnfinishedSignupOrPostUser(ctx context.Context, user model.User, subID string, preauthID string, preauthTokenHash string) (model.User, error) { +func (s *Service) GetUnfinishedSignupOrPostUser(ctx context.Context, user model.User, subID string, sessionId string) (model.User, error) { email := user.Email pass := user.PasswordPlain user, err := s.Store.GetUserByEmailUnfinishedSignup(ctx, email) @@ -101,7 +103,7 @@ func (s *Service) GetUnfinishedSignupOrPostUser(ctx context.Context, user model. PasswordPlain: pass, IsActive: false, } - err = s.PostUser(ctx, user, subID, preauthID, preauthTokenHash) + err = s.PostUser(ctx, user, subID, sessionId) if err != nil { log.Printf("error creating user: %s", err.Error()) return model.User{}, ErrPostUser @@ -135,14 +137,25 @@ func (s *Service) SaveUser(ctx context.Context, user model.User) error { return nil } -func (s *Service) PostUser(ctx context.Context, user model.User, subID string, preauthID string, preauthTokenHash string) error { - preauth, err := s.Http.GetPreauth(preauthID) +func (s *Service) PostUser(ctx context.Context, user model.User, subID string, sessionId string) error { + paSession, err := s.GetPASession(ctx, sessionId) + if err != nil { + log.Printf("error creating user: %s", err.Error()) + return ErrPASessionNotFound + } + + preauthId := paSession.PreauthId + token := paSession.Token + tokenHash := sha256.Sum256([]byte(token)) + tokenHashStr := base64.StdEncoding.EncodeToString(tokenHash[:]) + + preauth, err := s.Http.GetPreauth(preauthId) if err != nil { log.Printf("error creating user: %s", err.Error()) return ErrInvalidSubscription } - if preauth.TokenHash != preauthTokenHash { + if preauth.TokenHash != tokenHashStr { log.Printf("error creating user: Token hash does not match") return ErrTokenHashMismatch } diff --git a/api/internal/transport/api/req.go b/api/internal/transport/api/req.go index fd5e779..c4931c6 100644 --- a/api/internal/transport/api/req.go +++ b/api/internal/transport/api/req.go @@ -11,25 +11,19 @@ type EmailReq struct { } type SignupUserReq struct { - Email string `json:"email" validate:"required,emailx"` - Password string `json:"password" validate:"password"` - SubID string `json:"subid" validate:"required,uuid"` - PreauthID string `json:"preauthid" validate:"required,uuid"` - PreauthTokenHash string `json:"preauthtokenhash" validate:"required"` + Email string `json:"email" validate:"required,emailx"` + Password string `json:"password" validate:"password"` + SubID string `json:"subid" validate:"required,uuid"` } type SignupEmailReq struct { - Email string `json:"email" validate:"required,emailx"` - SubID string `json:"subid" validate:"required,uuid"` - PreauthID string `json:"preauthid" validate:"required,uuid"` - PreauthTokenHash string `json:"preauthtokenhash" validate:"required"` + Email string `json:"email" validate:"required,emailx"` + SubID string `json:"subid" validate:"required,uuid"` } type SubscriptionReq struct { - ID string `json:"id" validate:"required,uuid"` - SubID string `json:"subid" validate:"required,uuid"` - PreauthID string `json:"preauthid" validate:"required,uuid"` - PreauthTokenHash string `json:"preauthtokenhash" validate:"required"` + ID string `json:"id" validate:"required,uuid"` + SubID string `json:"subid" validate:"required,uuid"` } type AliasReq struct { @@ -77,3 +71,13 @@ type ActivateReq struct { type TotpReq struct { OTP string `json:"otp" validate:"required,min=6,max=8"` } + +type PASessionReq struct { + ID string `json:"id" validate:"required,uuid"` + PreauthId string `json:"preauth_id" validate:"required,uuid"` + Token string `json:"token" validate:"required"` +} + +type RotatePASessionReq struct { + ID string `json:"sessionid" validate:"required,uuid"` +} diff --git a/api/internal/transport/api/routes.go b/api/internal/transport/api/routes.go index afba0e6..abeab80 100644 --- a/api/internal/transport/api/routes.go +++ b/api/internal/transport/api/routes.go @@ -27,12 +27,17 @@ func (h *Handler) SetupRoutes(cfg config.APIConfig) { h.Server.Post("/v1/login", limit.New(5, 10*time.Minute), h.Login) h.Server.Post("/v1/initiatepasswordreset", limiter.New(), h.InitiatePasswordReset) h.Server.Put("/v1/resetpassword", limiter.New(), h.ResetPassword) + h.Server.Put("/v1/rotatepasession", limiter.New(), h.RotatePASession) h.Server.Post("/v1/register/begin", limiter.New(), h.BeginRegistration) h.Server.Post("/v1/register/finish", limiter.New(), h.FinishRegistration) h.Server.Post("/v1/login/begin", limiter.New(), h.BeginLogin) h.Server.Post("/v1/login/finish", limiter.New(), h.FinishLogin) + session := h.Server.Group("/v1/pasession") + session.Use(auth.NewPSK(cfg)) + session.Post("/add", h.AddPASession) + v1 := h.Server.Group("/v1") v1.Use(auth.New(cfg, h.Cache, h.Service)) diff --git a/api/internal/transport/api/subscription.go b/api/internal/transport/api/subscription.go index f699beb..f043031 100644 --- a/api/internal/transport/api/subscription.go +++ b/api/internal/transport/api/subscription.go @@ -11,11 +11,14 @@ import ( var ( UpdateSubscriptionSuccess = "Subscription updated successfully." AddSubscriptionSuccess = "Subscription added successfully." + InvalidPASessionId = "Invalid pre-auth session ID." ) type SubscriptionService interface { GetSubscription(context.Context, string) (model.Subscription, error) - UpdateSubscription(context.Context, model.Subscription, string, string, string) error + UpdateSubscription(context.Context, model.Subscription, string, string) error + AddPASession(context.Context, model.PASession) error + RotatePASessionId(context.Context, string) (string, error) } // @Summary Get subscription @@ -51,6 +54,9 @@ func (h *Handler) GetSubscription(c *fiber.Ctx) error { // @Failure 400 {object} ErrorRes // @Router /subscription/update [put] func (h *Handler) UpdateSubscription(c *fiber.Ctx) error { + sessionId := c.Cookies(auth.PA_SESSION_COOKIE) + userID := auth.GetUserID(c) + req := SubscriptionReq{} err := c.BodyParser(&req) if err != nil { @@ -68,8 +74,14 @@ func (h *Handler) UpdateSubscription(c *fiber.Ctx) error { sub := model.Subscription{} sub.ID = req.ID + sub, err = h.Service.GetSubscription(c.Context(), userID) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": err.Error(), + }) + } - err = h.Service.UpdateSubscription(c.Context(), sub, req.SubID, req.PreauthID, req.PreauthTokenHash) + err = h.Service.UpdateSubscription(c.Context(), sub, req.SubID, sessionId) if err != nil { return c.Status(400).JSON(fiber.Map{ "error": err.Error(), @@ -80,3 +92,82 @@ func (h *Handler) UpdateSubscription(c *fiber.Ctx) error { "message": UpdateSubscriptionSuccess, }) } + +// @Summary Add pre-auth session +// @Description Add pre-auth session +// @Tags subscription +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body PASessionReq true "Pre-auth session request" +// @Success 200 {object} SuccessRes +// @Failure 400 {object} ErrorRes +// @Router /sub/session [post] +func (h *Handler) AddPASession(c *fiber.Ctx) error { + req := PASessionReq{} + err := c.BodyParser(&req) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": ErrInvalidRequest, + }) + } + + err = h.Validator.Struct(req) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": ErrInvalidRequest, + }) + } + + paSession := model.PASession{ + ID: req.ID, + PreauthId: req.PreauthId, + Token: req.Token, + } + + err = h.Service.AddPASession(c.Context(), paSession) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return nil +} + +// @Summary Rotate pre-auth session ID +// @Description Rotate pre-auth session ID +// @Tags subscription +// @Accept json +// @Produce json +// @Param body body RotatePASessionReq true "Rotate pre-auth session request" +// @Success 200 {object} SuccessRes +// @Failure 400 {object} ErrorRes +// @Router /rotatepasession [put] +func (h *Handler) RotatePASession(c *fiber.Ctx) error { + req := RotatePASessionReq{} + err := c.BodyParser(&req) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": InvalidPASessionId, + }) + } + + err = h.Validator.Struct(req) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": InvalidPASessionId, + }) + } + + newID, err := h.Service.RotatePASessionId(c.Context(), req.ID) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": InvalidPASessionId, + }) + } + + c.Cookie(auth.NewCookiePASession(newID)) + + return c.SendStatus(fiber.StatusOK) +} diff --git a/api/internal/transport/api/user.go b/api/internal/transport/api/user.go index fcb19ab..7f60c4f 100644 --- a/api/internal/transport/api/user.go +++ b/api/internal/transport/api/user.go @@ -37,7 +37,7 @@ type UserService interface { GetUserByCredentials(context.Context, string, string) (model.User, error) GetUserByPassword(context.Context, string, string) (model.User, error) GetUserByEmail(context.Context, string) (model.User, error) - GetUnfinishedSignupOrPostUser(context.Context, model.User, string, string, string) (model.User, error) + GetUnfinishedSignupOrPostUser(context.Context, model.User, string, string) (model.User, error) SaveUser(context.Context, model.User) error DeleteUserRequest(context.Context, string) (string, error) DeleteUser(context.Context, string, string) error @@ -65,6 +65,9 @@ type UserService interface { // @Failure 400 {object} ErrorRes // @Router /register [post] func (h *Handler) Register(c *fiber.Ctx) error { + // Get session ID from cookie + sessionId := c.Cookies(auth.PA_SESSION_COOKIE) + // Parse the request req := SignupUserReq{} err := c.BodyParser(&req) @@ -90,7 +93,7 @@ func (h *Handler) Register(c *fiber.Ctx) error { } // Get unfinished signup user or create new user - user, err = h.Service.GetUnfinishedSignupOrPostUser(c.Context(), user, req.SubID, req.PreauthID, req.PreauthTokenHash) + user, err = h.Service.GetUnfinishedSignupOrPostUser(c.Context(), user, req.SubID, sessionId) if err != nil { return c.Status(400).JSON(fiber.Map{ "error": err.Error(), diff --git a/api/internal/transport/api/webauthn.go b/api/internal/transport/api/webauthn.go index f5d2da2..4b230bb 100644 --- a/api/internal/transport/api/webauthn.go +++ b/api/internal/transport/api/webauthn.go @@ -51,6 +51,9 @@ type CredentialService interface { // @Failure 400 {object} ErrorRes // @Router /register/begin [post] func (h *Handler) BeginRegistration(c *fiber.Ctx) error { + // Get session ID from cookie + sessionId := c.Cookies(auth.PA_SESSION_COOKIE) + // Parse the request req := SignupEmailReq{} err := c.BodyParser(&req) @@ -75,7 +78,7 @@ func (h *Handler) BeginRegistration(c *fiber.Ctx) error { } // Get unfinished signup user or create new user - user, err = h.Service.GetUnfinishedSignupOrPostUser(c.Context(), user, req.SubID, req.PreauthID, req.PreauthTokenHash) + user, err = h.Service.GetUnfinishedSignupOrPostUser(c.Context(), user, req.SubID, sessionId) if err != nil { return c.Status(400).JSON(fiber.Map{ "error": err.Error(), diff --git a/app/src/api/subscription.ts b/app/src/api/subscription.ts index a45f189..23c25c4 100644 --- a/app/src/api/subscription.ts +++ b/app/src/api/subscription.ts @@ -3,4 +3,5 @@ import { api } from './api' export const subscriptionApi = { get: () => api.get('/sub'), update: (data: any) => api.put('/sub/update', data), + rotateSessionId: (data: any) => api.put('/rotatepasession', data), } \ No newline at end of file diff --git a/app/src/components/AccountSubscription.vue b/app/src/components/AccountSubscription.vue index ad82f6d..c89af55 100644 --- a/app/src/components/AccountSubscription.vue +++ b/app/src/components/AccountSubscription.vue @@ -73,17 +73,15 @@ import events from '../events.ts' const sub = ref({ id: '', - active_until: '', - is_active: false, updated_at: '', - is_grace_period: false, + active_until: '', + status: '', outage: false, }) const error = ref('') const email = ref(localStorage.getItem('email')) const subid = ref('') -const preauthid = ref('') -const preauthtokenhash = ref('') +const sessionid = ref('') const currentRoute = useRoute() const syncing = ref(false) @@ -93,13 +91,13 @@ const getSubscription = async () => { sub.value = res.data } catch (err) { if (axios.isAxiosError(err)) { - error.value = err.message + error.value = err.response?.data.error || err.message } } } const updateSubscription = async () => { - if (!subid.value || !preauthid.value || !preauthtokenhash.value) { + if (!subid.value) { return } @@ -108,13 +106,32 @@ const updateSubscription = async () => { await subscriptionApi.update({ id: sub.value.id, subid: subid.value, - preauthid: preauthid.value, - preauthtokenhash: preauthtokenhash.value, }) await getSubscription() } catch (err) { if (axios.isAxiosError(err)) { - error.value = err.message + error.value = err.response?.data.error || err.message + } + } finally { + syncing.value = false + } +} + +const rotateSessionId = async () => { + if (!sessionid.value) { + return + } + + syncing.value = true + try { + await subscriptionApi.rotateSessionId({ + sessionid: sessionid.value, + }) + await getSubscription() + await updateSubscription() + } catch (err) { + if (axios.isAxiosError(err)) { + error.value = err.response?.data.error || err.message } } finally { syncing.value = false @@ -122,15 +139,15 @@ const updateSubscription = async () => { } const isActive = () => { - return sub.value.active_until > new Date().toISOString() + return sub.value.status === 'active' || sub.value.status === 'grace_period' } const isLimited = () => { - return sub.value.is_grace_period && !isActive() + return sub.value.status === 'limited_access' } const isPendingDelete = () => { - return !sub.value.is_grace_period && !isActive() + return sub.value.status === 'pending_delete' } const activeUntilDate = () => { @@ -154,23 +171,17 @@ const parseParams = () => { const q = route.query const first = (v: unknown) => typeof v === 'string' ? v : Array.isArray(v) ? v[0] : '' subid.value = first(q.subid) || (route.params.subid as string) || '' - preauthid.value = first(q.preauthid) || (route.params.preauthid as string) || '' - preauthtokenhash.value = first(q.preauthtokenhash) || (route.params.preauthtokenhash as string) || '' - preauthtokenhash.value = preauthtokenhash.value.replace(/ /g, '+') + sessionid.value = first(q.sessionid) || (route.params.sessionid as string) || '' if (!subid.value || !subid.value.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)) { return } - if (!preauthid.value || !preauthid.value.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)) { - return - } - - if (!preauthtokenhash.value) { + if (!sessionid.value || !sessionid.value.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)) { return } - updateSubscription() + rotateSessionId() } onMounted(() => { diff --git a/app/src/components/AccountSubscriptionStatus.vue b/app/src/components/AccountSubscriptionStatus.vue index 31f4093..0687db7 100644 --- a/app/src/components/AccountSubscriptionStatus.vue +++ b/app/src/components/AccountSubscriptionStatus.vue @@ -1,5 +1,5 @@