Expose gateway billing flows from the synced server baseline
The preserved local work adds PopiNewAPI billing checkout, gateway account binding, and matching web console/pricing surfaces on top of the current test-server baseline. During the merge, the start-end video helpers from the deployed baseline were kept alongside the actionGenerate prompt handling from the WIP.
Constraint: Current usable baseline is 7054436, already deployed on the test server.
Rejected: Commit the WIP before syncing the baseline | would have hidden conflicts with the deployed start-end-frame changes.
Confidence: medium
Scope-risk: broad
Directive: Do not deploy this commit to the test server without rechecking gateway credentials and billing checkout behavior in that environment.
Tested: go test ./...
Tested: make build
Tested: cd web && npm run build
This commit is contained in:
@@ -56,6 +56,8 @@ func (r *sqliteRepository) migrate() error {
|
||||
user_json TEXT NOT NULL,
|
||||
token_enc BLOB NOT NULL,
|
||||
token_masked TEXT NOT NULL,
|
||||
gateway_user_id INTEGER,
|
||||
gateway_access_token_enc BLOB,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL
|
||||
);`,
|
||||
@@ -154,6 +156,14 @@ func (r *sqliteRepository) migrate() error {
|
||||
return fmt.Errorf("upgrade sqlite jobs schema: %w", err)
|
||||
}
|
||||
}
|
||||
for _, stmt := range []string{
|
||||
`ALTER TABLE sessions ADD COLUMN gateway_user_id INTEGER;`,
|
||||
`ALTER TABLE sessions ADD COLUMN gateway_access_token_enc BLOB;`,
|
||||
} {
|
||||
if _, err := r.db.Exec(stmt); err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return fmt.Errorf("upgrade sqlite sessions schema: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -194,17 +204,27 @@ func (r *sqliteRepository) CreateSession(upstreamKey string, u user, ttl time.Du
|
||||
|
||||
func (r *sqliteRepository) GetSession(token string) (session, bool, error) {
|
||||
row := r.db.QueryRow(
|
||||
`SELECT user_id, user_json, token_enc, created_at, expires_at
|
||||
`SELECT user_id, user_json, token_enc, gateway_user_id, gateway_access_token_enc, created_at, expires_at
|
||||
FROM sessions WHERE session_id = ?`,
|
||||
token,
|
||||
)
|
||||
var (
|
||||
current session
|
||||
userJSON string
|
||||
tokenEnc []byte
|
||||
createdAt, expiresAt string
|
||||
current session
|
||||
userJSON string
|
||||
tokenEnc []byte
|
||||
gatewayUserID sql.NullInt64
|
||||
gatewayAccessTokenEnc []byte
|
||||
createdAt, expiresAt string
|
||||
)
|
||||
if err := row.Scan(¤t.UserID, &userJSON, &tokenEnc, &createdAt, &expiresAt); errors.Is(err, sql.ErrNoRows) {
|
||||
if err := row.Scan(
|
||||
¤t.UserID,
|
||||
&userJSON,
|
||||
&tokenEnc,
|
||||
&gatewayUserID,
|
||||
&gatewayAccessTokenEnc,
|
||||
&createdAt,
|
||||
&expiresAt,
|
||||
); errors.Is(err, sql.ErrNoRows) {
|
||||
return session{}, false, nil
|
||||
} else if err != nil {
|
||||
return session{}, false, fmt.Errorf("scan session: %w", err)
|
||||
@@ -217,6 +237,16 @@ func (r *sqliteRepository) GetSession(token string) (session, bool, error) {
|
||||
if err != nil {
|
||||
return session{}, false, err
|
||||
}
|
||||
if gatewayUserID.Valid {
|
||||
current.GatewayUserID = int(gatewayUserID.Int64)
|
||||
}
|
||||
if len(gatewayAccessTokenEnc) > 0 {
|
||||
gatewayAccessToken, err := r.decryptSecret(gatewayAccessTokenEnc)
|
||||
if err != nil {
|
||||
return session{}, false, err
|
||||
}
|
||||
current.GatewayAccessToken = gatewayAccessToken
|
||||
}
|
||||
current.Token = token
|
||||
current.UpstreamKey = upstreamKey
|
||||
current.CreatedAt = parseRFC3339(createdAt)
|
||||
@@ -248,9 +278,41 @@ func (r *sqliteRepository) RotateSession(oldToken string, ttl time.Duration) (se
|
||||
if err != nil {
|
||||
return session{}, false, err
|
||||
}
|
||||
if current.GatewayUserID > 0 && strings.TrimSpace(current.GatewayAccessToken) != "" {
|
||||
if err := r.UpdateSessionGatewayBinding(next.Token, current.GatewayUserID, current.GatewayAccessToken); err != nil {
|
||||
return session{}, false, err
|
||||
}
|
||||
next.GatewayUserID = current.GatewayUserID
|
||||
next.GatewayAccessToken = current.GatewayAccessToken
|
||||
}
|
||||
return next, true, nil
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) UpdateSessionGatewayBinding(token string, gatewayUserID int, gatewayAccessToken string) error {
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return errors.New("session token is required")
|
||||
}
|
||||
if gatewayUserID <= 0 {
|
||||
return errors.New("gateway user id is required")
|
||||
}
|
||||
if strings.TrimSpace(gatewayAccessToken) == "" {
|
||||
return errors.New("gateway access token is required")
|
||||
}
|
||||
enc, err := r.encryptSecret(gatewayAccessToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := r.db.Exec(
|
||||
`UPDATE sessions SET gateway_user_id = ?, gateway_access_token_enc = ? WHERE session_id = ?`,
|
||||
gatewayUserID,
|
||||
enc,
|
||||
token,
|
||||
); err != nil {
|
||||
return fmt.Errorf("update session gateway binding: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) CreateJob(record *job) (*job, int, error) {
|
||||
if record == nil {
|
||||
return nil, 0, errors.New("job record is required")
|
||||
|
||||
Reference in New Issue
Block a user