Feat: add login
This commit is contained in:
@@ -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().
|
||||||
|
|||||||
31
docs/docs.go
31
docs/docs.go
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
11
handlers/auth/handlers.go
Normal 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}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
73
handlers/auth/postLogin.go
Normal file
73
handlers/auth/postLogin.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
15
utils/randomString.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user