feat: own shared user API tokens

This commit is contained in:
Jakub Zych
2026-10-04 22:15:57 +02:00
parent 8a658903ed
commit 0fe5b91b63
17 changed files with 926 additions and 35 deletions

View File

@@ -0,0 +1,36 @@
package updates
import (
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var apiTokenMigrations = []*gormigrate.Migration{
{
ID: "202610040001_create_user_api_tokens",
Migrate: func(tx *gorm.DB) error {
return tx.Exec(`CREATE TABLE user_api_tokens (
id BIGSERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NULL,
token_hash VARCHAR(64) NOT NULL UNIQUE,
scopes TEXT NULL,
collection_ids TEXT NULL,
expires_at TIMESTAMPTZ NULL,
revoked_at TIMESTAMPTZ NULL,
last_used_at TIMESTAMPTZ NULL,
last_used_ip VARCHAR(64) NULL,
oauth_client_id TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX user_api_tokens_user_id_index ON user_api_tokens (user_id);
CREATE INDEX user_api_tokens_oauth_client_id_index ON user_api_tokens (oauth_client_id)`).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Exec("DROP TABLE IF EXISTS user_api_tokens").Error
},
},
}
func init() { Register(apiTokenMigrations...) }

View File

@@ -0,0 +1,42 @@
package updates
import (
"testing"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/lagoon"
"github.com/go-gormigrate/gormigrate/v2"
)
func TestAPITokenMigration(t *testing.T) {
sqlDB := dedicatedDB(t, "user_api_tokens")
db, err := lagoon.Use(t.Context(), sqlDB)
if err != nil {
t.Fatal(err)
}
m := gormigrate.New(db, &gormigrate.Options{TableName: "summer_migrations_golem15_user", UseTransaction: true}, All())
if err := m.Migrate(); err != nil {
t.Fatal(err)
}
if !db.Migrator().HasTable((&models.APIToken{}).TableName()) {
t.Fatal("user_api_tokens missing")
}
for _, column := range []string{"id", "user_id", "name", "token_hash", "scopes", "collection_ids", "expires_at", "revoked_at", "last_used_at", "last_used_ip", "oauth_client_id", "created_at", "updated_at"} {
if !db.Migrator().HasColumn(&models.APIToken{}, column) {
t.Fatalf("user_api_tokens.%s missing", column)
}
}
var oauthClientType string
if err := db.Raw(`SELECT data_type FROM information_schema.columns WHERE table_name = 'user_api_tokens' AND column_name = 'oauth_client_id'`).Scan(&oauthClientType).Error; err != nil {
t.Fatal(err)
}
if oauthClientType != "text" {
t.Fatalf("oauth_client_id type = %q, want text for legacy OAuth client IDs", oauthClientType)
}
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}
if db.Migrator().HasTable(&models.APIToken{}) {
t.Fatal("user_api_tokens survived rollback")
}
}

View File

@@ -45,6 +45,7 @@ func TestCreateOrganisationsMigration(t *testing.T) {
"202609220006_create_user_throttle",
"202609220007_create_jwt_blacklist",
"202610020001_create_user_groups",
"202610040001_create_user_api_tokens",
}
if len(ids) != len(want) {
t.Fatalf("history = %v", ids)
@@ -55,8 +56,8 @@ func TestCreateOrganisationsMigration(t *testing.T) {
}
}
// user groups, jwt blacklist, throttle, extend-users, then organisations.
for range 5 {
// API tokens, user groups, jwt blacklist, throttle, extend-users, then organisations.
for range 6 {
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}

View File

@@ -54,8 +54,10 @@ func TestUserGroupsMigration(t *testing.T) {
t.Fatalf("users_groups primary key = %q", pk)
}
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
for range 2 { // API tokens, then user groups.
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}
}
if gdb.Migrator().HasTable("user_groups") || gdb.Migrator().HasTable("users_groups") {
t.Fatal("user groups tables survived rollback")

View File

@@ -47,8 +47,8 @@ func TestExtendUsersMigration(t *testing.T) {
if !row.HasSelfSetPassword || row.IsActivated || row.MarketingConsent || row.IsOnboarded {
t.Fatalf("defaults = %+v", row)
}
// user groups, jwt blacklist, throttle, then extend-users.
for range 4 {
// API tokens, user groups, jwt blacklist, throttle, then extend-users.
for range 5 {
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}
@@ -87,8 +87,8 @@ func TestCreateUserThrottleMigration(t *testing.T) {
t.Fatalf("missing column %s", col)
}
}
// user groups, jwt blacklist, then throttle.
for range 3 {
// API tokens, user groups, jwt blacklist, then throttle.
for range 4 {
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}
@@ -119,8 +119,8 @@ func TestCreateJwtBlacklistMigration(t *testing.T) {
t.Fatalf("missing column %s", col)
}
}
// user groups, then jwt blacklist.
for range 2 {
// API tokens, user groups, then jwt blacklist.
for range 3 {
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}