Feat: add login

This commit is contained in:
2025-12-07 12:15:48 +08:00
parent fb1c47b321
commit 2eb8a18c40
11 changed files with 265 additions and 7 deletions

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"gitea.konchin.com/go2025/backend/handlers/api" "gitea.konchin.com/go2025/backend/handlers/api"
"gitea.konchin.com/go2025/backend/handlers/auth"
"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"
@@ -77,8 +78,9 @@ var serveCmd = &cobra.Command{
// s3 := implements.NewMinIOObjectStorage(mc) // s3 := implements.NewMinIOObjectStorage(mc)
// Initialize handlers // Initialize handlers
apis := api.NewHandlers()
midHandlers := middlewares.NewHandlers(db) midHandlers := middlewares.NewHandlers(db)
apis := api.NewHandlers()
auths := auth.NewHandlers(db)
// Initialize backend router // Initialize backend router
router := bunrouter.New() router := bunrouter.New()
@@ -93,6 +95,9 @@ var serveCmd = &cobra.Command{
Use(midHandlers.CheckAccessToken) Use(midHandlers.CheckAccessToken)
apiGroup.GET("/images", apis.GetImages) apiGroup.GET("/images", apis.GetImages)
authGroup := backend.NewGroup("/auth")
authGroup.POST("/login", auths.PostLogin)
if viper.GetBool("swagger") { if viper.GetBool("swagger") {
backend.GET("/swagger/*any", backend.GET("/swagger/*any",
bunrouter.HTTPHandlerFunc( bunrouter.HTTPHandlerFunc(
@@ -111,6 +116,15 @@ func init() {
serveCmd.Flags(). serveCmd.Flags().
String("cors-origin", "", "CORS origin") String("cors-origin", "", "CORS origin")
serveCmd.Flags().
Int64("access-token-timeout", 300, "Timeout of Access Token JWT")
serveCmd.Flags().
String("access-token-secret", "poop", "Access Token JWT HMAC secret")
serveCmd.Flags().
Int64("refresh-token-timeout", 3600, "Timeout of Refresh Token JWT")
serveCmd.Flags().
String("refresh-token-secret", "poop", "Refresh Token JWT HMAC secret")
serveCmd.Flags(). serveCmd.Flags().
Bool("zap-production", true, "Toggle production log format") Bool("zap-production", true, "Toggle production log format")
serveCmd.Flags(). serveCmd.Flags().

View File

@@ -231,6 +231,29 @@ const docTemplate = `{
} }
} }
} }
},
"/auth/login": {
"post": {
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/auth.postLoginInput"
}
}
],
"responses": {
"302": {
"description": "redirect to root page",
"schema": {
"type": "string"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@@ -280,6 +303,14 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
} }
},
"auth.postLoginInput": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
}
} }
} }
}` }`

View File

@@ -223,6 +223,29 @@
} }
} }
} }
},
"/auth/login": {
"post": {
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/auth.postLoginInput"
}
}
],
"responses": {
"302": {
"description": "redirect to root page",
"schema": {
"type": "string"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@@ -272,6 +295,14 @@
"type": "string" "type": "string"
} }
} }
},
"auth.postLoginInput": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
}
} }
} }
} }

View File

@@ -30,6 +30,11 @@ definitions:
loginUrl: loginUrl:
type: string type: string
type: object type: object
auth.postLoginInput:
properties:
token:
type: string
type: object
info: info:
contact: {} contact: {}
license: license:
@@ -176,4 +181,18 @@ paths:
$ref: '#/definitions/auth.postGenLoginUrlOutput' $ref: '#/definitions/auth.postGenLoginUrlOutput'
"400": "400":
description: Bad Request description: Bad Request
/auth/login:
post:
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/auth.postLoginInput'
responses:
"302":
description: redirect to root page
schema:
type: string
swagger: "2.0" swagger: "2.0"

11
handlers/auth/handlers.go Normal file
View File

@@ -0,0 +1,11 @@
package auth
import "gitea.konchin.com/go2025/backend/interfaces"
type Handlers struct {
db interfaces.Database
}
func NewHandlers(db interfaces.Database) *Handlers {
return &Handlers{db: db}
}

View File

@@ -1,9 +1,12 @@
package auth package auth
import ( import (
"encoding/json"
"io"
"net/http" "net/http"
"gitea.konchin.com/go2025/backend/utils" "gitea.konchin.com/go2025/backend/middlewares"
"github.com/spf13/viper"
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"
) )
@@ -24,5 +27,38 @@ type postGenLoginUrlOutput struct {
func (self *Handlers) PostGenLoginUrl( func (self *Handlers) PostGenLoginUrl(
w http.ResponseWriter, req bunrouter.Request, w http.ResponseWriter, req bunrouter.Request,
) error { ) error {
return utils.Success(w) ctx := req.Context()
b, err := io.ReadAll(req.Body)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to read payload",
OriginError: err,
}
}
var input postGenLoginUrlInput
if err := json.Unmarshal(b, &input); err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to unmarshal json",
OriginError: err,
}
}
token, err := self.db.UpdateLoginToken(ctx, input.UserId)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to update token",
OriginError: err,
}
}
return bunrouter.JSON(w, postGenLoginUrlOutput{
LoginUrl: viper.GetString("extern-url") +
"/auth/login?" +
"token=" + token,
})
} }

View File

@@ -0,0 +1,73 @@
package auth
import (
"encoding/json"
"io"
"net/http"
"time"
"gitea.konchin.com/go2025/backend/middlewares"
"gitea.konchin.com/go2025/backend/utils"
"github.com/spf13/viper"
"github.com/uptrace/bunrouter"
)
type postLoginInput struct {
Token string `json:"token"`
}
// PostLogin
//
// @param payload body postLoginInput true "payload"
// @success 302 {object} string "redirect to root page"
// @router /auth/login [post]
func (self *Handlers) PostLogin(
w http.ResponseWriter, req bunrouter.Request,
) error {
ctx := req.Context()
b, err := io.ReadAll(req.Body)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to read payload",
OriginError: err,
}
}
var input postLoginInput
if err := json.Unmarshal(b, &input); err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to unmarshal json",
OriginError: err,
}
}
session, err := self.db.GetSession(ctx, input.Token)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "session not found",
OriginError: err,
}
}
session, err = self.db.UpdateRefreshToken(ctx, session.UserId)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to update refresh token",
OriginError: err,
}
}
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: session.RefreshToken,
Expires: time.Now().Add(time.Duration(
viper.GetInt64("REFRESH_TOKEN_TIMEOUT")) * time.Second),
})
return utils.Success(w)
}

View File

@@ -5,6 +5,7 @@ import (
"gitea.konchin.com/go2025/backend/models" "gitea.konchin.com/go2025/backend/models"
"gitea.konchin.com/go2025/backend/tracing" "gitea.konchin.com/go2025/backend/tracing"
"gitea.konchin.com/go2025/backend/utils"
"github.com/uptrace/bun" "github.com/uptrace/bun"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -19,14 +20,14 @@ func NewBunDatabase(db *bun.DB) *BunDatabase {
func (self *BunDatabase) GetSession( func (self *BunDatabase) GetSession(
ctx context.Context, ctx context.Context,
userId string, loginToken string,
) (models.Session, error) { ) (models.Session, error) {
ret := models.Session{ ret := models.Session{
UserId: userId, LoginToken: loginToken,
} }
err := self.db.NewSelect(). err := self.db.NewSelect().
Model(&ret). Model(&ret).
WherePK(). Where("login_token = ?", loginToken).
Scan(ctx) Scan(ctx)
if err != nil { if err != nil {
return models.Session{}, err return models.Session{}, err
@@ -67,3 +68,23 @@ func (self *BunDatabase) UpdateRefreshToken(
} }
return ret, nil return ret, nil
} }
func (self *BunDatabase) UpdateLoginToken(
ctx context.Context,
userId string,
) (string, error) {
token, err := utils.RandomString(24)
if err != nil {
return "", err
}
_, err = self.db.NewUpdate().
Model((*models.Session)(nil)).
Set("login_token = ?", token).
Where("user_id = ?", userId).
Exec(ctx)
if err != nil {
return "", err
}
return token, nil
}

View File

@@ -9,11 +9,16 @@ import (
type Database interface { type Database interface {
GetSession( GetSession(
ctx context.Context, ctx context.Context,
userId string, loginToken string,
) (models.Session, error) ) (models.Session, error)
UpdateRefreshToken( UpdateRefreshToken(
ctx context.Context, ctx context.Context,
userId string, userId string,
) (models.Session, error) ) (models.Session, error)
UpdateLoginToken(
ctx context.Context,
userId string,
) (string, error)
} }

View File

@@ -7,4 +7,6 @@ var (
WrongFormatError = fmt.Errorf("wrong format") WrongFormatError = fmt.Errorf("wrong format")
HTTPRequestFailedError = fmt.Errorf("http request failed") HTTPRequestFailedError = fmt.Errorf("http request failed")
RandomGenerationFailedError = fmt.Errorf("failed to generate random string")
) )

15
utils/randomString.go Normal file
View File

@@ -0,0 +1,15 @@
package utils
import (
"crypto/rand"
"encoding/base64"
)
func RandomString(length int) (string, error) {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b)[:length], nil
}