diff --git a/api/api_impl/impl.go b/api/api_impl/impl.go index 1f759ae4..060b6d4f 100644 --- a/api/api_impl/impl.go +++ b/api/api_impl/impl.go @@ -13,4 +13,5 @@ type APIController struct { controller.VersionController controller.ObjectController + controller.UserController } diff --git a/api/jiaozifs.gen.go b/api/jiaozifs.gen.go index eb5e6000..43ac5ca8 100644 --- a/api/jiaozifs.gen.go +++ b/api/jiaozifs.gen.go @@ -15,6 +15,7 @@ import ( "net/url" "path" "strings" + "time" "github.com/getkin/kin-openapi/openapi3" "github.com/go-chi/chi/v5" @@ -33,6 +34,15 @@ const ( Object ObjectStatsPathType = "object" ) +// AuthenticationToken defines model for AuthenticationToken. +type AuthenticationToken struct { + // Token a JWT token that could be used to authenticate requests + Token string `json:"token"` + + // TokenExpiration Unix Epoch in seconds + TokenExpiration *int64 `json:"token_expiration,omitempty"` +} + // ObjectStats defines model for ObjectStats. type ObjectStats struct { Checksum string `json:"checksum"` @@ -67,6 +77,25 @@ type ObjectStatsPathType string // ObjectUserMetadata defines model for ObjectUserMetadata. type ObjectUserMetadata map[string]string +// UserInfo defines model for UserInfo. +type UserInfo struct { + CreatedAt *time.Time `json:"createdAt,omitempty"` + CurrentSignInAt *time.Time `json:"currentSignInAt,omitempty"` + CurrentSignInIP *string `json:"currentSignInIP,omitempty"` + Email openapi_types.Email `json:"email"` + LastSignInAt *time.Time `json:"lastSignInAt,omitempty"` + LastSignInIP *string `json:"lastSignInIP,omitempty"` + UpdateAt *time.Time `json:"updateAt,omitempty"` + Username string `json:"username"` +} + +// UserRegisterInfo defines model for UserRegisterInfo. +type UserRegisterInfo struct { + Email openapi_types.Email `json:"email"` + Password string `json:"password"` + Username string `json:"username"` +} + // VersionResult defines model for VersionResult. type VersionResult struct { // ApiVersion runtime version @@ -76,6 +105,12 @@ type VersionResult struct { Version string `json:"version"` } +// LoginJSONBody defines parameters for Login. +type LoginJSONBody struct { + Password string `json:"password"` + Username string `json:"username"` +} + // DeleteObjectParams defines parameters for DeleteObject. type DeleteObjectParams struct { // Path relative to the ref @@ -121,6 +156,12 @@ type UploadObjectParams struct { IfNoneMatch *string `json:"If-None-Match,omitempty"` } +// LoginJSONRequestBody defines body for Login for application/json ContentType. +type LoginJSONRequestBody LoginJSONBody + +// RegisterJSONRequestBody defines body for Register for application/json ContentType. +type RegisterJSONRequestBody = UserRegisterInfo + // UploadObjectMultipartRequestBody defines body for UploadObject for multipart/form-data ContentType. type UploadObjectMultipartRequestBody UploadObjectMultipartBody @@ -197,6 +238,19 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { + // LoginWithBody request with any body + LoginWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + Login(ctx context.Context, body LoginJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // RegisterWithBody request with any body + RegisterWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + Register(ctx context.Context, body RegisterJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetUserInfo request + GetUserInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // DeleteObject request DeleteObject(ctx context.Context, repository string, params *DeleteObjectParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -213,6 +267,66 @@ type ClientInterface interface { GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) } +func (c *Client) LoginWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewLoginRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) Login(ctx context.Context, body LoginJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewLoginRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RegisterWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRegisterRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) Register(ctx context.Context, body RegisterJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRegisterRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetUserInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetUserInfoRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) DeleteObject(ctx context.Context, repository string, params *DeleteObjectParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewDeleteObjectRequest(c.Server, repository, params) if err != nil { @@ -273,6 +387,113 @@ func (c *Client) GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) return c.Client.Do(req) } +// NewLoginRequest calls the generic Login builder with application/json body +func NewLoginRequest(server string, body LoginJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewLoginRequestWithBody(server, "application/json", bodyReader) +} + +// NewLoginRequestWithBody generates requests for Login with any type of body +func NewLoginRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/auth/login") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewRegisterRequest calls the generic Register builder with application/json body +func NewRegisterRequest(server string, body RegisterJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewRegisterRequestWithBody(server, "application/json", bodyReader) +} + +// NewRegisterRequestWithBody generates requests for Register with any type of body +func NewRegisterRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/auth/register") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetUserInfoRequest generates requests for GetUserInfo +func NewGetUserInfoRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/auth/user") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewDeleteObjectRequest generates requests for DeleteObject func NewDeleteObjectRequest(server string, repository string, params *DeleteObjectParams) (*http.Request, error) { var err error @@ -630,6 +851,19 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { + // LoginWithBodyWithResponse request with any body + LoginWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*LoginResponse, error) + + LoginWithResponse(ctx context.Context, body LoginJSONRequestBody, reqEditors ...RequestEditorFn) (*LoginResponse, error) + + // RegisterWithBodyWithResponse request with any body + RegisterWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RegisterResponse, error) + + RegisterWithResponse(ctx context.Context, body RegisterJSONRequestBody, reqEditors ...RequestEditorFn) (*RegisterResponse, error) + + // GetUserInfoWithResponse request + GetUserInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetUserInfoResponse, error) + // DeleteObjectWithResponse request DeleteObjectWithResponse(ctx context.Context, repository string, params *DeleteObjectParams, reqEditors ...RequestEditorFn) (*DeleteObjectResponse, error) @@ -646,6 +880,71 @@ type ClientWithResponsesInterface interface { GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) } +type LoginResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *AuthenticationToken +} + +// Status returns HTTPResponse.Status +func (r LoginResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r LoginResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type RegisterResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r RegisterResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RegisterResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetUserInfoResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UserInfo +} + +// Status returns HTTPResponse.Status +func (r GetUserInfoResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetUserInfoResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type DeleteObjectResponse struct { Body []byte HTTPResponse *http.Response @@ -753,6 +1052,49 @@ func (r GetVersionResponse) StatusCode() int { return 0 } +// LoginWithBodyWithResponse request with arbitrary body returning *LoginResponse +func (c *ClientWithResponses) LoginWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*LoginResponse, error) { + rsp, err := c.LoginWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseLoginResponse(rsp) +} + +func (c *ClientWithResponses) LoginWithResponse(ctx context.Context, body LoginJSONRequestBody, reqEditors ...RequestEditorFn) (*LoginResponse, error) { + rsp, err := c.Login(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseLoginResponse(rsp) +} + +// RegisterWithBodyWithResponse request with arbitrary body returning *RegisterResponse +func (c *ClientWithResponses) RegisterWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RegisterResponse, error) { + rsp, err := c.RegisterWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRegisterResponse(rsp) +} + +func (c *ClientWithResponses) RegisterWithResponse(ctx context.Context, body RegisterJSONRequestBody, reqEditors ...RequestEditorFn) (*RegisterResponse, error) { + rsp, err := c.Register(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRegisterResponse(rsp) +} + +// GetUserInfoWithResponse request returning *GetUserInfoResponse +func (c *ClientWithResponses) GetUserInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetUserInfoResponse, error) { + rsp, err := c.GetUserInfo(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetUserInfoResponse(rsp) +} + // DeleteObjectWithResponse request returning *DeleteObjectResponse func (c *ClientWithResponses) DeleteObjectWithResponse(ctx context.Context, repository string, params *DeleteObjectParams, reqEditors ...RequestEditorFn) (*DeleteObjectResponse, error) { rsp, err := c.DeleteObject(ctx, repository, params, reqEditors...) @@ -798,6 +1140,74 @@ func (c *ClientWithResponses) GetVersionWithResponse(ctx context.Context, reqEdi return ParseGetVersionResponse(rsp) } +// ParseLoginResponse parses an HTTP response from a LoginWithResponse call +func ParseLoginResponse(rsp *http.Response) (*LoginResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &LoginResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest AuthenticationToken + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseRegisterResponse parses an HTTP response from a RegisterWithResponse call +func ParseRegisterResponse(rsp *http.Response) (*RegisterResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RegisterResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetUserInfoResponse parses an HTTP response from a GetUserInfoWithResponse call +func ParseGetUserInfoResponse(rsp *http.Response) (*GetUserInfoResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetUserInfoResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UserInfo + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseDeleteObjectResponse parses an HTTP response from a DeleteObjectWithResponse call func ParseDeleteObjectResponse(rsp *http.Response) (*DeleteObjectResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -900,6 +1310,15 @@ func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) { // ServerInterface represents all server handlers. type ServerInterface interface { + // perform a login + // (POST /auth/login) + Login(w *JiaozifsResponse, r *http.Request) + // perform user registration + // (POST /auth/register) + Register(w *JiaozifsResponse, r *http.Request) + // get information of the currently logged-in user + // (GET /auth/user) + GetUserInfo(w *JiaozifsResponse, r *http.Request) // delete object. Missing objects will not return a NotFound error. // (DELETE /repositories/{repository}/objects) DeleteObject(w *JiaozifsResponse, r *http.Request, repository string, params DeleteObjectParams) @@ -921,6 +1340,24 @@ type ServerInterface interface { type Unimplemented struct{} +// perform a login +// (POST /auth/login) +func (_ Unimplemented) Login(w *JiaozifsResponse, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// perform user registration +// (POST /auth/register) +func (_ Unimplemented) Register(w *JiaozifsResponse, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// get information of the currently logged-in user +// (GET /auth/user) +func (_ Unimplemented) GetUserInfo(w *JiaozifsResponse, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // delete object. Missing objects will not return a NotFound error. // (DELETE /repositories/{repository}/objects) func (_ Unimplemented) DeleteObject(w *JiaozifsResponse, r *http.Request, repository string, params DeleteObjectParams) { @@ -959,6 +1396,53 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler +// Login operation middleware +func (siw *ServerInterfaceWrapper) Login(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.Login(&JiaozifsResponse{w}, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// Register operation middleware +func (siw *ServerInterfaceWrapper) Register(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.Register(&JiaozifsResponse{w}, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetUserInfo operation middleware +func (siw *ServerInterfaceWrapper) GetUserInfo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{}) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetUserInfo(&JiaozifsResponse{w}, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + // DeleteObject operation middleware func (siw *ServerInterfaceWrapper) DeleteObject(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -974,9 +1458,9 @@ func (siw *ServerInterfaceWrapper) DeleteObject(w http.ResponseWriter, r *http.R return } - ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{"aaaa"}) + ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{}) - ctx = context.WithValue(ctx, Basic_authScopes, []string{"aaaaa"}) + ctx = context.WithValue(ctx, Basic_authScopes, []string{}) // Parameter object where we will unmarshal all parameters from the context var params DeleteObjectParams @@ -1022,9 +1506,9 @@ func (siw *ServerInterfaceWrapper) GetObject(w http.ResponseWriter, r *http.Requ return } - ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{"aaaa"}) + ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{}) - ctx = context.WithValue(ctx, Basic_authScopes, []string{"aaaaa"}) + ctx = context.WithValue(ctx, Basic_authScopes, []string{}) // Parameter object where we will unmarshal all parameters from the context var params GetObjectParams @@ -1099,9 +1583,9 @@ func (siw *ServerInterfaceWrapper) HeadObject(w http.ResponseWriter, r *http.Req return } - ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{"aaaa"}) + ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{}) - ctx = context.WithValue(ctx, Basic_authScopes, []string{"aaaaa"}) + ctx = context.WithValue(ctx, Basic_authScopes, []string{}) // Parameter object where we will unmarshal all parameters from the context var params HeadObjectParams @@ -1168,9 +1652,9 @@ func (siw *ServerInterfaceWrapper) UploadObject(w http.ResponseWriter, r *http.R return } - ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{"aaaa"}) + ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{}) - ctx = context.WithValue(ctx, Basic_authScopes, []string{"aaaaa"}) + ctx = context.WithValue(ctx, Basic_authScopes, []string{}) // Parameter object where we will unmarshal all parameters from the context var params UploadObjectParams @@ -1360,6 +1844,15 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl ErrorHandlerFunc: options.ErrorHandlerFunc, } + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/auth/login", wrapper.Login) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/auth/register", wrapper.Register) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/auth/user", wrapper.GetUserInfo) + }) r.Group(func(r chi.Router) { r.Delete(options.BaseURL+"/repositories/{repository}/objects", wrapper.DeleteObject) }) @@ -1382,37 +1875,44 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "Gus4chqDEeO7Q764Wfw7AAD//58N5ajGGQAA", + "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", + "2WK0+HcAAAD//87c0PRRIwAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/swagger.yml b/api/swagger.yml index 1ab95d18..5fab774d 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -13,8 +13,8 @@ servers: description: jiaozifs server endpoint security: - - jwt_token: ["aaaa"] - - basic_auth: ["aaaaa"] + - jwt_token: [] + - basic_auth: [] components: securitySchemes: basic_auth: @@ -37,6 +37,80 @@ components: in: cookie name: saml_auth_session schemas: + UserUpdate: + type: object + required: + - username + - email + properties: + username: + type: string + email: + type: string + format: email + password: + type: string + minLength: 8 + + UserInfo: + type: object + required: + - username + - email + properties: + username: + type: string + email: + type: string + format: email + currentSignInAt: + type: string + format: date-time + lastSignInAt: + type: string + format: date-time + currentSignInIP: + type: string + format: ipv4 + lastSignInIP: + type: string + format: ipv4 + createdAt: + type: string + format: date-time + updateAt: + type: string + format: date-time + + UserRegisterInfo: + type: object + required: + - username + - email + - password + properties: + username: + type: string + password: + type: string + minLength: 8 + email: + type: string + format: email + + AuthenticationToken: + type: object + required: + - token + properties: + token: + description: a JWT token that could be used to authenticate requests + type: string + token_expiration: + type: integer + format: int64 + description: Unix Epoch in seconds + VersionResult: type: object required: @@ -379,3 +453,84 @@ paths: description: NotFound 420: description: too many requests + + /auth/login: + post: + tags: + - auth + operationId: login + summary: perform a login + security: [] # No authentication + requestBody: + content: + application/json: + schema: + type: object + required: + - username + - password + properties: + username: + type: string + password: + type: string + responses: + 200: + description: successful login + headers: + Set-Cookie: + schema: + type: string + example: "access_token=abcde12356; Path=/; HttpOnly" + content: + application/json: + schema: + $ref: "#/components/schemas/AuthenticationToken" + 401: + description: Unauthorized ValidationError + 420: + description: too many requests + default: + description: Internal Server Error + + /auth/register: + post: + tags: + - auth + operationId: register + summary: perform user registration + security: [] # No authentication + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UserRegisterInfo" + responses: + 201: + description: registration success + 400: + description: Bad Request - Validation Error + 420: + description: too many requests + default: + description: Internal Server Error + + /auth/user: + get: + tags: + - auth + operationId: getUserInfo + summary: get information of the currently logged-in user + security: + - jwt_token: [] + responses: + 200: + description: Successful get of user Info + content: + application/json: + schema: + $ref: "#/components/schemas/UserInfo" + 401: + description: Unauthorized + default: + description: Internal Server Error diff --git a/auth/auth_test.go b/auth/auth_test.go new file mode 100644 index 00000000..63b0d708 --- /dev/null +++ b/auth/auth_test.go @@ -0,0 +1,71 @@ +package auth + +import ( + "context" + "fmt" + "testing" + + "github.com/brianvoe/gofakeit/v6" + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + "github.com/jiaozifs/jiaozifs/config" + "github.com/jiaozifs/jiaozifs/models" + "github.com/jiaozifs/jiaozifs/models/migrations" + "github.com/phayes/freeport" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun" + "go.uber.org/fx/fxtest" +) + +var testConnTmpl = "postgres://postgres:postgres@localhost:%d/jiaozifs?sslmode=disable" + +func setup(ctx context.Context, t *testing.T) (*embeddedpostgres.EmbeddedPostgres, *bun.DB) { + port, err := freeport.GetFreePort() + require.NoError(t, err) + postgres := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig().Port(uint32(port)).Database("jiaozifs")) + err = postgres.Start() + require.NoError(t, err) + + db, err := models.SetupDatabase(ctx, fxtest.NewLifecycle(t), &config.DatabaseConfig{Debug: true, Connection: fmt.Sprintf(testConnTmpl, port)}) + require.NoError(t, err) + + err = migrations.MigrateDatabase(ctx, db) + require.NoError(t, err) + return postgres, db +} + +func TestLogin_Success(t *testing.T) { + ctx := context.Background() + postgres, db := setup(ctx, t) + defer postgres.Stop() //nolint + // repo + mockRepo := models.NewUserRepo(db) + // config + mockConfig := &config.Config{Auth: config.AuthConfig{SecretKey: []byte("THIS_MUST_BE_CHANGED_IN_PRODUCTION")}} + // user + userModel := &models.User{} + require.NoError(t, gofakeit.Struct(userModel)) + + // registration + register := &Register{ + Username: userModel.Name, + Email: userModel.Email, + Password: userModel.EncryptedPassword, + } + err := register.Register(ctx, mockRepo) + require.NoError(t, err) + + // login + login := &Login{ + Username: userModel.Name, + Password: userModel.EncryptedPassword, + } + token, err := login.Login(context.Background(), mockRepo, mockConfig) + require.NoError(t, err, "Login should not return an error") + require.NotEmpty(t, token.Token, "Token should not be empty") + require.NotNil(t, token.TokenExpiration, "Token expiration should not be nil") + // profile + userInfo := &UserInfo{Token: token.Token} + profile, err := userInfo.UserProfile(ctx, mockRepo, mockConfig) + require.NoError(t, err) + require.NotEmpty(t, profile) +} diff --git a/auth/basic_auth.go b/auth/basic_auth.go index 8832b06d..d69d3678 100644 --- a/auth/basic_auth.go +++ b/auth/basic_auth.go @@ -1 +1,147 @@ package auth + +import ( + "context" + "time" + + "github.com/jiaozifs/jiaozifs/config" + + "github.com/golang-jwt/jwt" + openapi_types "github.com/oapi-codegen/runtime/types" + + "github.com/go-openapi/swag" + logging "github.com/ipfs/go-log/v2" + "github.com/jiaozifs/jiaozifs/api" + "github.com/jiaozifs/jiaozifs/models" + "golang.org/x/crypto/bcrypt" +) + +var log = logging.Logger("auth") + +type Login struct { + Username string `json:"username"` + Password string `json:"password"` +} + +func (l *Login) Login(ctx context.Context, repo models.IUserRepo, config *config.Config) (token api.AuthenticationToken, err error) { + // Get user encryptedPassword by username + ep, err := repo.GetEPByName(ctx, l.Username) + if err != nil { + log.Errorf("username err: %s", err) + return token, err + } + + // Compare ep and password + err = bcrypt.CompareHashAndPassword([]byte(ep), []byte(l.Password)) + if err != nil { + log.Errorf("password err: %s", err) + return token, err + } + // Generate user token + loginTime := time.Now() + expires := loginTime.Add(expirationDuration) + secretKey := config.Auth.SecretKey + + tokenString, err := GenerateJWTLogin(secretKey, l.Username, loginTime, expires) + if err != nil { + log.Errorf("generate token err: %s", err) + return token, err + } + + log.Info("login successful") + + token.Token = tokenString + token.TokenExpiration = swag.Int64(expires.Unix()) + + return token, nil +} + +type Register struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} + +func (r *Register) Register(ctx context.Context, repo models.IUserRepo) (err error) { + // check username, email + _, err1 := repo.GetUserByName(ctx, r.Username) + _, err2 := repo.GetUserByEmail(ctx, r.Email) + if err1 == nil || err2 == nil { + err = ErrInvalidNameEmail + log.Error(ErrInvalidNameEmail) + return + } + // reserve temporarily + password, err := bcrypt.GenerateFromPassword([]byte(r.Password), passwordCost) + if err != nil { + log.Error(ErrComparePassword) + return + } + + // insert db + user := &models.User{ + Name: r.Username, + Email: r.Email, + EncryptedPassword: string(password), + CurrentSignInAt: time.Time{}, + LastSignInAt: time.Time{}, + CurrentSignInIP: "", + LastSignInIP: "", + CreatedAt: time.Now(), + UpdatedAt: time.Time{}, + } + insertUser, err := repo.Insert(ctx, user) + if err != nil { + log.Error("create user error") + return + } + // return + log.Infof("%s registration success", insertUser.Name) + return nil +} + +type UserInfo struct { + Token string `json:"token"` +} + +func (u *UserInfo) UserProfile(ctx context.Context, repo models.IUserRepo, config *config.Config) (api.UserInfo, error) { + userInfo := api.UserInfo{} + // Parse JWT Token + token, err := jwt.Parse(u.Token, func(token *jwt.Token) (interface{}, error) { + return config.Auth.SecretKey, nil + }) + if err != nil { + log.Error(ErrParseToken) + return userInfo, err + } + // Check Token validity + if !token.Valid { + log.Error(ErrInvalidToken) + return userInfo, ErrInvalidToken + } + // Get username by token + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + log.Error(ErrExtractClaims) + return userInfo, ErrExtractClaims + } + username := claims["sub"].(string) + + // Get user by username + user, err := repo.GetUserByName(ctx, username) + if err != nil { + return userInfo, err + } + userInfo = api.UserInfo{ + CreatedAt: &user.CreatedAt, + CurrentSignInAt: &user.CurrentSignInAt, + CurrentSignInIP: &user.CurrentSignInIP, + Email: openapi_types.Email(user.Email), + LastSignInAt: &user.LastSignInAt, + LastSignInIP: &user.LastSignInIP, + UpdateAt: &user.UpdatedAt, + Username: user.Name, + } + log.Info("get user profile success") + return userInfo, nil +} diff --git a/auth/errors.go b/auth/errors.go new file mode 100644 index 00000000..3e824527 --- /dev/null +++ b/auth/errors.go @@ -0,0 +1,11 @@ +package auth + +import "errors" + +var ( + ErrComparePassword = errors.New("compare password error") + ErrParseToken = errors.New("parse token error") + ErrInvalidToken = errors.New("invalid token") + ErrInvalidNameEmail = errors.New("invalid name or email") + ErrExtractClaims = errors.New("failed to extract claims from JWT token") +) diff --git a/auth/jwt_login.go b/auth/jwt_login.go new file mode 100644 index 00000000..0dbd533f --- /dev/null +++ b/auth/jwt_login.go @@ -0,0 +1,27 @@ +package auth + +import ( + "time" + + "github.com/golang-jwt/jwt" + "github.com/google/uuid" +) + +const ( + LoginAudience = "login" +) + +// GenerateJWTLogin creates a jwt token which can be used for authentication during login only, i.e. it will not work for password reset. +// It supports backward compatibility for creating a login jwt. The audience is not set for login token. Any audience will make the token +// invalid for login. No email is passed to support the ability of login for users via user/access keys which don't have an email yet +func GenerateJWTLogin(secret []byte, userID string, issuedAt, expiresAt time.Time) (string, error) { + claims := jwt.MapClaims{ + "id": uuid.NewString(), + "aud": LoginAudience, + "sub": userID, + "iat": issuedAt.Unix(), + "exp": expiresAt.Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(secret) +} diff --git a/auth/types.go b/auth/types.go new file mode 100644 index 00000000..badea89f --- /dev/null +++ b/auth/types.go @@ -0,0 +1,8 @@ +package auth + +import "time" + +const ( + expirationDuration = time.Hour + passwordCost = 12 +) diff --git a/cmd/daemon.go b/cmd/daemon.go index cd5e8038..425a0e4c 100644 --- a/cmd/daemon.go +++ b/cmd/daemon.go @@ -52,10 +52,10 @@ var daemonCmd = &cobra.Command{ //database fx_opt.Override(new(*bun.DB), models.SetupDatabase), fx_opt.Override(fx_opt.NextInvoke(), migrations.MigrateDatabase), - fx_opt.Override(new(*models.IUserRepo), models.NewUserRepo), - fx_opt.Override(new(*models.IRepositoryRepo), models.NewRepositoryRepo), - fx_opt.Override(new(*models.IRefRepo), models.NewRefRepo), - fx_opt.Override(new(*models.IObjectRepo), models.NewObjectRepo), + fx_opt.Override(new(models.IUserRepo), models.NewUserRepo), + fx_opt.Override(new(models.IRepositoryRepo), models.NewRepositoryRepo), + fx_opt.Override(new(models.IRefRepo), models.NewRefRepo), + fx_opt.Override(new(models.IObjectRepo), models.NewObjectRepo), //api fx_opt.Override(fx_opt.NextInvoke(), apiImpl.SetupAPI), diff --git a/config/config.go b/config/config.go index 0f034e2c..5494619f 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,7 @@ type Config struct { Log LogConfig `mapstructure:"log"` API APIConfig `mapstructure:"api"` Database DatabaseConfig `mapstructure:"database"` + Auth AuthConfig `mapstructure:"auth"` Blockstore BlockStoreConfig `mapstructure:"blockstore"` } @@ -32,6 +33,10 @@ type DatabaseConfig struct { Debug bool `mapstructure:"debug"` } +type AuthConfig struct { + SecretKey []byte `mapstructure:"secretKey"` +} + func InitConfig() error { // Find home directory. home, err := os.UserHomeDir() diff --git a/config/default.go b/config/default.go index 47d6d8bf..b1709816 100644 --- a/config/default.go +++ b/config/default.go @@ -25,4 +25,7 @@ var defaultCfg = Config{ AllowedExternalPrefixes []string }{Path: DefaultLocalBSPath, ImportEnabled: false, ImportHidden: false, AllowedExternalPrefixes: nil}), }, + Auth: AuthConfig{ + SecretKey: []byte("THIS_MUST_BE_CHANGED_IN_PRODUCTION"), + }, } diff --git a/controller/user_ctl.go b/controller/user_ctl.go new file mode 100644 index 00000000..221af5e6 --- /dev/null +++ b/controller/user_ctl.go @@ -0,0 +1,75 @@ +package controller + +import ( + "encoding/json" + "net/http" + + "github.com/jiaozifs/jiaozifs/config" + "github.com/jiaozifs/jiaozifs/models" + + "github.com/jiaozifs/jiaozifs/api" + "github.com/jiaozifs/jiaozifs/auth" + "go.uber.org/fx" +) + +type UserController struct { + fx.In + + Repo models.IUserRepo + Config *config.Config +} + +func (A UserController) Login(w *api.JiaozifsResponse, r *http.Request) { + ctx := r.Context() + // Decode requestBody + var login auth.Login + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&login); err != nil { + w.RespError(err) + return + } + + // Perform login + resp, err := login.Login(ctx, A.Repo, A.Config) + if err != nil { + w.RespError(err) + return + } + + // resp + w.RespJSON(resp) +} + +func (A UserController) Register(w *api.JiaozifsResponse, r *http.Request) { + ctx := r.Context() + // Decode requestBody + var register auth.Register + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(®ister); err != nil { + w.RespError(err) + } + // Perform register + err := register.Register(ctx, A.Repo) + if err != nil { + w.RespError(err) + return + } + // resp + w.RespJSON("registration success") +} + +func (A UserController) GetUserInfo(w *api.JiaozifsResponse, r *http.Request) { + ctx := r.Context() + // Get token from Header + tokenString := r.Header.Get("Authorization") + userInfo := &auth.UserInfo{Token: tokenString} + + // Perform GetUserInfo + info, err := userInfo.UserProfile(ctx, A.Repo, A.Config) + if err != nil { + w.RespError(err) + return + } + // resp + w.RespJSON(info) +} diff --git a/go.mod b/go.mod index 9147bc05..295405ca 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,9 @@ require ( github.com/getkin/kin-openapi v0.118.0 github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/cors v1.2.1 + github.com/go-openapi/swag v0.22.4 github.com/go-test/deep v1.1.0 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.4.0 github.com/hnlq715/golang-lru v0.4.0 @@ -47,6 +49,7 @@ require ( github.com/uptrace/bun/driver/pgdriver v1.1.16 github.com/uptrace/bun/extra/bundebug v1.1.16 go.uber.org/fx v1.20.1 + golang.org/x/crypto v0.16.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/oauth2 v0.13.0 google.golang.org/api v0.147.0 @@ -90,7 +93,6 @@ require ( github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/swag v0.22.4 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.0.0 // indirect @@ -158,13 +160,12 @@ require ( go.uber.org/dig v1.17.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.23.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.13.0 // indirect + golang.org/x/tools v0.16.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect diff --git a/go.sum b/go.sum index 6e6f6997..5897a877 100644 --- a/go.sum +++ b/go.sum @@ -185,6 +185,8 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -497,8 +499,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -536,8 +538,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -572,8 +574,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -596,8 +598,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -643,8 +645,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -709,8 +711,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/models/user.go b/models/user.go index 87996a0b..1104a7df 100644 --- a/models/user.go +++ b/models/user.go @@ -26,6 +26,10 @@ type User struct { type IUserRepo interface { Get(ctx context.Context, id uuid.UUID) (*User, error) Insert(ctx context.Context, user *User) (*User, error) + + GetEPByName(ctx context.Context, name string) (string, error) + GetUserByName(ctx context.Context, name string) (*User, error) + GetUserByEmail(ctx context.Context, email string) (*User, error) } var _ IUserRepo = (*UserRepo)(nil) @@ -50,3 +54,22 @@ func (userRepo *UserRepo) Insert(ctx context.Context, user *User) (*User, error) } return user, nil } + +func (userRepo *UserRepo) GetEPByName(ctx context.Context, name string) (string, error) { + var ep string + return ep, userRepo.DB.NewSelect(). + Model((*User)(nil)).Column("encrypted_password"). + Where("name = ?", name). + Scan(ctx, &ep) +} + +func (userRepo *UserRepo) GetUserByName(ctx context.Context, name string) (*User, error) { + user := &User{} + return user, userRepo.DB.NewSelect().Model(user).Where("name = ?", name).Scan(ctx) +} + +func (userRepo *UserRepo) GetUserByEmail(ctx context.Context, email string) (*User, error) { + user := &User{} + return user, userRepo.DB.NewSelect(). + Model(user).Where("email = ?", email).Scan(ctx) +} diff --git a/models/user_test.go b/models/user_test.go index 40468bd3..d0ffc743 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -63,4 +63,16 @@ func TestNewUserRepo(t *testing.T) { require.NoError(t, err) require.True(t, cmp.Equal(userModel.UpdatedAt, user.UpdatedAt, dbTimeCmpOpt)) + + ep, err := repo.GetEPByName(ctx, newUser.Name) + require.NoError(t, err) + require.True(t, cmp.Equal(userModel.EncryptedPassword, ep)) + + userByEmail, err := repo.GetUserByEmail(ctx, newUser.Email) + require.NoError(t, err) + require.True(t, cmp.Equal(userModel.UpdatedAt, userByEmail.UpdatedAt, dbTimeCmpOpt)) + + userByName, err := repo.GetUserByName(ctx, newUser.Name) + require.NoError(t, err) + require.True(t, cmp.Equal(userModel.UpdatedAt, userByName.UpdatedAt, dbTimeCmpOpt)) }