From 9234826238ff301bcb20c42ebd0cc5527914f7af Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Fri, 12 Sep 2025 10:00:02 +0200 Subject: [PATCH 01/63] feat(model): update subscription.go --- api/internal/model/subscription.go | 18 ++++++++++-------- api/internal/model/subscription_test.go | 2 +- api/internal/service/alias.go | 2 +- api/internal/service/processor.go | 2 +- api/internal/service/recipient.go | 4 ++-- api/internal/service/subscription.go | 2 -- api/internal/service/user.go | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/api/internal/model/subscription.go b/api/internal/model/subscription.go index 3a006e1..ff5e12e 100644 --- a/api/internal/model/subscription.go +++ b/api/internal/model/subscription.go @@ -17,16 +17,18 @@ var ( ) type Subscription struct { - ID string `gorm:"unique" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"-"` - UserID string `json:"-"` - Type SubscriptionType `json:"type"` - ActiveUntil time.Time `json:"active_until"` + ID string `gorm:"unique" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"-"` + UserID string `json:"-"` + ActiveUntil time.Time `json:"active_until"` + IsActive bool `json:"is_active"` + Tier string `json:"tier"` + TokenHash string `json:"-"` } -func (s *Subscription) IsActive() bool { - return s.ActiveUntil.After(time.Now()) +func (s *Subscription) IsActiveCheck() bool { + return s.ActiveUntil.After(time.Now()) || s.IsActive } func (s *Subscription) IsActiveWithGracePeriod(days int) bool { diff --git a/api/internal/model/subscription_test.go b/api/internal/model/subscription_test.go index db337cd..52cedaf 100644 --- a/api/internal/model/subscription_test.go +++ b/api/internal/model/subscription_test.go @@ -33,7 +33,7 @@ func TestSubscription_IsActive(t *testing.T) { s := &Subscription{ ActiveUntil: tt.activeUntil, } - if got := s.IsActive(); got != tt.want { + if got := s.IsActiveCheck(); got != tt.want { t.Errorf("Subscription.IsActive() = %v, want %v", got, tt.want) } }) diff --git a/api/internal/service/alias.go b/api/internal/service/alias.go index 8073c0b..b9b089e 100644 --- a/api/internal/service/alias.go +++ b/api/internal/service/alias.go @@ -84,7 +84,7 @@ func (s *Service) PostAlias(ctx context.Context, alias model.Alias, format strin return model.Alias{}, ErrPostAlias } - if !sub.IsActive() { + if !sub.IsActiveCheck() { log.Println("error creating alias: subscription is not active") return model.Alias{}, ErrPostAlias } diff --git a/api/internal/service/processor.go b/api/internal/service/processor.go index 572bab7..0059747 100644 --- a/api/internal/service/processor.go +++ b/api/internal/service/processor.go @@ -45,7 +45,7 @@ func (s *Service) ProcessMessage(data []byte) error { } // Reply | Send - if relayType != model.Forward && !sub.IsActive() { + if relayType != model.Forward && !sub.IsActiveCheck() { log.Println("inactive subscription for reply/send") continue } diff --git a/api/internal/service/recipient.go b/api/internal/service/recipient.go index 7c1d6ab..1730b24 100644 --- a/api/internal/service/recipient.go +++ b/api/internal/service/recipient.go @@ -84,7 +84,7 @@ func (s *Service) PostRecipient(ctx context.Context, recipient model.Recipient) return ErrPostRecipient } - if !sub.IsActive() { + if !sub.IsActiveCheck() { log.Println("error creating recipient: subscription is not active") return ErrPostRecipient } @@ -182,7 +182,7 @@ func (s *Service) UpdateRecipient(ctx context.Context, recipient model.Recipient return ErrUpdateRecipient } - if !sub.IsActive() { + if !sub.IsActiveCheck() { log.Println("error updating recipient: subscription is not active") return ErrUpdateRecipient } diff --git a/api/internal/service/subscription.go b/api/internal/service/subscription.go index 1e07d2d..3c01897 100644 --- a/api/internal/service/subscription.go +++ b/api/internal/service/subscription.go @@ -42,7 +42,6 @@ func (s *Service) PostSubscription(ctx context.Context, userID string, subID str } sub := model.Subscription{ - Type: model.Managed, UserID: userID, ActiveUntil: activeUntilTime, } @@ -79,7 +78,6 @@ func (s *Service) AddSubscription(ctx context.Context, subscription model.Subscr } func (s *Service) UpdateSubscription(ctx context.Context, subscription model.Subscription) error { - subscription.Type = model.Managed err := s.Store.UpdateSubscription(ctx, subscription) if err != nil { log.Printf("error updating subscription: %s", err.Error()) diff --git a/api/internal/service/user.go b/api/internal/service/user.go index 9bba0f7..43940dc 100644 --- a/api/internal/service/user.go +++ b/api/internal/service/user.go @@ -256,7 +256,7 @@ func (s *Service) ActivateUser(ctx context.Context, ID string, otp string) error return nil } - if !sub.IsActive() { + if !sub.IsActiveCheck() { log.Println("error creating recipient: subscription is not active") return nil } From 4b6ccfce9592e14d075015aa00d1672d32f0ac3c Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Fri, 12 Sep 2025 10:10:49 +0200 Subject: [PATCH 02/63] feat(api): update config.go --- api/.env.sample | 98 +++++++++++++++--------------- api/config/config.go | 4 ++ api/internal/model/preauth.go | 11 ++++ api/internal/model/subscription.go | 9 +-- 4 files changed, 66 insertions(+), 56 deletions(-) create mode 100644 api/internal/model/preauth.go diff --git a/api/.env.sample b/api/.env.sample index 86a1c96..d1a3637 100644 --- a/api/.env.sample +++ b/api/.env.sample @@ -1,52 +1,54 @@ -FQDN="localhost" -API_NAME="Service Name" -API_PORT="3000" -API_ALLOW_ORIGIN="http://localhost:3001" -TOKEN_SECRET="secret" +FQDN=localhost +API_NAME=Service Name +API_PORT=3000 +API_ALLOW_ORIGIN=http://localhost:3001 +TOKEN_SECRET=secret TOKEN_EXPIRATION=60m -PSK="" -PSK_ALLOW_ORIGIN="http://localhost:3001" -DOMAINS="example1.net,example2.com" -LOG_FILE="/var/log/api.log" -BASIC_AUTH_USER="" -BASIC_AUTH_PASSWORD="" -NET_SUBNET="" -NET_GATEWAY="" -SIGNUP_WEBHOOK_URL="" -SIGNUP_WEBHOOK_PSK="" +PSK= +PSK_ALLOW_ORIGIN=http://localhost:3001 +DOMAINS=example1.net,example2.com +LOG_FILE=/var/log/api.log +BASIC_AUTH_USER= +BASIC_AUTH_PASSWORD= +NET_SUBNET= +NET_GATEWAY= +SIGNUP_WEBHOOK_URL= +SIGNUP_WEBHOOK_PSK= +PREAUTH_URL= +PREAUTH_PSK= -APP_PORT="3001" +APP_PORT=3001 -DB_HOSTS="db" -DB_PORT="3306" -DB_NAME="email" -DB_USER="email" -DB_PASSWORD="email" -DB_ROOT_USER="root" -DB_ROOT_PASSWORD="root" +DB_HOSTS=db +DB_PORT=3306 +DB_NAME=email +DB_USER=email +DB_PASSWORD=email +DB_ROOT_USER=root +DB_ROOT_PASSWORD=root -REDIS_ADDR="redis:6379" -REDIS_ADDRS="" -REDIS_MASTER_NAME="" -REDIS_USERNAME="" -REDIS_PASSWORD="" -REDIS_FAILOVER_USERNAME="" -REDIS_FAILOVER_PASSWORD="" -REDIS_TLS_ENABLED="false" -REDIS_CERT_FILE="" -REDIS_KEY_FILE="" -REDIS_CA_CERT_FILE="" -REDIS_TLS_INSECURE_SKIP_VERIFY="false" +REDIS_ADDR=redis:6379 +REDIS_ADDRS= +REDIS_MASTER_NAME= +REDIS_USERNAME= +REDIS_PASSWORD= +REDIS_FAILOVER_USERNAME= +REDIS_FAILOVER_PASSWORD= +REDIS_TLS_ENABLED=false +REDIS_CERT_FILE= +REDIS_KEY_FILE= +REDIS_CA_CERT_FILE= +REDIS_TLS_INSECURE_SKIP_VERIFY=false -SMTP_CLIENT_HOST="smtp.example.net" -SMTP_CLIENT_PORT="2525" -SMTP_CLIENT_USER="" -SMTP_CLIENT_PASSWORD="" -SMTP_CLIENT_SENDER="from@example.net" -SMTP_CLIENT_SENDER_NAME="From Name" +SMTP_CLIENT_HOST=smtp.example.net +SMTP_CLIENT_PORT=2525 +SMTP_CLIENT_USER= +SMTP_CLIENT_PASSWORD= +SMTP_CLIENT_SENDER=from@example.net +SMTP_CLIENT_SENDER_NAME=From Name OTP_EXPIRATION=15m -SUBSCRIPTION_TYPE="" +SUBSCRIPTION_TYPE= MAX_CREDENTIALS=10 MAX_RECIPIENTS=10 MAX_DAILY_ALIASES=100 @@ -57,10 +59,10 @@ ACCOUNT_GRACE_PERIOD_DAYS=194 ID_LIMITER_MAX=5 ID_LIMITER_EXPIRATION=60m -BACKUP_FILENAME="backup" -BACKUP_CRON_EXPRESSION="0 0 29 2 1" +BACKUP_FILENAME=backup +BACKUP_CRON_EXPRESSION=0 0 29 2 1 BACKUP_RETENTION_DAYS=7 -GPG_PASSPHRASE="" -AWS_S3_BUCKET_NAME="" -AWS_ACCESS_KEY_ID="" -AWS_SECRET_ACCESS_KEY="" +GPG_PASSPHRASE= +AWS_S3_BUCKET_NAME= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= diff --git a/api/config/config.go b/api/config/config.go index 3d9be5b..263ab5d 100644 --- a/api/config/config.go +++ b/api/config/config.go @@ -22,6 +22,8 @@ type APIConfig struct { BasicAuthPassword string SignupWebhookURL string SignupWebhookPSK string + PreauthURL string + PreauthPSK string } type DBConfig struct { @@ -155,6 +157,8 @@ func New() (Config, error) { BasicAuthPassword: os.Getenv("BASIC_AUTH_PASSWORD"), SignupWebhookURL: os.Getenv("SIGNUP_WEBHOOK_URL"), SignupWebhookPSK: os.Getenv("SIGNUP_WEBHOOK_PSK"), + PreauthURL: os.Getenv("PREAUTH_URL"), + PreauthPSK: os.Getenv("PREAUTH_PSK"), }, DB: DBConfig{ Hosts: dbHosts, diff --git a/api/internal/model/preauth.go b/api/internal/model/preauth.go new file mode 100644 index 0000000..9d33b88 --- /dev/null +++ b/api/internal/model/preauth.go @@ -0,0 +1,11 @@ +package model + +import "time" + +type Preauth struct { + ID string `json:"id"` + TokenHash string `json:"token_hash"` + IsActive bool `json:"is_active"` + ActiveUntil time.Time `json:"active_until"` + Tier string `json:"tier"` +} diff --git a/api/internal/model/subscription.go b/api/internal/model/subscription.go index ff5e12e..626b023 100644 --- a/api/internal/model/subscription.go +++ b/api/internal/model/subscription.go @@ -5,13 +5,6 @@ import ( "time" ) -type SubscriptionType string - -const ( - Free SubscriptionType = "Free" - Managed SubscriptionType = "Managed" -) - var ( ErrDuplicateSubscription = errors.New("subscription already exists") ) @@ -24,7 +17,7 @@ type Subscription struct { ActiveUntil time.Time `json:"active_until"` IsActive bool `json:"is_active"` Tier string `json:"tier"` - TokenHash string `json:"-"` + TokenHash string `gorm:"unique" json:"-"` } func (s *Subscription) IsActiveCheck() bool { From 66b1e69467286d8df096448ae26f2c73923984c3 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Fri, 12 Sep 2025 10:17:41 +0200 Subject: [PATCH 03/63] feat(client): update http.go --- api/internal/client/http/http.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/api/internal/client/http/http.go b/api/internal/client/http/http.go index bcef7fd..3e8a9ae 100644 --- a/api/internal/client/http/http.go +++ b/api/internal/client/http/http.go @@ -1,12 +1,14 @@ package http import ( + "encoding/json" "errors" "log" "net/http" "github.com/gofiber/fiber/v2" "ivpn.net/email/api/config" + "ivpn.net/email/api/internal/model" ) type Http struct { @@ -39,3 +41,29 @@ func (h Http) SignupWebhook(subID string) error { return nil } + +func (h Http) GetPreauth(ID string) (model.Preauth, error) { + req := fiber.Get(h.Cfg.PreauthURL + "/" + ID) + req.Set("Content-Type", "application/json") + req.Set("Accept", "application/json") + req.Set("Authorization", "Bearer "+h.Cfg.PreauthPSK) + + var preauth model.Preauth + status, body, err := req.Bytes() + if err != nil { + log.Printf("Error calling preauth service: %v", err) + return model.Preauth{}, errors.New("error calling preauth service") + } + + if status != http.StatusOK { + log.Printf("Error calling preauth service, status: %d", status) + return model.Preauth{}, errors.New("error response from preauth service") + } + + if err := json.Unmarshal(body, &preauth); err != nil { + log.Printf("Error parsing preauth response: %v", err) + return model.Preauth{}, errors.New("error parsing preauth response") + } + + return preauth, nil +} From a35970f7c4357c67435504870827da28431b27c0 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Fri, 12 Sep 2025 11:26:33 +0200 Subject: [PATCH 04/63] feat(service): update user.go --- api/internal/service/subscription.go | 25 ++++++++----------------- api/internal/service/user.go | 17 ++++++++++++----- api/internal/transport/api/req.go | 14 +++++++++----- api/internal/transport/api/user.go | 5 ++--- api/internal/transport/api/webauthn.go | 2 +- 5 files changed, 32 insertions(+), 31 deletions(-) diff --git a/api/internal/service/subscription.go b/api/internal/service/subscription.go index 3c01897..d313ea6 100644 --- a/api/internal/service/subscription.go +++ b/api/internal/service/subscription.go @@ -5,8 +5,8 @@ import ( "errors" "log" - "github.com/araddon/dateparse" "github.com/go-sql-driver/mysql" + "github.com/google/uuid" "ivpn.net/email/api/internal/model" ) @@ -34,20 +34,17 @@ func (s *Service) GetSubscription(ctx context.Context, userID string) (model.Sub return subscription, nil } -func (s *Service) PostSubscription(ctx context.Context, userID string, subID string, activeUntil string) error { - activeUntilTime, err := dateparse.ParseAny(activeUntil) - if err != nil { - log.Printf("error posting subscription: %s", err.Error()) - return ErrPostSubscription - } - +func (s *Service) PostSubscription(ctx context.Context, userID string, preauth model.Preauth) error { sub := model.Subscription{ UserID: userID, - ActiveUntil: activeUntilTime, + ActiveUntil: preauth.ActiveUntil, + IsActive: preauth.IsActive, + Tier: preauth.Tier, + TokenHash: preauth.TokenHash, } - sub.ID = subID + sub.ID = uuid.New().String() - err = s.Store.PostSubscription(ctx, sub) + err := s.Store.PostSubscription(ctx, sub) if err != nil { log.Printf("error posting subscription: %s", err.Error()) var mysqlErr *mysql.MySQLError @@ -58,12 +55,6 @@ func (s *Service) PostSubscription(ctx context.Context, userID string, subID str } } - err = s.Cache.Del(ctx, "sub_"+subID) - if err != nil { - log.Printf("error deleting subscription: %s", err.Error()) - return ErrPostSubscription - } - return nil } diff --git a/api/internal/service/user.go b/api/internal/service/user.go index 43940dc..b2b1c21 100644 --- a/api/internal/service/user.go +++ b/api/internal/service/user.go @@ -41,6 +41,7 @@ var ( ErrTotpDisable = errors.New("Unable to disable 2FA.") ErrInvalidTOTPCode = errors.New("The 2FA code you entered is invalid.") ErrInvalidSubscription = errors.New("Invalid subscription or signup URL.") + ErrTokenHashMismatch = errors.New("Subscription token hash does not match.") ) type UserStore interface { @@ -90,7 +91,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) (model.User, error) { +func (s *Service) GetUnfinishedSignupOrPostUser(ctx context.Context, user model.User, subID string, preauthID string, preauthTokenHash string) (model.User, error) { email := user.Email pass := user.PasswordPlain user, err := s.Store.GetUserByEmailUnfinishedSignup(ctx, email) @@ -100,7 +101,7 @@ func (s *Service) GetUnfinishedSignupOrPostUser(ctx context.Context, user model. PasswordPlain: pass, IsActive: false, } - err = s.PostUser(ctx, user, subID) + err = s.PostUser(ctx, user, subID, preauthID, preauthTokenHash) if err != nil { log.Printf("error creating user: %s", err.Error()) return model.User{}, ErrPostUser @@ -134,12 +135,18 @@ 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) error { - activeUntil, err := s.Cache.Get(ctx, "sub_"+subID) +func (s *Service) PostUser(ctx context.Context, user model.User, subID string, preauthID string, preauthTokenHash string) error { + preauth, err := s.Http.GetPreauth(preauthID) if err != nil { + log.Printf("error creating user: %s", err.Error()) return ErrInvalidSubscription } + if preauth.TokenHash != preauthTokenHash { + log.Printf("error creating user: Token hash does not match") + return ErrTokenHashMismatch + } + exists, err := s.Store.CheckDuplicateRecipient(ctx, user.Email) if exists || err != nil { log.Printf("error creating user: ErrDuplicateEmail") @@ -165,7 +172,7 @@ func (s *Service) PostUser(ctx context.Context, user model.User, subID string) e } } - err = s.PostSubscription(ctx, user.ID, subID, activeUntil) + err = s.PostSubscription(ctx, user.ID, preauth) if err != nil { log.Printf("error creating user: %s", err.Error()) return ErrPostUser diff --git a/api/internal/transport/api/req.go b/api/internal/transport/api/req.go index 356d3cd..e59f6e8 100644 --- a/api/internal/transport/api/req.go +++ b/api/internal/transport/api/req.go @@ -11,14 +11,18 @@ 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"` + 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,sha256"` } type SignupEmailReq struct { - Email string `json:"email" validate:"required,emailx"` - SubID string `json:"subid" validate:"required,uuid"` + 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,sha256"` } type SubscriptionReq struct { diff --git a/api/internal/transport/api/user.go b/api/internal/transport/api/user.go index d179dfd..fcb19ab 100644 --- a/api/internal/transport/api/user.go +++ b/api/internal/transport/api/user.go @@ -32,13 +32,12 @@ var ( ) type UserService interface { - PostUser(context.Context, model.User, string) error SendUserOTP(context.Context, string) error ActivateUser(context.Context, string, string) error 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) (model.User, error) + GetUnfinishedSignupOrPostUser(context.Context, model.User, string, string, string) (model.User, error) SaveUser(context.Context, model.User) error DeleteUserRequest(context.Context, string) (string, error) DeleteUser(context.Context, string, string) error @@ -91,7 +90,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) + user, err = h.Service.GetUnfinishedSignupOrPostUser(c.Context(), user, req.SubID, req.PreauthID, req.PreauthTokenHash) 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 731ef76..f5d2da2 100644 --- a/api/internal/transport/api/webauthn.go +++ b/api/internal/transport/api/webauthn.go @@ -75,7 +75,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) + user, err = h.Service.GetUnfinishedSignupOrPostUser(c.Context(), user, req.SubID, req.PreauthID, req.PreauthTokenHash) if err != nil { return c.Status(400).JSON(fiber.Map{ "error": err.Error(), From c5334aca0b906149d57fcb1e2b0cf637b47e2756 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Fri, 12 Sep 2025 11:36:14 +0200 Subject: [PATCH 05/63] feat(app): update Signup.vue --- app/src/components/Signup.vue | 30 ++++++++++++++++++++++++++---- app/src/router.ts | 2 +- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/src/components/Signup.vue b/app/src/components/Signup.vue index c86cd6a..da8a8cd 100644 --- a/app/src/components/Signup.vue +++ b/app/src/components/Signup.vue @@ -119,6 +119,9 @@ const apiError = ref('') const isLoading = ref(false) const passkeySupported = ref(false) const subid = ref('') +const preauthid = ref('') +const preauthtokenhash = ref('') +const service = ref('') const validateEmail = () => { emailError.value = !email.value @@ -148,7 +151,9 @@ const register = async () => { const data = { email: email.value, password: password.value, - subid: subid.value + subid: subid.value, + preauthid: preauthid.value, + preauthtokenhash: preauthtokenhash.value } try { @@ -176,7 +181,9 @@ const registerWithPasskey = async () => { const data = { email: emailAuthn.value, - subid: subid.value + subid: subid.value, + preauthid: preauthid.value, + preauthtokenhash: preauthtokenhash.value } try { @@ -199,12 +206,27 @@ const registerWithPasskey = async () => { } } -const parseSubid = () => { +const parseParams = () => { const route = useRoute() subid.value = route.params.subid as string + preauthid.value = route.params.preauthid as string + preauthtokenhash.value = route.params.preauthtokenhash as string + service.value = route.params.service 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}$/)) { window.location.href = '/login' } + + 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}$/)) { + window.location.href = '/login' + } + + if (!preauthtokenhash.value) { + window.location.href = '/login' + } + + if (!service.value) { + window.location.href = '/login' + } } const isLoggedIn = (): boolean => { @@ -217,7 +239,7 @@ onMounted(() => { window.location.href = '/' } - parseSubid() + parseParams() passkeySupported.value = browserSupportsWebAuthn() tabs.autoInit() }) diff --git a/app/src/router.ts b/app/src/router.ts index aa0d9eb..a313e14 100644 --- a/app/src/router.ts +++ b/app/src/router.ts @@ -70,7 +70,7 @@ const routes: RouteRecordRaw[] = [ children: dashboardChildren }, { - path: '/signup/:subid', + path: '/signup', name: `${AppName} - Sign Up`, component: Signup }, From 4800af702e5ac1f8891eecd936ed0edf16bc5b34 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Fri, 12 Sep 2025 11:40:28 +0200 Subject: [PATCH 06/63] feat(service): update user.go --- api/internal/client/http/http.go | 4 ++-- api/internal/service/user.go | 8 ++++---- api/internal/transport/api/req.go | 2 ++ api/internal/transport/api/user.go | 4 ++-- api/internal/transport/api/webauthn.go | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/api/internal/client/http/http.go b/api/internal/client/http/http.go index 3e8a9ae..75044fe 100644 --- a/api/internal/client/http/http.go +++ b/api/internal/client/http/http.go @@ -21,12 +21,12 @@ func New(cfg config.APIConfig) *Http { } } -func (h Http) SignupWebhook(subID string) error { +func (h Http) SignupWebhook(subID string, serviceName string) error { req := fiber.Post(h.Cfg.SignupWebhookURL) req.Set("Content-Type", "application/json") req.Set("Accept", "application/json") req.Set("Authorization", "Bearer "+h.Cfg.SignupWebhookPSK) - req.Body([]byte(`{"uuid": "` + subID + `"}`)) + req.Body([]byte(`{"uuid": "` + subID + `", "service": "` + serviceName + `"}`)) status, _, err := req.Bytes() if err != nil { diff --git a/api/internal/service/user.go b/api/internal/service/user.go index b2b1c21..1e305d8 100644 --- a/api/internal/service/user.go +++ b/api/internal/service/user.go @@ -91,7 +91,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, preauthID string, preauthTokenHash string, serviceName string) (model.User, error) { email := user.Email pass := user.PasswordPlain user, err := s.Store.GetUserByEmailUnfinishedSignup(ctx, email) @@ -101,7 +101,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, preauthID, preauthTokenHash, serviceName) if err != nil { log.Printf("error creating user: %s", err.Error()) return model.User{}, ErrPostUser @@ -135,7 +135,7 @@ 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 { +func (s *Service) PostUser(ctx context.Context, user model.User, subID string, preauthID string, preauthTokenHash string, serviceName string) error { preauth, err := s.Http.GetPreauth(preauthID) if err != nil { log.Printf("error creating user: %s", err.Error()) @@ -184,7 +184,7 @@ func (s *Service) PostUser(ctx context.Context, user model.User, subID string, p return ErrPostUser } - err = s.Http.SignupWebhook(subID) + err = s.Http.SignupWebhook(subID, serviceName) if err != nil { log.Printf("error creating user: %s", err.Error()) return ErrSignupWebhook diff --git a/api/internal/transport/api/req.go b/api/internal/transport/api/req.go index e59f6e8..85e1b2c 100644 --- a/api/internal/transport/api/req.go +++ b/api/internal/transport/api/req.go @@ -16,6 +16,7 @@ type SignupUserReq struct { SubID string `json:"subid" validate:"required,uuid"` PreauthID string `json:"preauthid" validate:"required,uuid"` PreauthTokenHash string `json:"preauthtokenhash" validate:"required,sha256"` + ServiceName string `json:"service" validate:"required"` } type SignupEmailReq struct { @@ -23,6 +24,7 @@ type SignupEmailReq struct { SubID string `json:"subid" validate:"required,uuid"` PreauthID string `json:"preauthid" validate:"required,uuid"` PreauthTokenHash string `json:"preauthtokenhash" validate:"required,sha256"` + ServiceName string `json:"service" validate:"required"` } type SubscriptionReq struct { diff --git a/api/internal/transport/api/user.go b/api/internal/transport/api/user.go index fcb19ab..5a60fe6 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, string, string) (model.User, error) SaveUser(context.Context, model.User) error DeleteUserRequest(context.Context, string) (string, error) DeleteUser(context.Context, string, string) error @@ -90,7 +90,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, req.PreauthID, req.PreauthTokenHash, req.ServiceName) 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..6e8fb44 100644 --- a/api/internal/transport/api/webauthn.go +++ b/api/internal/transport/api/webauthn.go @@ -75,7 +75,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, req.PreauthID, req.PreauthTokenHash, req.ServiceName) if err != nil { return c.Status(400).JSON(fiber.Map{ "error": err.Error(), From b6fd6a51e8e0fd3748d29b54abd4b5e315f4afd4 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Sat, 20 Sep 2025 08:57:55 +0200 Subject: [PATCH 07/63] feat(app): update Signup.vue --- app/src/components/Signup.vue | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/components/Signup.vue b/app/src/components/Signup.vue index da8a8cd..142a474 100644 --- a/app/src/components/Signup.vue +++ b/app/src/components/Signup.vue @@ -208,24 +208,28 @@ const registerWithPasskey = async () => { const parseParams = () => { const route = useRoute() - subid.value = route.params.subid as string - preauthid.value = route.params.preauthid as string - preauthtokenhash.value = route.params.preauthtokenhash as string - service.value = route.params.service as string + 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) || '' + service.value = first(q.service) || (route.params.service as string) || '' + preauthtokenhash.value = preauthtokenhash.value.replace(/ /g, '+') + 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}$/)) { - window.location.href = '/login' + console.error('Invalid or missing subid') } 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}$/)) { - window.location.href = '/login' + console.error('Invalid or missing preauthid') } if (!preauthtokenhash.value) { - window.location.href = '/login' + console.error('Invalid or missing preauthtokenhash') } if (!service.value) { - window.location.href = '/login' + console.error('Invalid or missing service') } } From 4f9c7d8ecfb5bd3a6f728fbbef3b96accfe2e17f Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Sun, 21 Sep 2025 10:40:48 +0200 Subject: [PATCH 08/63] feat(api): update req.go --- api/internal/transport/api/req.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/internal/transport/api/req.go b/api/internal/transport/api/req.go index 85e1b2c..b6fb435 100644 --- a/api/internal/transport/api/req.go +++ b/api/internal/transport/api/req.go @@ -15,7 +15,7 @@ type SignupUserReq struct { 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,sha256"` + PreauthTokenHash string `json:"preauthtokenhash" validate:"required"` ServiceName string `json:"service" validate:"required"` } @@ -23,7 +23,7 @@ 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,sha256"` + PreauthTokenHash string `json:"preauthtokenhash" validate:"required"` ServiceName string `json:"service" validate:"required"` } From 0b41f1ca99ec90d0c27df38574d4886f3d2c14ff Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Sun, 21 Sep 2025 10:48:02 +0200 Subject: [PATCH 09/63] feat(app): update Signup.vue --- app/src/components/Signup.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/components/Signup.vue b/app/src/components/Signup.vue index 142a474..386a955 100644 --- a/app/src/components/Signup.vue +++ b/app/src/components/Signup.vue @@ -153,7 +153,8 @@ const register = async () => { password: password.value, subid: subid.value, preauthid: preauthid.value, - preauthtokenhash: preauthtokenhash.value + preauthtokenhash: preauthtokenhash.value, + service: service.value } try { @@ -183,7 +184,8 @@ const registerWithPasskey = async () => { email: emailAuthn.value, subid: subid.value, preauthid: preauthid.value, - preauthtokenhash: preauthtokenhash.value + preauthtokenhash: preauthtokenhash.value, + service: service.value } try { From 971482d05ce389d5ae366e0616fb44c537c726a1 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Sun, 21 Sep 2025 10:58:04 +0200 Subject: [PATCH 10/63] feat(api): update http.go --- api/internal/client/http/http.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/internal/client/http/http.go b/api/internal/client/http/http.go index 75044fe..81841ec 100644 --- a/api/internal/client/http/http.go +++ b/api/internal/client/http/http.go @@ -28,14 +28,19 @@ func (h Http) SignupWebhook(subID string, serviceName string) error { req.Set("Authorization", "Bearer "+h.Cfg.SignupWebhookPSK) req.Body([]byte(`{"uuid": "` + subID + `", "service": "` + serviceName + `"}`)) - status, _, err := req.Bytes() + // Log request for debugging + log.Printf("Signup webhook request: %+v", req) + + status, res, err := req.Bytes() if err != nil { log.Printf("Error calling signup webhook: %v", err) return errors.New("error calling signup webhook") } if status != http.StatusOK { + // Log response for debugging log.Printf("Error calling signup webhook, status: %d", status) + log.Printf("Signup webhook response: %s", string(res)) return errors.New("error response from signup webhook") } From df001990f056b4a956ff9cbae934f385dfb723b4 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Thu, 25 Sep 2025 18:08:49 +0200 Subject: [PATCH 11/63] feat(api): update http.go --- api/internal/client/http/http.go | 4 ++-- api/internal/service/user.go | 8 ++++---- api/internal/transport/api/req.go | 2 -- api/internal/transport/api/user.go | 4 ++-- api/internal/transport/api/webauthn.go | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/api/internal/client/http/http.go b/api/internal/client/http/http.go index 81841ec..cbf0017 100644 --- a/api/internal/client/http/http.go +++ b/api/internal/client/http/http.go @@ -21,12 +21,12 @@ func New(cfg config.APIConfig) *Http { } } -func (h Http) SignupWebhook(subID string, serviceName string) error { +func (h Http) SignupWebhook(subID string) error { req := fiber.Post(h.Cfg.SignupWebhookURL) req.Set("Content-Type", "application/json") req.Set("Accept", "application/json") req.Set("Authorization", "Bearer "+h.Cfg.SignupWebhookPSK) - req.Body([]byte(`{"uuid": "` + subID + `", "service": "` + serviceName + `"}`)) + req.Body([]byte(`{"uuid": "` + subID + `", "service": "mail"}`)) // Log request for debugging log.Printf("Signup webhook request: %+v", req) diff --git a/api/internal/service/user.go b/api/internal/service/user.go index 1e305d8..b2b1c21 100644 --- a/api/internal/service/user.go +++ b/api/internal/service/user.go @@ -91,7 +91,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, serviceName string) (model.User, error) { +func (s *Service) GetUnfinishedSignupOrPostUser(ctx context.Context, user model.User, subID string, preauthID string, preauthTokenHash string) (model.User, error) { email := user.Email pass := user.PasswordPlain user, err := s.Store.GetUserByEmailUnfinishedSignup(ctx, email) @@ -101,7 +101,7 @@ func (s *Service) GetUnfinishedSignupOrPostUser(ctx context.Context, user model. PasswordPlain: pass, IsActive: false, } - err = s.PostUser(ctx, user, subID, preauthID, preauthTokenHash, serviceName) + err = s.PostUser(ctx, user, subID, preauthID, preauthTokenHash) if err != nil { log.Printf("error creating user: %s", err.Error()) return model.User{}, ErrPostUser @@ -135,7 +135,7 @@ 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, serviceName string) error { +func (s *Service) PostUser(ctx context.Context, user model.User, subID string, preauthID string, preauthTokenHash string) error { preauth, err := s.Http.GetPreauth(preauthID) if err != nil { log.Printf("error creating user: %s", err.Error()) @@ -184,7 +184,7 @@ func (s *Service) PostUser(ctx context.Context, user model.User, subID string, p return ErrPostUser } - err = s.Http.SignupWebhook(subID, serviceName) + err = s.Http.SignupWebhook(subID) if err != nil { log.Printf("error creating user: %s", err.Error()) return ErrSignupWebhook diff --git a/api/internal/transport/api/req.go b/api/internal/transport/api/req.go index b6fb435..2275dff 100644 --- a/api/internal/transport/api/req.go +++ b/api/internal/transport/api/req.go @@ -16,7 +16,6 @@ type SignupUserReq struct { SubID string `json:"subid" validate:"required,uuid"` PreauthID string `json:"preauthid" validate:"required,uuid"` PreauthTokenHash string `json:"preauthtokenhash" validate:"required"` - ServiceName string `json:"service" validate:"required"` } type SignupEmailReq struct { @@ -24,7 +23,6 @@ type SignupEmailReq struct { SubID string `json:"subid" validate:"required,uuid"` PreauthID string `json:"preauthid" validate:"required,uuid"` PreauthTokenHash string `json:"preauthtokenhash" validate:"required"` - ServiceName string `json:"service" validate:"required"` } type SubscriptionReq struct { diff --git a/api/internal/transport/api/user.go b/api/internal/transport/api/user.go index 5a60fe6..fcb19ab 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, string) (model.User, error) + GetUnfinishedSignupOrPostUser(context.Context, model.User, string, string, string) (model.User, error) SaveUser(context.Context, model.User) error DeleteUserRequest(context.Context, string) (string, error) DeleteUser(context.Context, string, string) error @@ -90,7 +90,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, req.ServiceName) + user, err = h.Service.GetUnfinishedSignupOrPostUser(c.Context(), user, req.SubID, req.PreauthID, req.PreauthTokenHash) 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 6e8fb44..f5d2da2 100644 --- a/api/internal/transport/api/webauthn.go +++ b/api/internal/transport/api/webauthn.go @@ -75,7 +75,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, req.ServiceName) + user, err = h.Service.GetUnfinishedSignupOrPostUser(c.Context(), user, req.SubID, req.PreauthID, req.PreauthTokenHash) if err != nil { return c.Status(400).JSON(fiber.Map{ "error": err.Error(), From b56df437dab9496d8fc406b2ad67335ad4d3873f Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Thu, 25 Sep 2025 18:09:56 +0200 Subject: [PATCH 12/63] feat(app): update Signup.vue --- app/src/components/Signup.vue | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/src/components/Signup.vue b/app/src/components/Signup.vue index 386a955..33c7890 100644 --- a/app/src/components/Signup.vue +++ b/app/src/components/Signup.vue @@ -121,7 +121,6 @@ const passkeySupported = ref(false) const subid = ref('') const preauthid = ref('') const preauthtokenhash = ref('') -const service = ref('') const validateEmail = () => { emailError.value = !email.value @@ -153,8 +152,7 @@ const register = async () => { password: password.value, subid: subid.value, preauthid: preauthid.value, - preauthtokenhash: preauthtokenhash.value, - service: service.value + preauthtokenhash: preauthtokenhash.value } try { @@ -184,8 +182,7 @@ const registerWithPasskey = async () => { email: emailAuthn.value, subid: subid.value, preauthid: preauthid.value, - preauthtokenhash: preauthtokenhash.value, - service: service.value + preauthtokenhash: preauthtokenhash.value } try { @@ -215,7 +212,6 @@ const parseParams = () => { 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) || '' - service.value = first(q.service) || (route.params.service as string) || '' preauthtokenhash.value = preauthtokenhash.value.replace(/ /g, '+') 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}$/)) { @@ -229,10 +225,6 @@ const parseParams = () => { if (!preauthtokenhash.value) { console.error('Invalid or missing preauthtokenhash') } - - if (!service.value) { - console.error('Invalid or missing service') - } } const isLoggedIn = (): boolean => { From 579de144a8f62cb325b596164e3945ae87f95cb4 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Thu, 2 Oct 2025 12:44:29 +0200 Subject: [PATCH 13/63] feat(model): update subscription.go --- api/internal/model/subscription.go | 21 +++++++++++++-------- api/internal/service/subscription.go | 6 ++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/api/internal/model/subscription.go b/api/internal/model/subscription.go index 626b023..599670d 100644 --- a/api/internal/model/subscription.go +++ b/api/internal/model/subscription.go @@ -10,14 +10,15 @@ var ( ) type Subscription struct { - ID string `gorm:"unique" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"-"` - UserID string `json:"-"` - ActiveUntil time.Time `json:"active_until"` - IsActive bool `json:"is_active"` - Tier string `json:"tier"` - TokenHash string `gorm:"unique" json:"-"` + ID string `gorm:"unique" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"-"` + UserID string `json:"-"` + ActiveUntil time.Time `json:"active_until"` + IsActive bool `json:"is_active"` + Tier string `json:"tier"` + TokenHash string `gorm:"unique" json:"-"` + IsGracePeriod bool `gorm:"-" json:"is_grace_period"` } func (s *Subscription) IsActiveCheck() bool { @@ -27,3 +28,7 @@ func (s *Subscription) IsActiveCheck() bool { func (s *Subscription) IsActiveWithGracePeriod(days int) bool { return s.ActiveUntil.AddDate(0, 0, days).After(time.Now()) } + +func (s *Subscription) IsGracePeriodCheck(days int) bool { + return !s.IsActiveCheck() && s.IsActiveWithGracePeriod(days) +} diff --git a/api/internal/service/subscription.go b/api/internal/service/subscription.go index d313ea6..cc36fd0 100644 --- a/api/internal/service/subscription.go +++ b/api/internal/service/subscription.go @@ -26,12 +26,14 @@ type SubscriptionStore interface { } func (s *Service) GetSubscription(ctx context.Context, userID string) (model.Subscription, error) { - subscription, err := s.Store.GetSubscription(ctx, userID) + sub, err := s.Store.GetSubscription(ctx, userID) if err != nil { return model.Subscription{}, ErrGetSubscription } - return subscription, nil + sub.IsGracePeriod = sub.IsGracePeriodCheck(s.Cfg.Service.ForwardGracePeriodDays) + + return sub, nil } func (s *Service) PostSubscription(ctx context.Context, userID string, preauth model.Preauth) error { From 60c568826300ac4cf524ffcbe4b6d6484541790c Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Tue, 7 Oct 2025 14:00:00 +0200 Subject: [PATCH 14/63] feat(app): update AccountSubscription.vue --- app/src/components/AccountSubscription.vue | 68 +++++++++++-------- .../components/AccountSubscriptionStatus.vue | 53 ++++++++++++--- 2 files changed, 84 insertions(+), 37 deletions(-) diff --git a/app/src/components/AccountSubscription.vue b/app/src/components/AccountSubscription.vue index 02e011e..5e81262 100644 --- a/app/src/components/AccountSubscription.vue +++ b/app/src/components/AccountSubscription.vue @@ -1,7 +1,7 @@