Feat: finish login and add test
This commit is contained in:
5
Makefile
5
Makefile
@@ -15,10 +15,7 @@ install:
|
|||||||
$(GO_ENV) go install
|
$(GO_ENV) go install
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -v ./tests/units
|
go test -v ./tests -count=1
|
||||||
|
|
||||||
bench:
|
|
||||||
go test -bench=. ./tests/benchmarks
|
|
||||||
|
|
||||||
swagger:
|
swagger:
|
||||||
$(SWAG) fmt
|
$(SWAG) fmt
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"gitea.konchin.com/go2025/backend/implements"
|
"gitea.konchin.com/go2025/backend/implements"
|
||||||
"gitea.konchin.com/go2025/backend/middlewares"
|
"gitea.konchin.com/go2025/backend/middlewares"
|
||||||
"gitea.konchin.com/go2025/backend/tracing"
|
"gitea.konchin.com/go2025/backend/tracing"
|
||||||
|
"gitea.konchin.com/go2025/backend/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
httpSwagger "github.com/swaggo/http-swagger"
|
httpSwagger "github.com/swaggo/http-swagger"
|
||||||
@@ -19,6 +20,7 @@ import (
|
|||||||
"github.com/uptrace/bun/extra/bunotel"
|
"github.com/uptrace/bun/extra/bunotel"
|
||||||
"github.com/uptrace/bunrouter"
|
"github.com/uptrace/bunrouter"
|
||||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
_ "gitea.konchin.com/go2025/backend/docs"
|
_ "gitea.konchin.com/go2025/backend/docs"
|
||||||
)
|
)
|
||||||
@@ -48,6 +50,13 @@ var serveCmd = &cobra.Command{
|
|||||||
bunDB := bun.NewDB(sqldb, pgdialect.New())
|
bunDB := bun.NewDB(sqldb, pgdialect.New())
|
||||||
bunDB.AddQueryHook(bunotel.NewQueryHook(bunotel.WithDBName("backend")))
|
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
|
// Initialize MinIO instance
|
||||||
mc, err := minio.New(viper.GetString("minio-host"), &minio.Options{
|
mc, err := minio.New(viper.GetString("minio-host"), &minio.Options{
|
||||||
@@ -97,6 +106,7 @@ var serveCmd = &cobra.Command{
|
|||||||
|
|
||||||
authGroup := backend.NewGroup("/auth")
|
authGroup := backend.NewGroup("/auth")
|
||||||
authGroup.POST("/login", auths.PostLogin)
|
authGroup.POST("/login", auths.PostLogin)
|
||||||
|
authGroup.POST("/gen-login-url", auths.PostGenLoginUrl)
|
||||||
|
|
||||||
if viper.GetBool("swagger") {
|
if viper.GetBool("swagger") {
|
||||||
backend.GET("/swagger/*any",
|
backend.GET("/swagger/*any",
|
||||||
@@ -113,6 +123,8 @@ var serveCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
serveCmd.Flags().
|
serveCmd.Flags().
|
||||||
String("port", "8080", "Port to listen on")
|
String("port", "8080", "Port to listen on")
|
||||||
|
serveCmd.Flags().
|
||||||
|
String("external-url", "http://localhost:8080", "External url for login")
|
||||||
serveCmd.Flags().
|
serveCmd.Flags().
|
||||||
String("cors-origin", "", "CORS origin")
|
String("cors-origin", "", "CORS origin")
|
||||||
|
|
||||||
@@ -132,7 +144,7 @@ func init() {
|
|||||||
|
|
||||||
serveCmd.Flags().
|
serveCmd.Flags().
|
||||||
String("pg-connection-string",
|
String("pg-connection-string",
|
||||||
"postgres://go2025:go2025@pg:5432/go2025?sslmode=disable",
|
"postgres://go2025:go2025@postgres:5432/go2025?sslmode=disable",
|
||||||
"Postgres connection string")
|
"Postgres connection string")
|
||||||
|
|
||||||
serveCmd.Flags().
|
serveCmd.Flags().
|
||||||
|
|||||||
@@ -246,11 +246,8 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"302": {
|
"200": {
|
||||||
"description": "redirect to root page",
|
"description": "OK"
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,11 +238,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"302": {
|
"200": {
|
||||||
"description": "redirect to root page",
|
"description": "OK"
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,8 +191,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/auth.postLoginInput'
|
$ref: '#/definitions/auth.postLoginInput'
|
||||||
responses:
|
responses:
|
||||||
"302":
|
"200":
|
||||||
description: redirect to root page
|
description: OK
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
swagger: "2.0"
|
swagger: "2.0"
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module gitea.konchin.com/go2025/backend
|
|||||||
go 1.25.4
|
go 1.25.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-resty/resty/v2 v2.17.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/minio/minio-go/v7 v7.0.97
|
github.com/minio/minio-go/v7 v7.0.97
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
|
|||||||
4
go.sum
4
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.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 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
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 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
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=
|
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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
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.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 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
|
|||||||
@@ -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 {
|
if err != nil {
|
||||||
return middlewares.HTTPError{
|
return middlewares.HTTPError{
|
||||||
StatusCode: http.StatusInternalServerError,
|
StatusCode: http.StatusInternalServerError,
|
||||||
@@ -57,7 +57,7 @@ func (self *Handlers) PostGenLoginUrl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return bunrouter.JSON(w, postGenLoginUrlOutput{
|
return bunrouter.JSON(w, postGenLoginUrlOutput{
|
||||||
LoginUrl: viper.GetString("extern-url") +
|
LoginUrl: viper.GetString("external-url") +
|
||||||
"/auth/login?" +
|
"/auth/login?" +
|
||||||
"token=" + token,
|
"token=" + token,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ type postLoginInput struct {
|
|||||||
|
|
||||||
// PostLogin
|
// PostLogin
|
||||||
//
|
//
|
||||||
// @param payload body postLoginInput true "payload"
|
// @param payload body postLoginInput true "payload"
|
||||||
// @success 302 {object} string "redirect to root page"
|
// @success 200
|
||||||
// @router /auth/login [post]
|
// @router /auth/login [post]
|
||||||
func (self *Handlers) PostLogin(
|
func (self *Handlers) PostLogin(
|
||||||
w http.ResponseWriter, req bunrouter.Request,
|
w http.ResponseWriter, req bunrouter.Request,
|
||||||
|
|||||||
@@ -57,19 +57,18 @@ func (self *BunDatabase) UpdateRefreshToken(
|
|||||||
return models.Session{}, err
|
return models.Session{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = self.db.NewUpdate().
|
_, err = self.db.NewUpdate().
|
||||||
Model((*models.Session)(nil)).
|
Model((*models.Session)(nil)).
|
||||||
Set("refresh_token = ?", ret.RefreshToken).
|
Set("refresh_token = ?", ret.RefreshToken).
|
||||||
Where("user_id = ?", ret.UserId).
|
Where("user_id = ?", userId).
|
||||||
Returning("*").
|
Exec(ctx)
|
||||||
Scan(ctx, &ret)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.Session{}, err
|
return models.Session{}, err
|
||||||
}
|
}
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *BunDatabase) UpdateLoginToken(
|
func (self *BunDatabase) UpsertLoginToken(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userId string,
|
userId string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
@@ -78,10 +77,14 @@ func (self *BunDatabase) UpdateLoginToken(
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = self.db.NewUpdate().
|
session := models.Session{
|
||||||
Model((*models.Session)(nil)).
|
UserId: userId,
|
||||||
Set("login_token = ?", token).
|
LoginToken: token,
|
||||||
Where("user_id = ?", userId).
|
}
|
||||||
|
_, err = self.db.NewInsert().
|
||||||
|
Model(&session).
|
||||||
|
On("CONFLICT (user_id) DO UPDATE").
|
||||||
|
Set("login_token = EXCLUDED.login_token").
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type Database interface {
|
|||||||
userId string,
|
userId string,
|
||||||
) (models.Session, error)
|
) (models.Session, error)
|
||||||
|
|
||||||
UpdateLoginToken(
|
UpsertLoginToken(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userId string,
|
userId string,
|
||||||
) (string, error)
|
) (string, error)
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ type AliasImage struct {
|
|||||||
AliasId int64 `bun:"alias_id,pk"`
|
AliasId int64 `bun:"alias_id,pk"`
|
||||||
ImageId int64 `bun:"image_id,pk"`
|
ImageId int64 `bun:"image_id,pk"`
|
||||||
|
|
||||||
Alias *Alias `bun:"belongs-to,join:alias_id=id"`
|
Alias *Alias `bun:"rel:belongs-to,join:alias_id=id"`
|
||||||
Image *Image `bun:"belongs-to,join:image_id=id"`
|
Image *Image `bun:"rel:belongs-to,join:image_id=id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ func (self *Session) RotateRefreshToken() error {
|
|||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
IssuedAt: &jwt.NumericDate{time.Now()},
|
IssuedAt: &jwt.NumericDate{time.Now()},
|
||||||
ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Duration(
|
ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Duration(
|
||||||
viper.GetInt64("REFRESH_TOKEN_TIMEOUT")) * time.Second)},
|
viper.GetInt64("refresh-token-timeout")) * time.Second)},
|
||||||
}}).SignedString([]byte(viper.GetString("REFRESH_TOKEN_SECRET")))
|
}}).SignedString([]byte(viper.GetString("refresh-token-secret")))
|
||||||
if err != nil {
|
if err == nil {
|
||||||
self.RefreshToken = refreshToken
|
self.RefreshToken = refreshToken
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -63,6 +63,6 @@ func (self *Session) ToAccessToken() (string, error) {
|
|||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
IssuedAt: &jwt.NumericDate{time.Now()},
|
IssuedAt: &jwt.NumericDate{time.Now()},
|
||||||
ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Duration(
|
ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Duration(
|
||||||
viper.GetInt64("ACCESS_TOKEN_TIMEOUT")) * time.Second)},
|
viper.GetInt64("access-token-timeout")) * time.Second)},
|
||||||
}}).SignedString([]byte(viper.GetString("ACCESS_TOKEN_SECRET")))
|
}}).SignedString([]byte(viper.GetString("access-token-secret")))
|
||||||
}
|
}
|
||||||
|
|||||||
59
tests/login_test.go
Normal file
59
tests/login_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@ import (
|
|||||||
"github.com/uptrace/bun"
|
"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,
|
return db.ResetModel(ctx,
|
||||||
(*models.Alias)(nil),
|
(*models.Alias)(nil),
|
||||||
(*models.Image)(nil),
|
(*models.Image)(nil),
|
||||||
|
|||||||
Reference in New Issue
Block a user