diff --git a/cmds/serve.go b/cmds/serve.go index eb9490b..7a69c8e 100644 --- a/cmds/serve.go +++ b/cmds/serve.go @@ -6,6 +6,7 @@ import ( "net/http" "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/middlewares" "gitea.konchin.com/go2025/backend/tracing" @@ -77,8 +78,9 @@ var serveCmd = &cobra.Command{ // s3 := implements.NewMinIOObjectStorage(mc) // Initialize handlers - apis := api.NewHandlers() midHandlers := middlewares.NewHandlers(db) + apis := api.NewHandlers() + auths := auth.NewHandlers(db) // Initialize backend router router := bunrouter.New() @@ -93,6 +95,9 @@ var serveCmd = &cobra.Command{ Use(midHandlers.CheckAccessToken) apiGroup.GET("/images", apis.GetImages) + authGroup := backend.NewGroup("/auth") + authGroup.POST("/login", auths.PostLogin) + if viper.GetBool("swagger") { backend.GET("/swagger/*any", bunrouter.HTTPHandlerFunc( @@ -111,6 +116,15 @@ func init() { serveCmd.Flags(). 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(). Bool("zap-production", true, "Toggle production log format") serveCmd.Flags(). diff --git a/docs/docs.go b/docs/docs.go index c195403..a2cb5ab 100644 --- a/docs/docs.go +++ b/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": { @@ -280,6 +303,14 @@ const docTemplate = `{ "type": "string" } } + }, + "auth.postLoginInput": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } } } }` diff --git a/docs/swagger.json b/docs/swagger.json index 508b9bb..f58654c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { @@ -272,6 +295,14 @@ "type": "string" } } + }, + "auth.postLoginInput": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index bc53e30..2245595 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -30,6 +30,11 @@ definitions: loginUrl: type: string type: object + auth.postLoginInput: + properties: + token: + type: string + type: object info: contact: {} license: @@ -176,4 +181,18 @@ paths: $ref: '#/definitions/auth.postGenLoginUrlOutput' "400": 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" diff --git a/handlers/auth/handlers.go b/handlers/auth/handlers.go new file mode 100644 index 0000000..fa0aa1e --- /dev/null +++ b/handlers/auth/handlers.go @@ -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} +} diff --git a/handlers/auth/postGenLoginUrl.go b/handlers/auth/postGenLoginUrl.go index 710dac1..0b15df7 100644 --- a/handlers/auth/postGenLoginUrl.go +++ b/handlers/auth/postGenLoginUrl.go @@ -1,9 +1,12 @@ package auth import ( + "encoding/json" + "io" "net/http" - "gitea.konchin.com/go2025/backend/utils" + "gitea.konchin.com/go2025/backend/middlewares" + "github.com/spf13/viper" "github.com/uptrace/bunrouter" ) @@ -24,5 +27,38 @@ type postGenLoginUrlOutput struct { func (self *Handlers) PostGenLoginUrl( w http.ResponseWriter, req bunrouter.Request, ) 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, + }) } diff --git a/handlers/auth/postLogin.go b/handlers/auth/postLogin.go new file mode 100644 index 0000000..3f305dd --- /dev/null +++ b/handlers/auth/postLogin.go @@ -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) +} diff --git a/implements/bunDatabase.go b/implements/bunDatabase.go index c1c5bbb..e5c66b0 100644 --- a/implements/bunDatabase.go +++ b/implements/bunDatabase.go @@ -5,6 +5,7 @@ import ( "gitea.konchin.com/go2025/backend/models" "gitea.konchin.com/go2025/backend/tracing" + "gitea.konchin.com/go2025/backend/utils" "github.com/uptrace/bun" "go.uber.org/zap" ) @@ -19,14 +20,14 @@ func NewBunDatabase(db *bun.DB) *BunDatabase { func (self *BunDatabase) GetSession( ctx context.Context, - userId string, + loginToken string, ) (models.Session, error) { ret := models.Session{ - UserId: userId, + LoginToken: loginToken, } err := self.db.NewSelect(). Model(&ret). - WherePK(). + Where("login_token = ?", loginToken). Scan(ctx) if err != nil { return models.Session{}, err @@ -67,3 +68,23 @@ func (self *BunDatabase) UpdateRefreshToken( } 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 +} diff --git a/interfaces/database.go b/interfaces/database.go index c211a28..31c367a 100644 --- a/interfaces/database.go +++ b/interfaces/database.go @@ -9,11 +9,16 @@ import ( type Database interface { GetSession( ctx context.Context, - userId string, + loginToken string, ) (models.Session, error) UpdateRefreshToken( ctx context.Context, userId string, ) (models.Session, error) + + UpdateLoginToken( + ctx context.Context, + userId string, + ) (string, error) } diff --git a/types/errors.go b/types/errors.go index 14114b1..2955cb2 100644 --- a/types/errors.go +++ b/types/errors.go @@ -7,4 +7,6 @@ var ( WrongFormatError = fmt.Errorf("wrong format") HTTPRequestFailedError = fmt.Errorf("http request failed") + + RandomGenerationFailedError = fmt.Errorf("failed to generate random string") ) diff --git a/utils/randomString.go b/utils/randomString.go new file mode 100644 index 0000000..6e38e66 --- /dev/null +++ b/utils/randomString.go @@ -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 +}