Skip to content

Commit

Permalink
refactor(spx-backend): integrate Casdoor User API
Browse files Browse the repository at this point in the history
Signed-off-by: Aofei Sheng <aofei@aofeisheng.com>
  • Loading branch information
aofei committed Oct 18, 2024
1 parent 189181c commit baa87df
Show file tree
Hide file tree
Showing 16 changed files with 162 additions and 114 deletions.
13 changes: 12 additions & 1 deletion docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ paths:
format: uri
description: URL of the image to process for background removal.
responses:
"201":
"200":
description: Successfully processed the image for background removal.
content:
application/json:
Expand Down Expand Up @@ -932,6 +932,17 @@ components:
examples:
- john
description: Unique username of the user.
displayName:
type: string
examples:
- John Doe
description: Display name of the user.
avatar:
type: string
format: uri
examples:
- https://avatars.githubusercontent.com/u/10137?v=4
description: URL of the user's avatar image.
description:
type: string
examples:
Expand Down
4 changes: 2 additions & 2 deletions spx-backend/cmd/spx-backend/get_projects_list.yap
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import (

ctx := &Context

user, _ := controller.UserFromContext(ctx.Context())
params := controller.NewListProjectsParams()

switch owner := ${owner}; owner {
case "":
if user == nil {
user, ok := controller.UserFromContext(ctx.Context())
if !ok {
replyWithCode(ctx, errorUnauthorized)
return
}
Expand Down
12 changes: 6 additions & 6 deletions spx-backend/cmd/spx-backend/gop_autogen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions spx-backend/internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type Controller struct {
db *gorm.DB
kodo *kodoConfig
aigcClient *aigc.AigcClient
casdoorClient *casdoorsdk.Client
casdoorClient casdoorClient
}

// New creates a new controller.
Expand Down Expand Up @@ -92,8 +92,14 @@ func newKodoConfig(logger *qiniuLog.Logger) *kodoConfig {
}
}

// casdoorClient is the client interface for Casdoor.
type casdoorClient interface {
ParseJwtToken(token string) (*casdoorsdk.Claims, error)
GetUser(name string) (*casdoorsdk.User, error)
}

// newCasdoorClient creates a new [casdoorsdk.Client].
func newCasdoorClient(logger *qiniuLog.Logger) *casdoorsdk.Client {
func newCasdoorClient(logger *qiniuLog.Logger) casdoorClient {
config := &casdoorsdk.AuthConfig{
Endpoint: mustEnv(logger, "GOP_CASDOOR_ENDPOINT"),
ClientId: mustEnv(logger, "GOP_CASDOOR_CLIENTID"),
Expand Down
28 changes: 27 additions & 1 deletion spx-backend/internal/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/goplus/builder/spx-backend/internal/aigc"
"github.com/goplus/builder/spx-backend/internal/log"
"github.com/goplus/builder/spx-backend/internal/model/modeltest"
Expand Down Expand Up @@ -47,13 +48,38 @@ VTh1XIl/IELBoZ+rQXozGA==
t.Setenv("GOP_CASDOOR_APPLICATIONNAME", "fake-application")
}

type mockCasdoorClient struct {
casdoorClient casdoorClient
jwt string
}

func newMockCasdoorClient(casdoorClient casdoorClient, jwt string) *mockCasdoorClient {
return &mockCasdoorClient{casdoorClient: casdoorClient, jwt: jwt}
}

func (m *mockCasdoorClient) ParseJwtToken(token string) (*casdoorsdk.Claims, error) {
return m.casdoorClient.ParseJwtToken(token)
}

func (m *mockCasdoorClient) GetUser(name string) (*casdoorsdk.User, error) {
claims, err := m.casdoorClient.ParseJwtToken(m.jwt)
if err != nil {
return nil, err
}
return &claims.User, nil
}

const fakeUserToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJvd25lciI6IkdvUGx1cyIsIm5hbWUiOiJmYWtlLW5hbWUiLCJpZCI6IjEiLCJpc3MiOiJHb1BsdXMiLCJzdWIiOiIxIiwiZXhwIjo0ODcwNDI5MDQwfQ." +
"X0T-v-RJggMRy3Mmui2FoRH-_4DQsNA6DekUx1BfIljTZaEbHbuW59dSlKQ-i2MuYD7_8mI18vZqT3iysbKQ1T70NF97B_A130ML3pulZWlj1ZokgjCkVug25QRbq_N7JMd4apJZFlyZj8Bd2VfqtAKMlJJ4HzKzNXB-GBogDVlKeu4xJ1BiXO2rHL1PNa5KyKLSSMXmuP_Wc108RXZ0BiKDE30IG1fvcyvudXcetmltuWjuU6JRj3FGedxuVEqZLXqcm13dCxHnuFV1x1XU9KExcDvVyVB91FpBe5npzYp6WMX0fx9vU1b4eJ69EZoeMdMolhmvYInT1G8r1PEmbg"

func newTestController(t *testing.T) (ctrl *Controller, dbMock sqlmock.Sqlmock, closeDB func() error) {
setTestEnv(t)

logger := log.GetLogger()
kodoConfig := newKodoConfig(logger)
aigcClient := aigc.NewAigcClient(mustEnv(logger, "AIGC_ENDPOINT"))
casdoorClient := newCasdoorClient(logger)
casdoorClient := newMockCasdoorClient(newCasdoorClient(logger), fakeUserToken)

db, dbMock, closeDB, err := modeltest.NewMockDB()
require.NoError(t, err)
Expand Down
10 changes: 9 additions & 1 deletion spx-backend/internal/controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type UserDTO struct {
ModelDTO

Username string `json:"username"`
DisplayName string `json:"displayName"`
Avatar string `json:"avatar"`
Description string `json:"description"`
FollowerCount int64 `json:"followerCount"`
FollowingCount int64 `json:"followingCount"`
Expand All @@ -29,6 +31,8 @@ func toUserDTO(mUser model.User) UserDTO {
return UserDTO{
ModelDTO: toModelDTO(mUser.Model),
Username: mUser.Username,
DisplayName: mUser.DisplayName,
Avatar: mUser.Avatar,
Description: mUser.Description,
FollowerCount: mUser.FollowerCount,
FollowingCount: mUser.FollowingCount,
Expand Down Expand Up @@ -71,7 +75,11 @@ func (ctrl *Controller) UserFromToken(ctx context.Context, token string) (*model
if err != nil {
return nil, fmt.Errorf("ctrl.casdoorClient.ParseJwtToken failed: %w: %w", ErrUnauthorized, err)
}
mUser, err := model.FirstOrCreateUser(ctx, ctrl.db, claims.Name)
casdoorUser, err := ctrl.casdoorClient.GetUser(claims.Name)
if err != nil {
return nil, fmt.Errorf("ctrl.casdoorClient.GetUser failed: %w", err)
}
mUser, err := model.FirstOrCreateUser(ctx, ctrl.db, casdoorUser)
if err != nil {
return nil, err
}
Expand Down
4 changes: 0 additions & 4 deletions spx-backend/internal/controller/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ func TestEnsureUser(t *testing.T) {
})
}

const fakeUserToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJvd25lciI6IkdvUGx1cyIsIm5hbWUiOiJmYWtlLW5hbWUiLCJpZCI6IjEiLCJpc3MiOiJHb1BsdXMiLCJzdWIiOiIxIiwiZXhwIjo0ODcwNDI5MDQwfQ." +
"X0T-v-RJggMRy3Mmui2FoRH-_4DQsNA6DekUx1BfIljTZaEbHbuW59dSlKQ-i2MuYD7_8mI18vZqT3iysbKQ1T70NF97B_A130ML3pulZWlj1ZokgjCkVug25QRbq_N7JMd4apJZFlyZj8Bd2VfqtAKMlJJ4HzKzNXB-GBogDVlKeu4xJ1BiXO2rHL1PNa5KyKLSSMXmuP_Wc108RXZ0BiKDE30IG1fvcyvudXcetmltuWjuU6JRj3FGedxuVEqZLXqcm13dCxHnuFV1x1XU9KExcDvVyVB91FpBe5npzYp6WMX0fx9vU1b4eJ69EZoeMdMolhmvYInT1G8r1PEmbg"

func TestControllerUserFromToken(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
ctrl, dbMock, closeDB := newTestController(t)
Expand Down
35 changes: 29 additions & 6 deletions spx-backend/internal/model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"

"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
Expand All @@ -15,6 +16,12 @@ type User struct {
// Username is the unique username.
Username string `gorm:"column:username;index:,unique,where:deleted_at IS NULL"`

// DisplayName is the display name.
DisplayName string `gorm:"column:display_name"`

// Avatar is the URL of the avatar image.
Avatar string `gorm:"column:avatar"`

// Description is the brief bio or description.
Description string `gorm:"column:description"`

Expand All @@ -40,26 +47,42 @@ func (User) TableName() string {
}

// FirstOrCreateUser gets or creates a user.
func FirstOrCreateUser(ctx context.Context, db *gorm.DB, username string) (*User, error) {
func FirstOrCreateUser(ctx context.Context, db *gorm.DB, casdoorUser *casdoorsdk.User) (*User, error) {
var mUser User
if err := db.WithContext(ctx).
Where("username = ?", username).
Attrs(User{Username: username}).
Where("username = ?", casdoorUser.Name).
Attrs(User{
Username: casdoorUser.Name,
DisplayName: casdoorUser.DisplayName,
Avatar: casdoorUser.Avatar,
}).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "username"}},
DoNothing: true,
}).
FirstOrCreate(&mUser).
Error; err != nil {
return nil, fmt.Errorf("failed to get/create user %s: %w", username, err)
return nil, fmt.Errorf("failed to get/create user %s: %w", casdoorUser.Name, err)
}
if mUser.ID == 0 {
// Unfortunatlly, MySQL doesn't support the RETURNING clause.
if err := db.WithContext(ctx).
Where("username = ?", username).
Where("username = ?", casdoorUser.Name).
First(&mUser).
Error; err != nil {
return nil, fmt.Errorf("failed to get user %s: %w", username, err)
return nil, fmt.Errorf("failed to get user %s: %w", casdoorUser.Name, err)
}
}
userUpdates := map[string]any{}
if mUser.DisplayName != casdoorUser.DisplayName {
userUpdates["display_name"] = casdoorUser.DisplayName
}
if mUser.Avatar != casdoorUser.Avatar {
userUpdates["avatar"] = casdoorUser.Avatar
}
if len(userUpdates) > 0 {
if err := db.WithContext(ctx).Model(&mUser).Updates(userUpdates).Error; err != nil {
return nil, fmt.Errorf("failed to update user %s: %w", mUser.Username, err)
}
}
return &mUser, nil
Expand Down
62 changes: 58 additions & 4 deletions spx-backend/internal/model/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/goplus/builder/spx-backend/internal/model/modeltest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -23,9 +24,17 @@ func TestFirstOrCreateUser(t *testing.T) {
generateUserDBRows, err := modeltest.NewDBRowsGenerator(db, User{})
require.NoError(t, err)

expectedCasdoorUser := casdoorsdk.User{
Name: "john",
DisplayName: "John Doe",
Avatar: "https://example.com/avatar.jpg",
}

mExpectedUser := User{
Model: Model{ID: 1},
Username: "john",
Username: expectedCasdoorUser.Name,
DisplayName: expectedCasdoorUser.DisplayName,
Avatar: expectedCasdoorUser.Avatar,
Description: "I'm John",
FollowerCount: 10,
FollowingCount: 5,
Expand All @@ -48,7 +57,7 @@ func TestFirstOrCreateUser(t *testing.T) {
WithArgs(dbMockArgs...).
WillReturnRows(sqlmock.NewRows(userDBColumns).AddRows(generateUserDBRows(mExpectedUser)...))

mUser, err := FirstOrCreateUser(context.Background(), db, mExpectedUser.Username)
mUser, err := FirstOrCreateUser(context.Background(), db, &expectedCasdoorUser)
require.NoError(t, err)
assert.Equal(t, mExpectedUser, *mUser)

Expand All @@ -75,7 +84,11 @@ func TestFirstOrCreateUser(t *testing.T) {
Columns: []clause.Column{{Name: "username"}},
DoNothing: true,
}).
Create(&User{Username: mExpectedUser.Username}).
Create(&User{
Username: mExpectedUser.Username,
DisplayName: mExpectedUser.DisplayName,
Avatar: mExpectedUser.Avatar,
}).
Statement
dbMockArgs = modeltest.ToDriverValueSlice(dbMockStmt.Vars...)
dbMockArgs[0] = sqlmock.AnyArg()
Expand All @@ -95,10 +108,51 @@ func TestFirstOrCreateUser(t *testing.T) {
WithArgs(dbMockArgs...).
WillReturnRows(sqlmock.NewRows(userDBColumns).AddRows(generateUserDBRows(mExpectedUser)...))

mUser, err := FirstOrCreateUser(context.Background(), db, mExpectedUser.Username)
mUser, err := FirstOrCreateUser(context.Background(), db, &expectedCasdoorUser)
require.NoError(t, err)
assert.Equal(t, mExpectedUser, *mUser)

require.NoError(t, dbMock.ExpectationsWereMet())
})

t.Run("UpdateExistingUser", func(t *testing.T) {
db, dbMock, closeDB, err := modeltest.NewMockDB()
require.NoError(t, err)
defer closeDB()

mExistingUser := mExpectedUser
mExistingUser.DisplayName = "Old Name"
mExistingUser.Avatar = "https://example.com/old-avatar.jpg"

dbMockStmt := db.Session(&gorm.Session{DryRun: true}).
Where("username = ?", mExistingUser.Username).
First(&User{}).
Statement
dbMockArgs := modeltest.ToDriverValueSlice(dbMockStmt.Vars...)
dbMock.ExpectQuery(regexp.QuoteMeta(dbMockStmt.SQL.String())).
WithArgs(dbMockArgs...).
WillReturnRows(sqlmock.NewRows(userDBColumns).AddRows(generateUserDBRows(mExistingUser)...))

dbMock.ExpectBegin()
dbMockStmt = db.Session(&gorm.Session{DryRun: true, SkipDefaultTransaction: true}).
Model(&User{Model: mExistingUser.Model}).
Updates(map[string]any{
"display_name": mExpectedUser.DisplayName,
"avatar": mExpectedUser.Avatar,
}).
Statement
dbMockArgs = modeltest.ToDriverValueSlice(dbMockStmt.Vars...)
dbMockArgs[2] = sqlmock.AnyArg()
dbMock.ExpectExec(regexp.QuoteMeta(dbMockStmt.SQL.String())).
WithArgs(dbMockArgs...).
WillReturnResult(sqlmock.NewResult(0, 1))
dbMock.ExpectCommit()

mUser, err := FirstOrCreateUser(context.Background(), db, &expectedCasdoorUser)
require.NoError(t, err)
assert.Equal(t, mExpectedUser.DisplayName, mUser.DisplayName)
assert.Equal(t, mExpectedUser.Avatar, mUser.Avatar)

require.NoError(t, dbMock.ExpectationsWereMet())
})
}
15 changes: 0 additions & 15 deletions spx-gui/src/apis/casdoor-user.ts

This file was deleted.

Loading

0 comments on commit baa87df

Please sign in to comment.