diff --git a/Makefile b/Makefile index 2170da7..2b12abc 100644 --- a/Makefile +++ b/Makefile @@ -15,10 +15,7 @@ install: $(GO_ENV) go install test: - go test -v ./tests/units - -bench: - go test -bench=. ./tests/benchmarks + go test -v ./tests -count=1 swagger: $(SWAG) fmt diff --git a/cmds/serve.go b/cmds/serve.go index 7a69c8e..e199dc8 100644 --- a/cmds/serve.go +++ b/cmds/serve.go @@ -10,6 +10,7 @@ import ( "gitea.konchin.com/go2025/backend/implements" "gitea.konchin.com/go2025/backend/middlewares" "gitea.konchin.com/go2025/backend/tracing" + "gitea.konchin.com/go2025/backend/utils" "github.com/spf13/cobra" "github.com/spf13/viper" httpSwagger "github.com/swaggo/http-swagger" @@ -19,6 +20,7 @@ import ( "github.com/uptrace/bun/extra/bunotel" "github.com/uptrace/bunrouter" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.uber.org/zap" _ "gitea.konchin.com/go2025/backend/docs" ) @@ -48,6 +50,13 @@ var serveCmd = &cobra.Command{ bunDB := bun.NewDB(sqldb, pgdialect.New()) bunDB.AddQueryHook(bunotel.NewQueryHook(bunotel.WithDBName("backend"))) + if err := utils.InitDB(ctx, bunDB); err != nil { + tracing.Logger.Ctx(ctx). + Panic("failed to init database", + zap.Error(err)) + panic(err) + } + /* // Initialize MinIO instance mc, err := minio.New(viper.GetString("minio-host"), &minio.Options{ @@ -97,6 +106,7 @@ var serveCmd = &cobra.Command{ authGroup := backend.NewGroup("/auth") authGroup.POST("/login", auths.PostLogin) + authGroup.POST("/gen-login-url", auths.PostGenLoginUrl) if viper.GetBool("swagger") { backend.GET("/swagger/*any", @@ -113,6 +123,8 @@ var serveCmd = &cobra.Command{ func init() { serveCmd.Flags(). String("port", "8080", "Port to listen on") + serveCmd.Flags(). + String("external-url", "http://localhost:8080", "External url for login") serveCmd.Flags(). String("cors-origin", "", "CORS origin") @@ -132,7 +144,7 @@ func init() { serveCmd.Flags(). String("pg-connection-string", - "postgres://go2025:go2025@pg:5432/go2025?sslmode=disable", + "postgres://go2025:go2025@postgres:5432/go2025?sslmode=disable", "Postgres connection string") serveCmd.Flags(). diff --git a/docs/docs.go b/docs/docs.go index a2cb5ab..4449bab 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -246,11 +246,8 @@ const docTemplate = `{ } ], "responses": { - "302": { - "description": "redirect to root page", - "schema": { - "type": "string" - } + "200": { + "description": "OK" } } } diff --git a/docs/swagger.json b/docs/swagger.json index f58654c..06035e5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -238,11 +238,8 @@ } ], "responses": { - "302": { - "description": "redirect to root page", - "schema": { - "type": "string" - } + "200": { + "description": "OK" } } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2245595..779a0d6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -191,8 +191,6 @@ paths: schema: $ref: '#/definitions/auth.postLoginInput' responses: - "302": - description: redirect to root page - schema: - type: string + "200": + description: OK swagger: "2.0" diff --git a/go.mod b/go.mod index 76146f7..0dba92c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitea.konchin.com/go2025/backend go 1.25.4 require ( + github.com/go-resty/resty/v2 v2.17.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/minio/minio-go/v7 v7.0.97 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index a8ce5bc..9f0b8f2 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+gN0= +github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= @@ -201,6 +203,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= diff --git a/handlers/auth/postGenLoginUrl.go b/handlers/auth/postGenLoginUrl.go index 0b15df7..0a82a15 100644 --- a/handlers/auth/postGenLoginUrl.go +++ b/handlers/auth/postGenLoginUrl.go @@ -47,7 +47,7 @@ func (self *Handlers) PostGenLoginUrl( } } - token, err := self.db.UpdateLoginToken(ctx, input.UserId) + token, err := self.db.UpsertLoginToken(ctx, input.UserId) if err != nil { return middlewares.HTTPError{ StatusCode: http.StatusInternalServerError, @@ -57,7 +57,7 @@ func (self *Handlers) PostGenLoginUrl( } return bunrouter.JSON(w, postGenLoginUrlOutput{ - LoginUrl: viper.GetString("extern-url") + + LoginUrl: viper.GetString("external-url") + "/auth/login?" + "token=" + token, }) diff --git a/handlers/auth/postLogin.go b/handlers/auth/postLogin.go index 3f305dd..359f992 100644 --- a/handlers/auth/postLogin.go +++ b/handlers/auth/postLogin.go @@ -18,8 +18,8 @@ type postLoginInput struct { // PostLogin // -// @param payload body postLoginInput true "payload" -// @success 302 {object} string "redirect to root page" +// @param payload body postLoginInput true "payload" +// @success 200 // @router /auth/login [post] func (self *Handlers) PostLogin( w http.ResponseWriter, req bunrouter.Request, diff --git a/implements/bunDatabase.go b/implements/bunDatabase.go index e5c66b0..107079c 100644 --- a/implements/bunDatabase.go +++ b/implements/bunDatabase.go @@ -57,19 +57,18 @@ func (self *BunDatabase) UpdateRefreshToken( return models.Session{}, err } - err = self.db.NewUpdate(). + _, err = self.db.NewUpdate(). Model((*models.Session)(nil)). Set("refresh_token = ?", ret.RefreshToken). - Where("user_id = ?", ret.UserId). - Returning("*"). - Scan(ctx, &ret) + Where("user_id = ?", userId). + Exec(ctx) if err != nil { return models.Session{}, err } return ret, nil } -func (self *BunDatabase) UpdateLoginToken( +func (self *BunDatabase) UpsertLoginToken( ctx context.Context, userId string, ) (string, error) { @@ -78,10 +77,14 @@ func (self *BunDatabase) UpdateLoginToken( return "", err } - _, err = self.db.NewUpdate(). - Model((*models.Session)(nil)). - Set("login_token = ?", token). - Where("user_id = ?", userId). + session := models.Session{ + UserId: userId, + LoginToken: token, + } + _, err = self.db.NewInsert(). + Model(&session). + On("CONFLICT (user_id) DO UPDATE"). + Set("login_token = EXCLUDED.login_token"). Exec(ctx) if err != nil { return "", err diff --git a/interfaces/database.go b/interfaces/database.go index 31c367a..77b9dd3 100644 --- a/interfaces/database.go +++ b/interfaces/database.go @@ -17,7 +17,7 @@ type Database interface { userId string, ) (models.Session, error) - UpdateLoginToken( + UpsertLoginToken( ctx context.Context, userId string, ) (string, error) diff --git a/models/alias.go b/models/alias.go index fd7b0c1..6fc519a 100644 --- a/models/alias.go +++ b/models/alias.go @@ -17,6 +17,6 @@ type AliasImage struct { AliasId int64 `bun:"alias_id,pk"` ImageId int64 `bun:"image_id,pk"` - Alias *Alias `bun:"belongs-to,join:alias_id=id"` - Image *Image `bun:"belongs-to,join:image_id=id"` + Alias *Alias `bun:"rel:belongs-to,join:alias_id=id"` + Image *Image `bun:"rel:belongs-to,join:image_id=id"` } diff --git a/models/session.go b/models/session.go index 4077bbd..14821d6 100644 --- a/models/session.go +++ b/models/session.go @@ -47,9 +47,9 @@ func (self *Session) RotateRefreshToken() error { RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: &jwt.NumericDate{time.Now()}, ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Duration( - viper.GetInt64("REFRESH_TOKEN_TIMEOUT")) * time.Second)}, - }}).SignedString([]byte(viper.GetString("REFRESH_TOKEN_SECRET"))) - if err != nil { + viper.GetInt64("refresh-token-timeout")) * time.Second)}, + }}).SignedString([]byte(viper.GetString("refresh-token-secret"))) + if err == nil { self.RefreshToken = refreshToken } return err @@ -63,6 +63,6 @@ func (self *Session) ToAccessToken() (string, error) { RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: &jwt.NumericDate{time.Now()}, ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Duration( - viper.GetInt64("ACCESS_TOKEN_TIMEOUT")) * time.Second)}, - }}).SignedString([]byte(viper.GetString("ACCESS_TOKEN_SECRET"))) + viper.GetInt64("access-token-timeout")) * time.Second)}, + }}).SignedString([]byte(viper.GetString("access-token-secret"))) } diff --git a/tests/login_test.go b/tests/login_test.go new file mode 100644 index 0000000..e174b2b --- /dev/null +++ b/tests/login_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "net/http" + "net/url" + "testing" + + "github.com/go-resty/resty/v2" +) + +type genLoginUrlPayload struct { + LoginUrl string `json:"loginUrl"` +} + +type loginPayload struct { + Token string `json:"token"` +} + +func TestLogin(t *testing.T) { + client := resty.New() + + var payload genLoginUrlPayload + resp, err := client.R(). + SetBody(`{"userId": "testuser1"}`). + SetResult(&payload). + Post("http://localhost:8080/auth/gen-login-url") + + if err != nil || resp.StatusCode() != http.StatusOK { + t.Fatal("failed to get login url") + } + + loginUrl, err := url.Parse(payload.LoginUrl) + if err != nil { + t.Logf("login-url: %s", payload.LoginUrl) + t.Fatal("failed to parse login url") + } + + resp, err = client.R(). + SetBody(loginPayload{Token: loginUrl.Query().Get("token")}). + Post(loginUrl.Scheme + "://" + loginUrl.Host + loginUrl.Path) + if err != nil || resp.StatusCode() != http.StatusOK { + t.Fatal("failed to login") + } + + for _, rawCookie := range resp.Header()["Set-Cookie"] { + cookie, err := http.ParseSetCookie(rawCookie) + if err != nil { + t.Fatal("failed to parse cookie") + } + + if cookie.Name == "refresh_token" { + if len(cookie.Value) == 0 { + t.Fatal("empty refresh token") + } + return + } + } + t.Fatal("refresh token not exist") +} diff --git a/utils/initDB.go b/utils/initDB.go index b1c3203..4290e16 100644 --- a/utils/initDB.go +++ b/utils/initDB.go @@ -7,7 +7,8 @@ import ( "github.com/uptrace/bun" ) -func initDB(ctx context.Context, db *bun.DB) error { +func InitDB(ctx context.Context, db *bun.DB) error { + db.RegisterModel((*models.AliasImage)(nil)) return db.ResetModel(ctx, (*models.Alias)(nil), (*models.Image)(nil),