From 9f2d2dde5f4e210b61fc588cd40cab572f917eff Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 18:19:03 +0100 Subject: [PATCH 01/45] add deprecation warning to profile spec --- api/profile.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/profile.go b/api/profile.go index 622e11b..f3d0530 100644 --- a/api/profile.go +++ b/api/profile.go @@ -24,6 +24,7 @@ func (h *ProfileHandler) getName() string { return "profile" } func (h *ProfileHandler) getSpec() Spec { spec := newSpecBase(h) + spec.Info.Description = "DEPRECATED " + spec.Info.Description userSchema := openapi3.NewObjectSchema(). WithProperty("id", openapi3.NewInt32Schema()). From 882f90a8889abc754d2a61edcc0ddd26918f6d8e Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 18:27:20 +0100 Subject: [PATCH 02/45] add security schema --- api/email.go | 8 ++------ api/handler.go | 16 ++++++++++++++++ api/profile.go | 8 ++------ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/api/email.go b/api/email.go index 98fcdcb..e4458c2 100644 --- a/api/email.go +++ b/api/email.go @@ -40,12 +40,8 @@ func (h *EmailHandler) getSpec() Spec { WithProperty("username", openapi3.NewStringSchema()). WithProperty("password", openapi3.NewStringSchema()) - spec.Components = &openapi3.Components{ - Schemas: openapi3.Schemas{ - "MailAccount": &openapi3.SchemaRef{Value: accountSchema}, - "AddAccountPayload": &openapi3.SchemaRef{Value: addAccountSchema}, - }, - } + spec.Components.Schemas["MailAccount"] = &openapi3.SchemaRef{Value: accountSchema} + spec.Components.Schemas["AddAccountPayload"] = &openapi3.SchemaRef{Value: addAccountSchema} spec.AddOperation("/", http.MethodGet, &openapi3.Operation{ Tags: []string{"email"}, diff --git a/api/handler.go b/api/handler.go index 74f8e20..dfe674f 100644 --- a/api/handler.go +++ b/api/handler.go @@ -86,6 +86,22 @@ func newSpecBase(handler Handler) Spec { }, } + securitySchemeName := "bearerAuth" + spec.Components = &openapi3.Components{ + SecuritySchemes: openapi3.SecuritySchemes{ + securitySchemeName: &openapi3.SecuritySchemeRef{ + Value: &openapi3.SecurityScheme{ + Type: "http", + Scheme: "bearer", + BearerFormat: "JWT", + Description: "Enter your bearer token in the format: Bearer ", + }, + }, + }, + } + + spec.Security = openapi3.SecurityRequirements{{securitySchemeName: []string{}}} + spec.AddServer(&openapi3.Server{ URL: fmt.Sprintf("https://api.piquel.fr/%s", handler.getName()), Description: "Main production endpoints", diff --git a/api/profile.go b/api/profile.go index f3d0530..f4626a9 100644 --- a/api/profile.go +++ b/api/profile.go @@ -42,12 +42,8 @@ func (h *ProfileHandler) getSpec() Spec { WithProperty("image", openapi3.NewStringSchema()). WithRequired([]string{"username", "name", "image"}) - spec.Components = &openapi3.Components{ - Schemas: openapi3.Schemas{ - "User": &openapi3.SchemaRef{Value: userSchema}, - "UpdateUserParams": &openapi3.SchemaRef{Value: updateUserSchema}, - }, - } + spec.Components.Schemas["User"] = &openapi3.SchemaRef{Value: userSchema} + spec.Components.Schemas["UpdateUserParams"] = &openapi3.SchemaRef{Value: updateUserSchema} spec.AddOperation("/{user}", http.MethodGet, &openapi3.Operation{ Tags: []string{"users"}, From 5cbcfd105725db8bfd73593470b9eae66d1df764 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:17:47 +0100 Subject: [PATCH 03/45] add new user service --- api/handler.go | 3 +- main.go | 4 ++- services/users/users.go | 67 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 services/users/users.go diff --git a/api/handler.go b/api/handler.go index dfe674f..93db20f 100644 --- a/api/handler.go +++ b/api/handler.go @@ -9,6 +9,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/email" + "github.com/piquel-fr/api/services/users" "github.com/piquel-fr/api/utils/middleware" ) @@ -20,7 +21,7 @@ type Handler interface { createHttpHandler() http.Handler } -func CreateRouter(authService auth.AuthService, emailService email.EmailService) (http.Handler, error) { +func CreateRouter(userService users.UserService, authService auth.AuthService, emailService email.EmailService) (http.Handler, error) { // these routes are unauthenticated and should remail so. // do not any other routes to this router. all other routes // should be added to createProtectedRouter diff --git a/main.go b/main.go index b9b92af..828d100 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/email" + "github.com/piquel-fr/api/services/users" gh "github.com/piquel-fr/api/utils/github" "github.com/piquel-fr/api/utils/oauth" ) @@ -24,10 +25,11 @@ func main() { database.InitDatabase() defer database.Connection.Close() + userService := users.NewRealUserService() authService := auth.NewRealAuthService() emailService := email.NewRealEmailService() - router, err := api.CreateRouter(authService, emailService) + router, err := api.CreateRouter(userService, authService, emailService) if err != nil { panic(err) } diff --git a/services/users/users.go b/services/users/users.go new file mode 100644 index 0000000..80e0358 --- /dev/null +++ b/services/users/users.go @@ -0,0 +1,67 @@ +package users + +import ( + "context" + + "github.com/piquel-fr/api/database/repository" +) + +type UserService interface { + // getting the user + GetUserById(ctx context.Context, id int32) (*repository.User, error) + GetUserByUsername(ctx context.Context, username string) (*repository.User, error) + GetUserByEmail(ctx context.Context, email string) (*repository.User, error) + + // managing users + UpdateUser(ctx context.Context, params repository.UpdateUserParams) error + UpdateUserAdmin(ctx context.Context, params repository.UpdateUserAdminParams) error + RegisterUser(ctx context.Context, email, username, name, image, role string) error // does not validate the role + DeleteUser(ctx context.Context, id int32) error + + // other + ValidateUsername(username string) error + ListUsers(ctx context.Context, offset, limit int) ([]repository.User, error) +} + +type realUserService struct{} + +func NewRealUserService() *realUserService { + return &realUserService{} +} + +func (s *realUserService) GetUserById(ctx context.Context, id int32) (*repository.User, error) { + return nil, nil +} + +func (s *realUserService) GetUserByUsername(ctx context.Context, username string) (*repository.User, error) { + return nil, nil +} + +func (s *realUserService) GetUserByEmail(ctx context.Context, email string) (*repository.User, error) { + return nil, nil +} + +func (s *realUserService) UpdateUser(ctx context.Context, params repository.UpdateUserParams) error { + return nil +} + +func (s *realUserService) UpdateUserAdmin(ctx context.Context, params repository.UpdateUserAdminParams) error { + return nil +} + +// does not validate the role +func (s *realUserService) RegisterUser(ctx context.Context, email, username, name, image, role string) error { + return nil +} + +func (s *realUserService) DeleteUser(ctx context.Context, id int32) error { + return nil +} + +func (s *realUserService) ValidateUsername(username string) error { + return nil +} + +func (s *realUserService) ListUsers(ctx context.Context, offset, limit int) ([]repository.User, error) { + return nil, nil +} From 5d5f976aa0559d1bfeb697786237e239e784db56 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:18:34 +0100 Subject: [PATCH 04/45] add new user handler --- api/handler.go | 1 + api/users.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 api/users.go diff --git a/api/handler.go b/api/handler.go index 93db20f..18e9475 100644 --- a/api/handler.go +++ b/api/handler.go @@ -30,6 +30,7 @@ func CreateRouter(userService users.UserService, authService auth.AuthService, e router.Handle("/auth/", http.StripPrefix("/auth", CreateAuthHandler(authService).createHttpHandler())) handlers := []Handler{ + CreateUserHandler(authService), CreateProfileHandler(authService), CreateEmailHandler(authService, emailService), } diff --git a/api/users.go b/api/users.go new file mode 100644 index 0000000..b1eb06e --- /dev/null +++ b/api/users.go @@ -0,0 +1,156 @@ +package api + +import ( + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/piquel-fr/api/services/auth" + "github.com/piquel-fr/api/services/users" + "github.com/piquel-fr/api/utils/middleware" +) + +type UserHandler struct { + userService users.UserService + authService auth.AuthService +} + +func CreateUserHandler(userService users.UserService, authService auth.AuthService) *UserHandler { + return &UserHandler{userService, authService} +} + +func (h *UserHandler) getName() string { return "users" } + +func (h *UserHandler) getSpec() Spec { + spec := newSpecBase(h) + + userSchema := openapi3.NewObjectSchema(). + WithProperty("id", openapi3.NewInt32Schema()). + WithProperty("username", openapi3.NewStringSchema()). + WithProperty("name", openapi3.NewStringSchema()). + WithProperty("image", openapi3.NewStringSchema()). + WithProperty("email", openapi3.NewStringSchema().WithFormat("email")). + WithProperty("role", openapi3.NewStringSchema()). + WithProperty("createdAt", openapi3.NewDateTimeSchema()). + WithRequired([]string{"id", "username", "name", "image", "role", "createdAt"}) + + updateUserSchema := openapi3.NewObjectSchema(). + WithProperty("username", openapi3.NewStringSchema()). + WithProperty("name", openapi3.NewStringSchema()). + WithProperty("image", openapi3.NewStringSchema()). + WithRequired([]string{"username", "name", "image"}) + + spec.Components.Schemas["User"] = &openapi3.SchemaRef{Value: userSchema} + spec.Components.Schemas["UpdateUserParams"] = &openapi3.SchemaRef{Value: updateUserSchema} + + spec.AddOperation("/self", http.MethodGet, &openapi3.Operation{ + Tags: []string{"users"}, + Summary: "Get self user object", + Description: "Get the user that is associated with the auth", + OperationID: "get-self", + Responses: openapi3.NewResponses( + openapi3.WithStatus(200, &openapi3.ResponseRef{ + Value: openapi3.NewResponse().WithDescription("User profile found").WithJSONSchemaRef(openapi3.NewSchemaRef("#/components/schemas/User", userSchema)), + }), + ), + }) + + spec.AddOperation("/{user}", http.MethodGet, &openapi3.Operation{ + Tags: []string{"users"}, + Summary: "Get specific user", + Description: "Get the profile of the user specified in the path", + OperationID: "get-user-by-path", + Parameters: openapi3.Parameters{ + &openapi3.ParameterRef{ + Value: &openapi3.Parameter{ + Name: "user", + In: "path", + Required: true, + Description: "The username", + Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}, + }, + }, + }, + Responses: openapi3.NewResponses( + openapi3.WithStatus(200, &openapi3.ResponseRef{ + Value: openapi3.NewResponse().WithDescription("User profile found").WithJSONSchemaRef(openapi3.NewSchemaRef("#/components/schemas/User", userSchema)), + }), + ), + }) + + spec.AddOperation("/{user}", http.MethodPut, &openapi3.Operation{ + Tags: []string{"users"}, + Summary: "Update user", + Description: "Update the profile of the specified user", + OperationID: "update-user", + Parameters: openapi3.Parameters{ + &openapi3.ParameterRef{ + Value: &openapi3.Parameter{ + Name: "user", + In: "path", + Required: true, + Description: "The username to update", + Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}, + }, + }, + }, + RequestBody: &openapi3.RequestBodyRef{ + Value: &openapi3.RequestBody{ + Required: true, + Content: openapi3.NewContentWithJSONSchemaRef( + openapi3.NewSchemaRef("#/components/schemas/UpdateUserParams", updateUserSchema), + ), + }, + }, + Responses: openapi3.NewResponses( + openapi3.WithStatus(200, &openapi3.ResponseRef{Value: openapi3.NewResponse().WithDescription("User updated successfully")}), + openapi3.WithStatus(400, &openapi3.ResponseRef{Value: openapi3.NewResponse().WithContent(openapi3.NewContentWithSchema(openapi3.NewStringSchema(), []string{"text/plain"})).WithDescription("Invalid input or json")}), + ), + }) + + spec.AddOperation("/{user}", http.MethodDelete, &openapi3.Operation{ + Tags: []string{"users"}, + Summary: "Delete user", + Description: "Delete the user specified in the path", + OperationID: "delete-user", + Parameters: openapi3.Parameters{ + &openapi3.ParameterRef{ + Value: &openapi3.Parameter{ + Name: "user", + In: "path", + Required: true, + Description: "The username", + Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}, + }, + }, + }, + Responses: openapi3.NewResponses( + openapi3.WithStatus(200, &openapi3.ResponseRef{Value: openapi3.NewResponse().WithDescription("User deleted successfully")}), + openapi3.WithStatus(401, &openapi3.ResponseRef{Value: openapi3.NewResponse().WithContent(openapi3.NewContentWithSchema(openapi3.NewStringSchema(), []string{"text/plain"})).WithDescription("Unauthorized")}), + ), + }) + + return spec +} + +func (h *UserHandler) createHttpHandler() http.Handler { + handler := http.NewServeMux() + + handler.HandleFunc("GET /self", h.handleGetSelf) + handler.HandleFunc("GET /{user}", h.handleGetUser) + handler.HandleFunc("PUT /{user}", h.handlePutUser) + handler.HandleFunc("DELETE /{user}", h.handleDeleteUser) + + // TODO: add spec entry + handler.HandleFunc("PUT /{user}/admin", h.handlePutUserAdmin) + + handler.Handle("OPTIONS /self", middleware.CreateOptionsHandler("GET")) + handler.Handle("OPTIONS /{user}", middleware.CreateOptionsHandler("GET", "PUT", "DELETE")) + + return handler +} + +func (h *UserHandler) handleGetSelf(w http.ResponseWriter, r *http.Request) {} +func (h *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {} // TODO: check for view email address permission, if false return empty email address +func (h *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {} // TODO: implement loads of validation for username (like make blacklist) +func (h *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {} +func (h *UserHandler) handlePutUserAdmin(w http.ResponseWriter, r *http.Request) {} // TODO: allow updating role and email From 82797383220084349b98fa4065f9fb58ec90afd6 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:18:40 +0100 Subject: [PATCH 05/45] add new authservice interface to work with new user service --- services/auth/auth.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/services/auth/auth.go b/services/auth/auth.go index 58ee9dc..7ac9ea1 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -30,6 +30,25 @@ type AuthService interface { GetPolicy() *PolicyConfiguration } +// TEMP: the new auth interface that will be made alongside the new user service +type NewAuthService interface { + GetPolicy() *PolicyConfiguration + GetProvider(name string) (oauth.Provider, error) + + // token management + GenerateToken(user *repository.User) *jwt.Token // TODO: also save expiry and refresh + signToken(token *jwt.Token) (string, error) + getTokenFromRequest(r *http.Request) (*jwt.Token, error) + + // authentication + GetUserFromContext(ctx context.Context) (*repository.User, error) // gets user from context (should be saved there by auth middleware) + GetUserFromOAuthUser() + + // authorization + Authorize(request *Request) error + AuthMiddleware(next http.Handler) http.Handler +} + // auth service has no state type realAuthService struct{} From be21c0716fbe5d366a12e0a3ef56fbb998ac3f47 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:18:55 +0100 Subject: [PATCH 06/45] add new admin user update query --- database/queries/users.sql | 3 +++ database/repository/users.sql.go | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/database/queries/users.sql b/database/queries/users.sql index 607073f..e35fa3d 100644 --- a/database/queries/users.sql +++ b/database/queries/users.sql @@ -13,3 +13,6 @@ SELECT * FROM "users" WHERE "email" = $1; -- name: UpdateUser :exec UPDATE "users" SET "username" = $2, "name" = $3, "image" = $4 WHERE "id" = $1; + +-- name: UpdateUserAdmin :exec +UPDATE "users" SET "username" = $2, "email" = $3, "name" = $4, "image" = $5, "role" = $6 WHERE "id" = $1; diff --git a/database/repository/users.sql.go b/database/repository/users.sql.go index 1c1dd35..b596d8e 100644 --- a/database/repository/users.sql.go +++ b/database/repository/users.sql.go @@ -120,3 +120,28 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { ) return err } + +const updateUserAdmin = `-- name: UpdateUserAdmin :exec +UPDATE "users" SET "username" = $2, "email" = $3, "name" = $4, "image" = $5, "role" = $6 WHERE "id" = $1 +` + +type UpdateUserAdminParams struct { + ID int32 `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + Image string `json:"image"` + Role string `json:"role"` +} + +func (q *Queries) UpdateUserAdmin(ctx context.Context, arg UpdateUserAdminParams) error { + _, err := q.db.Exec(ctx, updateUserAdmin, + arg.ID, + arg.Username, + arg.Email, + arg.Name, + arg.Image, + arg.Role, + ) + return err +} From 78b5a71dc394c759d5199c2dd6ae015fd25ef731 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:28:49 +0100 Subject: [PATCH 07/45] move policy typedefs to config --- services/auth/models.go => config/policy.go | 12 +++++---- services/auth/auth.go | 11 +++++---- services/auth/policy.go | 27 +++++++++++---------- 3 files changed, 27 insertions(+), 23 deletions(-) rename services/auth/models.go => config/policy.go (78%) diff --git a/services/auth/models.go b/config/policy.go similarity index 78% rename from services/auth/models.go rename to config/policy.go index 06af735..bb53ac4 100644 --- a/services/auth/models.go +++ b/config/policy.go @@ -1,4 +1,4 @@ -package auth +package config import ( "context" @@ -8,7 +8,7 @@ import ( type PolicyConfiguration struct { Presets map[string]*Permission `json:"presets"` - Roles Roles `json:"roles"` + Roles map[string]*Role `json:"roles"` } type Permission struct { @@ -17,16 +17,16 @@ type Permission struct { Preset string `json:"preset"` } -type Conditions []func(request *Request) error +type Conditions []func(request *AuthRequest) error -type Roles map[string]*struct { +type Role struct { Name string `json:"name"` Color string `json:"color"` Permissions map[string][]*Permission `json:"permissions"` Parents []string `json:"parents"` } -type Request struct { +type AuthRequest struct { User *repository.User Ressource Resource Actions []string @@ -37,3 +37,5 @@ type Resource interface { GetResourceName() string GetOwner() int32 } + +var Policy *PolicyConfiguration diff --git a/services/auth/auth.go b/services/auth/auth.go index 7ac9ea1..82dcafd 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -24,15 +24,15 @@ type AuthService interface { GetUserFromRequest(r *http.Request) (*repository.User, error) GetUserFromUserId(ctx context.Context, userId int32) (*repository.User, error) GetUserFromUsername(ctx context.Context, username string) (*repository.User, error) - Authorize(request *Request) error + Authorize(request *config.AuthRequest) error GetProvider(name string) (oauth.Provider, error) - GetPolicy() *PolicyConfiguration + GetPolicy() *config.PolicyConfiguration } // TEMP: the new auth interface that will be made alongside the new user service type NewAuthService interface { - GetPolicy() *PolicyConfiguration + GetPolicy() *config.PolicyConfiguration GetProvider(name string) (oauth.Provider, error) // token management @@ -45,7 +45,7 @@ type NewAuthService interface { GetUserFromOAuthUser() // authorization - Authorize(request *Request) error + Authorize(request *config.AuthRequest) error AuthMiddleware(next http.Handler) http.Handler } @@ -53,10 +53,11 @@ type NewAuthService interface { type realAuthService struct{} func NewRealAuthService() *realAuthService { + config.Policy = &policy return &realAuthService{} } -func (s *realAuthService) GetPolicy() *PolicyConfiguration { return &policy } +func (s *realAuthService) GetPolicy() *config.PolicyConfiguration { return &policy } func (s *realAuthService) GenerateTokenString(userId int32) (string, error) { idString := strconv.Itoa(int(userId)) diff --git a/services/auth/policy.go b/services/auth/policy.go index 4928ef7..543b141 100644 --- a/services/auth/policy.go +++ b/services/auth/policy.go @@ -3,6 +3,7 @@ package auth import ( "slices" + "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/services/email" "github.com/piquel-fr/api/utils/errors" @@ -25,33 +26,33 @@ const ( ActionListEmailAccounts string = "list_email_accounts" ) -func own(request *Request) error { +func own(request *config.AuthRequest) error { if request.Ressource.GetOwner() == request.User.ID { return nil } return errors.ErrorForbidden } -func makeOwn(action string) *Permission { - return &Permission{ +func makeOwn(action string) *config.Permission { + return &config.Permission{ Action: action, - Conditions: Conditions{own}, + Conditions: config.Conditions{own}, } } -var policy = PolicyConfiguration{ - Presets: map[string]*Permission{}, - Roles: Roles{ +var policy = config.PolicyConfiguration{ + Presets: map[string]*config.Permission{}, + Roles: map[string]*config.Role{ RoleSystem: { Name: "System", Color: "gray", - Permissions: map[string][]*Permission{}, + Permissions: map[string][]*config.Permission{}, Parents: []string{RoleDefault, RoleDeveloper, RoleAdmin}, }, RoleAdmin: { Name: "Admin", Color: "red", - Permissions: map[string][]*Permission{ + Permissions: map[string][]*config.Permission{ repository.ResourceUser: { {Action: ActionUpdate}, {Action: ActionDelete}, @@ -69,12 +70,12 @@ var policy = PolicyConfiguration{ RoleDeveloper: { Name: "Developer", Color: "blue", - Permissions: map[string][]*Permission{ + Permissions: map[string][]*config.Permission{ repository.ResourceMailAccount: { { Action: ActionView, - Conditions: Conditions{ - func(request *Request) error { + Conditions: config.Conditions{ + func(request *config.AuthRequest) error { if request.Ressource.GetOwner() == request.User.ID { return nil } @@ -103,7 +104,7 @@ var policy = PolicyConfiguration{ RoleDefault: { Name: "", Color: "gray", - Permissions: map[string][]*Permission{ + Permissions: map[string][]*config.Permission{ repository.ResourceUser: { makeOwn(ActionUpdate), makeOwn(ActionDelete), From f740138de47b00937290b8afc62fd261139cb43e Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:29:34 +0100 Subject: [PATCH 08/45] fix build error --- api/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/handler.go b/api/handler.go index 18e9475..905e0c7 100644 --- a/api/handler.go +++ b/api/handler.go @@ -30,7 +30,7 @@ func CreateRouter(userService users.UserService, authService auth.AuthService, e router.Handle("/auth/", http.StripPrefix("/auth", CreateAuthHandler(authService).createHttpHandler())) handlers := []Handler{ - CreateUserHandler(authService), + CreateUserHandler(userService, authService), CreateProfileHandler(authService), CreateEmailHandler(authService, emailService), } From 3c6561e826fc4c0d0aff0a19c999deb81b165622 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:37:00 +0100 Subject: [PATCH 09/45] add public config handler --- api/auth.go | 2 +- api/handler.go | 20 ++++++++++++++++++++ config/config.go | 13 +++++++++++++ config/policy.go | 2 -- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/api/auth.go b/api/auth.go index cee390f..3301654 100644 --- a/api/auth.go +++ b/api/auth.go @@ -23,7 +23,7 @@ func CreateAuthHandler(authService auth.AuthService) *AuthHandler { func (h *AuthHandler) createHttpHandler() http.Handler { handler := http.NewServeMux() - handler.HandleFunc("GET /policy.json", h.policyHandler) + handler.HandleFunc("GET /policy.json", h.policyHandler) // DEPRECATED TODO: remove handler.HandleFunc("GET /{provider}", h.handleProviderLogin) handler.HandleFunc("GET /{provider}/callback", h.handleAuthCallback) diff --git a/api/handler.go b/api/handler.go index 905e0c7..dab8bc8 100644 --- a/api/handler.go +++ b/api/handler.go @@ -2,11 +2,13 @@ package api import ( "context" + "encoding/json" "fmt" "log" "net/http" "github.com/getkin/kin-openapi/openapi3" + "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/email" "github.com/piquel-fr/api/services/users" @@ -29,6 +31,12 @@ func CreateRouter(userService users.UserService, authService auth.AuthService, e router.HandleFunc("/{$}", rootHandler) router.Handle("/auth/", http.StripPrefix("/auth", CreateAuthHandler(authService).createHttpHandler())) + configHandler, err := configHandler() + if err != nil { + return nil, err + } + router.HandleFunc("/config.json", configHandler) + handlers := []Handler{ CreateUserHandler(userService, authService), CreateProfileHandler(authService), @@ -70,6 +78,18 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Welcome to the Piquel API! Visit the API for more information.")) } +func configHandler() (http.HandlerFunc, error) { + data, err := json.Marshal(config.GetPublicConfig()) + if err != nil { + return nil, err + } + + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.Write(data) + }, nil +} + func newSpecBase(handler Handler) Spec { spec := &openapi3.T{ OpenAPI: "3.0.3", diff --git a/config/config.go b/config/config.go index 5240af4..f671d47 100644 --- a/config/config.go +++ b/config/config.go @@ -28,10 +28,23 @@ type EnvsConfig struct { ImapPort string } +type PublicConfig struct { + Policy *PolicyConfiguration `json:"policy"` + UsernameBlacklist []string `json:"username_blacklist"` +} + +func GetPublicConfig() PublicConfig { + return PublicConfig{Policy, UsernameBlacklist} +} + var Envs EnvsConfig var MaxDocsInstanceCount int64 = 3 var JWTSigningMethod jwt.SigningMethod = jwt.SigningMethodHS256 +// these are populated by external services +var UsernameBlacklist []string +var Policy *PolicyConfiguration + func LoadConfig() { godotenv.Load() log.Printf("[Config] Loading configuration...") diff --git a/config/policy.go b/config/policy.go index bb53ac4..28461e7 100644 --- a/config/policy.go +++ b/config/policy.go @@ -37,5 +37,3 @@ type Resource interface { GetResourceName() string GetOwner() int32 } - -var Policy *PolicyConfiguration From 2a4917cda828f364b0ae97a0c89a637983eaac95 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:41:23 +0100 Subject: [PATCH 10/45] setup username validation --- services/users/users.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/services/users/users.go b/services/users/users.go index 80e0358..0af1e28 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -2,8 +2,12 @@ package users import ( "context" + "fmt" + "slices" + "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/database/repository" + "github.com/piquel-fr/api/utils" ) type UserService interface { @@ -19,13 +23,14 @@ type UserService interface { DeleteUser(ctx context.Context, id int32) error // other - ValidateUsername(username string) error + FormatAndValidateUsername(username string) (string, error) ListUsers(ctx context.Context, offset, limit int) ([]repository.User, error) } type realUserService struct{} func NewRealUserService() *realUserService { + config.UsernameBlacklist = []string{"self", "users", "admin", "system"} // TODO: add more return &realUserService{} } @@ -58,8 +63,12 @@ func (s *realUserService) DeleteUser(ctx context.Context, id int32) error { return nil } -func (s *realUserService) ValidateUsername(username string) error { - return nil +func (s *realUserService) ValidateAndFormatUsername(username string) (string, error) { + username = utils.FormatUsername(username) + if slices.Contains(config.UsernameBlacklist, username) { + return "", fmt.Errorf("username %s is not legal", username) + } + return username, nil } func (s *realUserService) ListUsers(ctx context.Context, offset, limit int) ([]repository.User, error) { From f017b786882002252f6e99d9a67330da2f6f77ad Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:46:43 +0100 Subject: [PATCH 11/45] setup listing users --- database/queries/users.sql | 3 +++ database/repository/users.sql.go | 37 ++++++++++++++++++++++++++++++++ services/users/users.go | 10 ++++++--- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/database/queries/users.sql b/database/queries/users.sql index e35fa3d..bba3663 100644 --- a/database/queries/users.sql +++ b/database/queries/users.sql @@ -11,6 +11,9 @@ SELECT * FROM "users" WHERE "id" = $1; -- name: GetUserByEmail :one SELECT * FROM "users" WHERE "email" = $1; +-- name: ListUsers :many +SELECT * FROM "users" ORDER BY "id" ASC LIMIT $1 OFFSET $2; + -- name: UpdateUser :exec UPDATE "users" SET "username" = $2, "name" = $3, "image" = $4 WHERE "id" = $1; diff --git a/database/repository/users.sql.go b/database/repository/users.sql.go index b596d8e..7348aa5 100644 --- a/database/repository/users.sql.go +++ b/database/repository/users.sql.go @@ -100,6 +100,43 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, return i, err } +const listUsers = `-- name: ListUsers :many +SELECT id, username, name, image, email, role, "createdAt" FROM "users" ORDER BY "id" ASC LIMIT $1 OFFSET $2 +` + +type ListUsersParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error) { + rows, err := q.db.Query(ctx, listUsers, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Name, + &i.Image, + &i.Email, + &i.Role, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const updateUser = `-- name: UpdateUser :exec UPDATE "users" SET "username" = $2, "name" = $3, "image" = $4 WHERE "id" = $1 ` diff --git a/services/users/users.go b/services/users/users.go index 0af1e28..54eb877 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -6,6 +6,7 @@ import ( "slices" "github.com/piquel-fr/api/config" + "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/utils" ) @@ -24,7 +25,7 @@ type UserService interface { // other FormatAndValidateUsername(username string) (string, error) - ListUsers(ctx context.Context, offset, limit int) ([]repository.User, error) + ListUsers(ctx context.Context, offset, limit int32) ([]repository.User, error) } type realUserService struct{} @@ -71,6 +72,9 @@ func (s *realUserService) ValidateAndFormatUsername(username string) (string, er return username, nil } -func (s *realUserService) ListUsers(ctx context.Context, offset, limit int) ([]repository.User, error) { - return nil, nil +func (s *realUserService) ListUsers(ctx context.Context, offset, limit int32) ([]repository.User, error) { + if limit > 200 { + limit = 200 + } + return database.Queries.ListUsers(ctx, repository.ListUsersParams{Offset: offset, Limit: limit}) } From b42150f13c6ce2a35d98540b5608d16a467e45c9 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:48:35 +0100 Subject: [PATCH 12/45] add helper function to validate role on policy config --- config/policy.go | 5 +++++ services/users/users.go | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/policy.go b/config/policy.go index 28461e7..e93d92d 100644 --- a/config/policy.go +++ b/config/policy.go @@ -11,6 +11,11 @@ type PolicyConfiguration struct { Roles map[string]*Role `json:"roles"` } +func (p *PolicyConfiguration) ValidateRole(role string) bool { + _, ok := p.Roles[role] + return ok +} + type Permission struct { Action string `json:"action"` Conditions Conditions `json:"-"` diff --git a/services/users/users.go b/services/users/users.go index 54eb877..02b7976 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -20,7 +20,7 @@ type UserService interface { // managing users UpdateUser(ctx context.Context, params repository.UpdateUserParams) error UpdateUserAdmin(ctx context.Context, params repository.UpdateUserAdminParams) error - RegisterUser(ctx context.Context, email, username, name, image, role string) error // does not validate the role + RegisterUser(ctx context.Context, email, username, name, image, role string) error DeleteUser(ctx context.Context, id int32) error // other @@ -55,7 +55,6 @@ func (s *realUserService) UpdateUserAdmin(ctx context.Context, params repository return nil } -// does not validate the role func (s *realUserService) RegisterUser(ctx context.Context, email, username, name, image, role string) error { return nil } From 3c1208cbb3dccb994912819165db6bf6a18b8864 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:51:34 +0100 Subject: [PATCH 13/45] setup getters --- services/auth/auth.go | 18 ++++++++---------- services/users/users.go | 9 ++++++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/services/auth/auth.go b/services/auth/auth.go index 82dcafd..e49447a 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -136,16 +136,6 @@ func (s *realAuthService) registerUser(ctx context.Context, inUser *oauth.User) return &user, err } -func (s *realAuthService) GetUserFromUsername(ctx context.Context, username string) (*repository.User, error) { - user, err := database.Queries.GetUserByUsername(ctx, username) - return &user, err -} - -func (s *realAuthService) GetUserFromUserId(ctx context.Context, userId int32) (*repository.User, error) { - user, err := database.Queries.GetUserById(ctx, userId) - return &user, err -} - func (s *realAuthService) GetProvider(name string) (oauth.Provider, error) { provider, ok := oauth.Providers[name] if !ok { @@ -153,3 +143,11 @@ func (s *realAuthService) GetProvider(name string) (oauth.Provider, error) { } return provider, nil } + +// TODO: remove +func (*realAuthService) GetUserFromUsername(context.Context, string) (*repository.User, error) { + return nil, nil +} +func (*realAuthService) GetUserFromUserId(context.Context, int32) (*repository.User, error) { + return nil, nil +} diff --git a/services/users/users.go b/services/users/users.go index 02b7976..d0238de 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -36,15 +36,18 @@ func NewRealUserService() *realUserService { } func (s *realUserService) GetUserById(ctx context.Context, id int32) (*repository.User, error) { - return nil, nil + user, err := database.Queries.GetUserById(ctx, id) + return &user, err } func (s *realUserService) GetUserByUsername(ctx context.Context, username string) (*repository.User, error) { - return nil, nil + user, err := database.Queries.GetUserByUsername(ctx, username) + return &user, err } func (s *realUserService) GetUserByEmail(ctx context.Context, email string) (*repository.User, error) { - return nil, nil + user, err := database.Queries.GetUserByEmail(ctx, email) + return &user, err } func (s *realUserService) UpdateUser(ctx context.Context, params repository.UpdateUserParams) error { From 04a716b09036a6f8ccc56330baa5d5516481fda4 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 19:56:20 +0100 Subject: [PATCH 14/45] update setting up config --- main.go | 3 +++ services/auth/auth.go | 1 - services/users/users.go | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 828d100..dd67889 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,9 @@ func main() { authService := auth.NewRealAuthService() emailService := email.NewRealEmailService() + config.UsernameBlacklist = userService.GetUsernameBlacklist() + config.Policy = authService.GetPolicy() + router, err := api.CreateRouter(userService, authService, emailService) if err != nil { panic(err) diff --git a/services/auth/auth.go b/services/auth/auth.go index e49447a..022b2b9 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -53,7 +53,6 @@ type NewAuthService interface { type realAuthService struct{} func NewRealAuthService() *realAuthService { - config.Policy = &policy return &realAuthService{} } diff --git a/services/users/users.go b/services/users/users.go index d0238de..68a4b98 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -12,6 +12,8 @@ import ( ) type UserService interface { + GetUsernameBlacklist() []string + // getting the user GetUserById(ctx context.Context, id int32) (*repository.User, error) GetUserByUsername(ctx context.Context, username string) (*repository.User, error) @@ -80,3 +82,7 @@ func (s *realUserService) ListUsers(ctx context.Context, offset, limit int32) ([ } return database.Queries.ListUsers(ctx, repository.ListUsersParams{Offset: offset, Limit: limit}) } + +func (s *realUserService) GetUsernameBlacklist() []string { + return []string{"self", "users", "admin", "system"} // TODO: add more +} From 11948f9bbcdd7725ebb4e8a19d361d875de4a4ae Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 20:28:57 +0100 Subject: [PATCH 15/45] add updating user with validation --- config/policy.go | 10 ++++++++-- services/users/users.go | 28 +++++++++++++++++++++------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/config/policy.go b/config/policy.go index e93d92d..5fb19b3 100644 --- a/config/policy.go +++ b/config/policy.go @@ -2,8 +2,11 @@ package config import ( "context" + "fmt" + "net/http" "github.com/piquel-fr/api/database/repository" + "github.com/piquel-fr/api/utils/errors" ) type PolicyConfiguration struct { @@ -11,9 +14,12 @@ type PolicyConfiguration struct { Roles map[string]*Role `json:"roles"` } -func (p *PolicyConfiguration) ValidateRole(role string) bool { +func (p *PolicyConfiguration) ValidateRole(role string) error { _, ok := p.Roles[role] - return ok + if !ok { + return errors.NewError(fmt.Sprintf("role %s does not exist in current policy", role), http.StatusBadRequest) + } + return nil } type Permission struct { diff --git a/services/users/users.go b/services/users/users.go index 68a4b98..41b9fb2 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -3,12 +3,14 @@ package users import ( "context" "fmt" + "net/http" "slices" "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/utils" + "github.com/piquel-fr/api/utils/errors" ) type UserService interface { @@ -20,8 +22,8 @@ type UserService interface { GetUserByEmail(ctx context.Context, email string) (*repository.User, error) // managing users - UpdateUser(ctx context.Context, params repository.UpdateUserParams) error - UpdateUserAdmin(ctx context.Context, params repository.UpdateUserAdminParams) error + UpdateUser(ctx context.Context, id int32, username, name, image string) error + UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error RegisterUser(ctx context.Context, email, username, name, image, role string) error DeleteUser(ctx context.Context, id int32) error @@ -33,7 +35,6 @@ type UserService interface { type realUserService struct{} func NewRealUserService() *realUserService { - config.UsernameBlacklist = []string{"self", "users", "admin", "system"} // TODO: add more return &realUserService{} } @@ -52,12 +53,25 @@ func (s *realUserService) GetUserByEmail(ctx context.Context, email string) (*re return &user, err } -func (s *realUserService) UpdateUser(ctx context.Context, params repository.UpdateUserParams) error { - return nil +func (s *realUserService) UpdateUser(ctx context.Context, id int32, username, name, image string) error { + username, err := s.ValidateAndFormatUsername(username) + if err != nil { + return err + } + + return database.Queries.UpdateUser(ctx, repository.UpdateUserParams{ID: id, Username: username, Name: name, Image: image}) } -func (s *realUserService) UpdateUserAdmin(ctx context.Context, params repository.UpdateUserAdminParams) error { - return nil +func (s *realUserService) UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error { + username, err := s.ValidateAndFormatUsername(username) + if err != nil { + return err + } + + if err := config.Policy.ValidateRole(role); err != nil { + return err + } + return database.Queries.UpdateUserAdmin(ctx, repository.UpdateUserAdminParams{ID: id, Username: username, Email: email, Name: name, Image: image, Role: role}) } func (s *realUserService) RegisterUser(ctx context.Context, email, username, name, image, role string) error { From 6857303fb34fe44cad82c3e6b96224ef45e2f0a7 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 20:32:53 +0100 Subject: [PATCH 16/45] add user registration --- services/users/users.go | 44 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/services/users/users.go b/services/users/users.go index 41b9fb2..5655c64 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -6,6 +6,7 @@ import ( "net/http" "slices" + "github.com/gogo/protobuf/test/data" "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" @@ -24,7 +25,7 @@ type UserService interface { // managing users UpdateUser(ctx context.Context, id int32, username, name, image string) error UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error - RegisterUser(ctx context.Context, email, username, name, image, role string) error + RegisterUser(ctx context.Context, username, email, name, image, role string) error DeleteUser(ctx context.Context, id int32) error // other @@ -59,7 +60,13 @@ func (s *realUserService) UpdateUser(ctx context.Context, id int32, username, na return err } - return database.Queries.UpdateUser(ctx, repository.UpdateUserParams{ID: id, Username: username, Name: name, Image: image}) + params := repository.UpdateUserParams{ + ID: id, + Username: username, + Name: name, + Image: image, + } + return database.Queries.UpdateUser(ctx, params) } func (s *realUserService) UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error { @@ -71,11 +78,38 @@ func (s *realUserService) UpdateUserAdmin(ctx context.Context, id int32, usernam if err := config.Policy.ValidateRole(role); err != nil { return err } - return database.Queries.UpdateUserAdmin(ctx, repository.UpdateUserAdminParams{ID: id, Username: username, Email: email, Name: name, Image: image, Role: role}) + + params := repository.UpdateUserAdminParams{ + ID: id, + Username: username, + Email: email, + Name: name, + Image: image, + Role: role, + } + return database.Queries.UpdateUserAdmin(ctx, params) } -func (s *realUserService) RegisterUser(ctx context.Context, email, username, name, image, role string) error { - return nil +func (s *realUserService) RegisterUser(ctx context.Context, username, email, name, image, role string) (*repository.User, error) { + username, err := s.ValidateAndFormatUsername(username) + if err != nil { + return nil, err + } + + if err := config.Policy.ValidateRole(role); err != nil { + return nil, err + } + + params := repository.AddUserParams{ + Username: username, + Email: email, + Name: name, + Image: image, + Role: role, + } + + user, err := database.Queries.AddUser(ctx, params) + return &user, err } func (s *realUserService) DeleteUser(ctx context.Context, id int32) error { From 04df9450c6ac84d06c18444674410d7141f09ca5 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 20:34:00 +0100 Subject: [PATCH 17/45] add blacklisted username --- services/users/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/users/users.go b/services/users/users.go index 5655c64..995354f 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -132,5 +132,5 @@ func (s *realUserService) ListUsers(ctx context.Context, offset, limit int32) ([ } func (s *realUserService) GetUsernameBlacklist() []string { - return []string{"self", "users", "admin", "system"} // TODO: add more + return []string{"self", "root", "users", "admin", "system"} // TODO: add more } From 1ed98f99b102454e0af107cab34561e204851060 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 20:35:56 +0100 Subject: [PATCH 18/45] finish user service --- services/users/users.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/services/users/users.go b/services/users/users.go index 995354f..38e4c55 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -3,15 +3,12 @@ package users import ( "context" "fmt" - "net/http" "slices" - "github.com/gogo/protobuf/test/data" "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/utils" - "github.com/piquel-fr/api/utils/errors" ) type UserService interface { @@ -55,7 +52,7 @@ func (s *realUserService) GetUserByEmail(ctx context.Context, email string) (*re } func (s *realUserService) UpdateUser(ctx context.Context, id int32, username, name, image string) error { - username, err := s.ValidateAndFormatUsername(username) + username, err := s.FormatAndValidateUsername(username) if err != nil { return err } @@ -70,7 +67,7 @@ func (s *realUserService) UpdateUser(ctx context.Context, id int32, username, na } func (s *realUserService) UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error { - username, err := s.ValidateAndFormatUsername(username) + username, err := s.FormatAndValidateUsername(username) if err != nil { return err } @@ -91,7 +88,7 @@ func (s *realUserService) UpdateUserAdmin(ctx context.Context, id int32, usernam } func (s *realUserService) RegisterUser(ctx context.Context, username, email, name, image, role string) (*repository.User, error) { - username, err := s.ValidateAndFormatUsername(username) + username, err := s.FormatAndValidateUsername(username) if err != nil { return nil, err } @@ -113,10 +110,11 @@ func (s *realUserService) RegisterUser(ctx context.Context, username, email, nam } func (s *realUserService) DeleteUser(ctx context.Context, id int32) error { + // TODO: delete user return nil } -func (s *realUserService) ValidateAndFormatUsername(username string) (string, error) { +func (s *realUserService) FormatAndValidateUsername(username string) (string, error) { username = utils.FormatUsername(username) if slices.Contains(config.UsernameBlacklist, username) { return "", fmt.Errorf("username %s is not legal", username) From 07b182f089d9b03661de813b4896fe77179bd49b Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 20:38:38 +0100 Subject: [PATCH 19/45] fix build errors --- api/email.go | 11 ++++++----- api/profile.go | 3 ++- services/auth/errors.go | 4 +++- services/auth/permissions.go | 15 ++++++++------- services/users/users.go | 2 +- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/api/email.go b/api/email.go index e4458c2..6747a54 100644 --- a/api/email.go +++ b/api/email.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/getkin/kin-openapi/openapi3" + "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/services/auth" @@ -248,7 +249,7 @@ func (h *EmailHandler) handleListAccounts(w http.ResponseWriter, r *http.Request user = requester } - if err := h.authService.Authorize(&auth.Request{ + if err := h.authService.Authorize(&config.AuthRequest{ User: requester, Ressource: user, Context: r.Context(), @@ -335,7 +336,7 @@ func (h *EmailHandler) handleAccountInfo(w http.ResponseWriter, r *http.Request) return } - if err := h.authService.Authorize(&auth.Request{ + if err := h.authService.Authorize(&config.AuthRequest{ User: user, Ressource: &accountInfo, Actions: []string{auth.ActionView}, @@ -371,7 +372,7 @@ func (h *EmailHandler) handleRemoveAccount(w http.ResponseWriter, r *http.Reques return } - if err := h.authService.Authorize(&auth.Request{ + if err := h.authService.Authorize(&config.AuthRequest{ User: user, Ressource: &account, Actions: []string{auth.ActionDelete}, @@ -400,7 +401,7 @@ func (h *EmailHandler) handleShareAccount(w http.ResponseWriter, r *http.Request return } - if err := h.authService.Authorize(&auth.Request{ + if err := h.authService.Authorize(&config.AuthRequest{ User: user, Ressource: &account, Actions: []string{auth.ActionShare}, @@ -441,7 +442,7 @@ func (h *EmailHandler) handleRemoveAccountShare(w http.ResponseWriter, r *http.R return } - if err := h.authService.Authorize(&auth.Request{ + if err := h.authService.Authorize(&config.AuthRequest{ User: user, Ressource: &account, Actions: []string{auth.ActionShare}, diff --git a/api/profile.go b/api/profile.go index f4626a9..6587f44 100644 --- a/api/profile.go +++ b/api/profile.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/getkin/kin-openapi/openapi3" + "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/services/auth" @@ -181,7 +182,7 @@ func (h *ProfileHandler) handleUpdateProfile(w http.ResponseWriter, r *http.Requ return } - request := &auth.Request{ + request := &config.AuthRequest{ User: user, Ressource: user, Actions: []string{auth.ActionUpdate}, diff --git a/services/auth/errors.go b/services/auth/errors.go index fef2ca5..df23948 100644 --- a/services/auth/errors.go +++ b/services/auth/errors.go @@ -2,9 +2,11 @@ package auth import ( "fmt" + + "github.com/piquel-fr/api/config" ) -func newRequestMalformedError(request *Request) error { +func newRequestMalformedError(request *config.AuthRequest) error { return fmt.Errorf("Request is malformed: %v", request) } diff --git a/services/auth/permissions.go b/services/auth/permissions.go index 6eeba42..2712c6e 100644 --- a/services/auth/permissions.go +++ b/services/auth/permissions.go @@ -3,10 +3,11 @@ package auth import ( "slices" + "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/utils/errors" ) -func (s *realAuthService) Authorize(request *Request) error { +func (s *realAuthService) Authorize(request *config.AuthRequest) error { if request.User == nil || request.Ressource == nil { return newRequestMalformedError(request) } @@ -35,16 +36,16 @@ func (s *realAuthService) Authorize(request *Request) error { return errors.ErrorForbidden } -func (s *realAuthService) authorize(request *Request, roleName, resourceName string, checkedRoles []string) (bool, error) { +func (s *realAuthService) authorize(request *config.AuthRequest, roleName, resourceName string, checkedRoles []string) (bool, error) { role, ok := policy.Roles[roleName] if !ok { return false, newRoleNotFoundError(roleName) } - var permissions []*Permission + var permissions []*config.Permission if role.Permissions == nil { - permissions = []*Permission{} + permissions = []*config.Permission{} } else { permissions = role.Permissions[resourceName] } @@ -65,7 +66,7 @@ func (s *realAuthService) authorize(request *Request, roleName, resourceName str checkedRoles = append(checkedRoles, roleName) for _, parent := range parents { - parentRequest := &Request{ + parentRequest := &config.AuthRequest{ User: request.User, Ressource: request.Ressource, Actions: []string{action}, @@ -95,7 +96,7 @@ func (s *realAuthService) authorize(request *Request, roleName, resourceName str return true, nil } -func (s *realAuthService) validateAction(permissions []*Permission, action string, request *Request) (bool, error) { +func (s *realAuthService) validateAction(permissions []*config.Permission, action string, request *config.AuthRequest) (bool, error) { for _, permission := range permissions { if permission.Preset != "" { @@ -123,7 +124,7 @@ func (s *realAuthService) validateAction(permissions []*Permission, action strin return false, nil } -func (s *realAuthService) checkPermission(permission *Permission, request *Request) (bool, error) { +func (s *realAuthService) checkPermission(permission *config.Permission, request *config.AuthRequest) (bool, error) { if permission.Conditions == nil { return true, nil } diff --git a/services/users/users.go b/services/users/users.go index 38e4c55..fb8e0f5 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -22,7 +22,7 @@ type UserService interface { // managing users UpdateUser(ctx context.Context, id int32, username, name, image string) error UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error - RegisterUser(ctx context.Context, username, email, name, image, role string) error + RegisterUser(ctx context.Context, username, email, name, image, role string) (*repository.User, error) DeleteUser(ctx context.Context, id int32) error // other From d9725ecaae0f2bc9d7ba7df6208add5e8c117d6f Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 20:41:12 +0100 Subject: [PATCH 20/45] fix build error --- api/email.go | 6 ++++-- api/profile.go | 6 ++++-- api/users.go | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/api/email.go b/api/email.go index 6747a54..e1726ef 100644 --- a/api/email.go +++ b/api/email.go @@ -41,8 +41,10 @@ func (h *EmailHandler) getSpec() Spec { WithProperty("username", openapi3.NewStringSchema()). WithProperty("password", openapi3.NewStringSchema()) - spec.Components.Schemas["MailAccount"] = &openapi3.SchemaRef{Value: accountSchema} - spec.Components.Schemas["AddAccountPayload"] = &openapi3.SchemaRef{Value: addAccountSchema} + spec.Components.Schemas = openapi3.Schemas{ + "MailAccount": &openapi3.SchemaRef{Value: accountSchema}, + "AddAccountPayload": &openapi3.SchemaRef{Value: addAccountSchema}, + } spec.AddOperation("/", http.MethodGet, &openapi3.Operation{ Tags: []string{"email"}, diff --git a/api/profile.go b/api/profile.go index 6587f44..e671bbb 100644 --- a/api/profile.go +++ b/api/profile.go @@ -43,8 +43,10 @@ func (h *ProfileHandler) getSpec() Spec { WithProperty("image", openapi3.NewStringSchema()). WithRequired([]string{"username", "name", "image"}) - spec.Components.Schemas["User"] = &openapi3.SchemaRef{Value: userSchema} - spec.Components.Schemas["UpdateUserParams"] = &openapi3.SchemaRef{Value: updateUserSchema} + spec.Components.Schemas = openapi3.Schemas{ + "User": &openapi3.SchemaRef{Value: userSchema}, + "UpdateUserParams": &openapi3.SchemaRef{Value: updateUserSchema}, + } spec.AddOperation("/{user}", http.MethodGet, &openapi3.Operation{ Tags: []string{"users"}, diff --git a/api/users.go b/api/users.go index b1eb06e..3369f0f 100644 --- a/api/users.go +++ b/api/users.go @@ -39,8 +39,10 @@ func (h *UserHandler) getSpec() Spec { WithProperty("image", openapi3.NewStringSchema()). WithRequired([]string{"username", "name", "image"}) - spec.Components.Schemas["User"] = &openapi3.SchemaRef{Value: userSchema} - spec.Components.Schemas["UpdateUserParams"] = &openapi3.SchemaRef{Value: updateUserSchema} + spec.Components.Schemas = openapi3.Schemas{ + "User": &openapi3.SchemaRef{Value: userSchema}, + "UpdateUserParams": &openapi3.SchemaRef{Value: updateUserSchema}, + } spec.AddOperation("/self", http.MethodGet, &openapi3.Operation{ Tags: []string{"users"}, From b85883030026dd03a7afe6f48ff980e96d4097c2 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 20:42:46 +0100 Subject: [PATCH 21/45] add view email permission --- services/auth/policy.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/auth/policy.go b/services/auth/policy.go index 543b141..26c15a0 100644 --- a/services/auth/policy.go +++ b/services/auth/policy.go @@ -23,6 +23,7 @@ const ( ActionDelete string = "delete" ActionShare string = "share" + ActionViewEmail string = "view_email" ActionListEmailAccounts string = "list_email_accounts" ) @@ -56,6 +57,7 @@ var policy = config.PolicyConfiguration{ repository.ResourceUser: { {Action: ActionUpdate}, {Action: ActionDelete}, + {Action: ActionViewEmail}, }, repository.ResourceMailAccount: { {Action: ActionView}, @@ -108,6 +110,7 @@ var policy = config.PolicyConfiguration{ repository.ResourceUser: { makeOwn(ActionUpdate), makeOwn(ActionDelete), + makeOwn(ActionViewEmail), }, }, }, From 4684ff8af7f4399c973f74ca9ed8a408fe3e5927 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 21:25:27 +0100 Subject: [PATCH 22/45] updated auth service --- services/auth/auth.go | 132 +++++++++++++-------------------- utils/middleware/middleware.go | 21 ------ 2 files changed, 53 insertions(+), 100 deletions(-) diff --git a/services/auth/auth.go b/services/auth/auth.go index 022b2b9..2d6d6cd 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -8,67 +8,65 @@ import ( "strings" "github.com/golang-jwt/jwt/v5" - "github.com/jackc/pgx/v5" "github.com/piquel-fr/api/config" - "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" + "github.com/piquel-fr/api/services/users" "github.com/piquel-fr/api/utils/errors" "github.com/piquel-fr/api/utils/oauth" ) type AuthService interface { - GenerateTokenString(userId int32) (string, error) - GetToken(r *http.Request) (*jwt.Token, error) - GetUserId(r *http.Request) (int32, error) - GetUser(ctx context.Context, inUser *oauth.User) (*repository.User, error) - GetUserFromRequest(r *http.Request) (*repository.User, error) - GetUserFromUserId(ctx context.Context, userId int32) (*repository.User, error) - GetUserFromUsername(ctx context.Context, username string) (*repository.User, error) - Authorize(request *config.AuthRequest) error - GetProvider(name string) (oauth.Provider, error) - - GetPolicy() *config.PolicyConfiguration -} - -// TEMP: the new auth interface that will be made alongside the new user service -type NewAuthService interface { GetPolicy() *config.PolicyConfiguration GetProvider(name string) (oauth.Provider, error) // token management GenerateToken(user *repository.User) *jwt.Token // TODO: also save expiry and refresh - signToken(token *jwt.Token) (string, error) + SignToken(token *jwt.Token) (string, error) getTokenFromRequest(r *http.Request) (*jwt.Token, error) + getUserFromToken(ctx context.Context, token *jwt.Token) (*repository.User, error) // authentication - GetUserFromContext(ctx context.Context) (*repository.User, error) // gets user from context (should be saved there by auth middleware) - GetUserFromOAuthUser() + GetUserFromContext(ctx context.Context) (*repository.User, error) // authorization Authorize(request *config.AuthRequest) error AuthMiddleware(next http.Handler) http.Handler } -// auth service has no state -type realAuthService struct{} +var userKey = "user" -func NewRealAuthService() *realAuthService { - return &realAuthService{} +type realAuthService struct { + userService users.UserService +} + +func NewRealAuthService(userService users.UserService) *realAuthService { + return &realAuthService{userService} } func (s *realAuthService) GetPolicy() *config.PolicyConfiguration { return &policy } -func (s *realAuthService) GenerateTokenString(userId int32) (string, error) { - idString := strconv.Itoa(int(userId)) - token := jwt.NewWithClaims(config.JWTSigningMethod, +func (s *realAuthService) GetProvider(name string) (oauth.Provider, error) { + provider, ok := oauth.Providers[name] + if !ok { + return nil, errors.NewError(fmt.Sprintf("provider %s does not exist", name), http.StatusBadRequest) + } + return provider, nil +} + +// TODO: also save expiry and refresh +func (s *realAuthService) GenerateToken(user *repository.User) *jwt.Token { + idString := strconv.Itoa(int(user.ID)) + return jwt.NewWithClaims(config.JWTSigningMethod, jwt.RegisteredClaims{ Subject: idString, }) +} +func (s *realAuthService) SignToken(token *jwt.Token) (string, error) { return token.SignedString(config.Envs.JWTSigningSecret) } -func (s *realAuthService) GetToken(r *http.Request) (*jwt.Token, error) { +func (s *realAuthService) getTokenFromRequest(r *http.Request) (*jwt.Token, error) { authHeader := r.Header.Get("Authorization") parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { @@ -81,72 +79,48 @@ func (s *realAuthService) GetToken(r *http.Request) (*jwt.Token, error) { }) } -func (s *realAuthService) GetUserId(r *http.Request) (int32, error) { - token, err := s.GetToken(r) - if err != nil { - return 0, err - } - +func (s *realAuthService) getUserFromToken(ctx context.Context, token *jwt.Token) (*repository.User, error) { subject, err := token.Claims.GetSubject() if err != nil { - return 0, err + return nil, err } id, err := strconv.Atoi(subject) - if err != nil { - return 0, err - } - - return int32(id), nil -} - -func (s *realAuthService) GetUserFromRequest(r *http.Request) (*repository.User, error) { - userId, err := s.GetUserId(r) if err != nil { return nil, err } - user, err := database.Queries.GetUserById(r.Context(), userId) - return &user, err + return s.userService.GetUserById(ctx, int32(id)) } -func (s *realAuthService) GetUser(ctx context.Context, inUser *oauth.User) (*repository.User, error) { - user, err := database.Queries.GetUserByEmail(ctx, inUser.Email) - if err != nil { - if err == pgx.ErrNoRows { - return s.registerUser(ctx, inUser) - } - return nil, err +func (s *realAuthService) GetUserFromContext(ctx context.Context) (*repository.User, error) { + user, ok := ctx.Value(userKey).(*repository.User) + if !ok { + return nil, fmt.Errorf("user is not in context") } - - return &user, nil + return user, nil } -func (s *realAuthService) registerUser(ctx context.Context, inUser *oauth.User) (*repository.User, error) { - params := repository.AddUserParams{} - - params.Email = inUser.Email - params.Username = inUser.Username - params.Role = RoleDefault - params.Image = inUser.Image - params.Name = inUser.Name +func (s *realAuthService) AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + next.ServeHTTP(w, r) + return + } - user, err := database.Queries.AddUser(ctx, params) - return &user, err -} + token, err := s.getTokenFromRequest(r) + if err != nil { + errors.HandleError(w, r, err) + return + } -func (s *realAuthService) GetProvider(name string) (oauth.Provider, error) { - provider, ok := oauth.Providers[name] - if !ok { - return nil, errors.NewError(fmt.Sprintf("provider %s does not exist", name), http.StatusBadRequest) - } - return provider, nil -} + user, err := s.getUserFromToken(r.Context(), token) + if err != nil { + errors.HandleError(w, r, err) + return + } -// TODO: remove -func (*realAuthService) GetUserFromUsername(context.Context, string) (*repository.User, error) { - return nil, nil -} -func (*realAuthService) GetUserFromUserId(context.Context, int32) (*repository.User, error) { - return nil, nil + newReq := r.WithContext(context.WithValue(r.Context(), userKey, user)) + next.ServeHTTP(w, newReq) + }) } diff --git a/utils/middleware/middleware.go b/utils/middleware/middleware.go index da78242..2182876 100644 --- a/utils/middleware/middleware.go +++ b/utils/middleware/middleware.go @@ -3,9 +3,6 @@ package middleware import ( "net/http" "strings" - - "github.com/piquel-fr/api/services/auth" - "github.com/piquel-fr/api/utils/errors" ) type Middleware func(http.Handler) http.Handler @@ -42,24 +39,6 @@ func CORSMiddleware(next http.Handler) http.Handler { }) } -func AuthMiddleware(auth auth.AuthService) Middleware { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodOptions { - next.ServeHTTP(w, r) - return - } - - _, err := auth.GetToken(r) - if err != nil { - errors.HandleError(w, r, err) - return - } - next.ServeHTTP(w, r) - }) - } -} - func CreateOptionsHandler(methods ...string) http.Handler { methods = append(methods, "OPTIONS") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From bc361527ce4bf2b915462814ac0a1192ba5cbe88 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Sun, 25 Jan 2026 21:25:41 +0100 Subject: [PATCH 23/45] fix build errors --- api/auth.go | 12 ++++++++---- api/email.go | 24 +++++++++++++----------- api/handler.go | 8 ++++---- api/profile.go | 17 +++++++---------- main.go | 2 +- services/users/users.go | 3 +++ 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/api/auth.go b/api/auth.go index 3301654..67bc7d5 100644 --- a/api/auth.go +++ b/api/auth.go @@ -7,17 +7,19 @@ import ( "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/services/auth" + "github.com/piquel-fr/api/services/users" "github.com/piquel-fr/api/utils" "github.com/piquel-fr/api/utils/errors" "github.com/piquel-fr/api/utils/middleware" ) type AuthHandler struct { + userService users.UserService authService auth.AuthService } -func CreateAuthHandler(authService auth.AuthService) *AuthHandler { - return &AuthHandler{authService} +func CreateAuthHandler(userService users.UserService, authService auth.AuthService) *AuthHandler { + return &AuthHandler{userService, authService} } func (h *AuthHandler) createHttpHandler() http.Handler { @@ -71,13 +73,15 @@ func (h *AuthHandler) handleAuthCallback(w http.ResponseWriter, r *http.Request) return } - user, err := h.authService.GetUser(r.Context(), oauthUser) + user, err := h.userService.GetUserByEmail(r.Context(), oauthUser.Email) if err != nil { errors.HandleError(w, r, err) return } - tokenString, err := h.authService.GenerateTokenString(user.ID) + // TODO: if user doesn't exist, create it + + tokenString, err := h.authService.SignToken(h.authService.GenerateToken(user)) if err != nil { errors.HandleError(w, r, err) return diff --git a/api/email.go b/api/email.go index e1726ef..18e1d2a 100644 --- a/api/email.go +++ b/api/email.go @@ -11,17 +11,19 @@ import ( "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/email" + "github.com/piquel-fr/api/services/users" "github.com/piquel-fr/api/utils/errors" "github.com/piquel-fr/api/utils/middleware" ) type EmailHandler struct { + userService users.UserService authService auth.AuthService emailService email.EmailService } -func CreateEmailHandler(authService auth.AuthService, emailService email.EmailService) *EmailHandler { - return &EmailHandler{authService, emailService} +func CreateEmailHandler(userService users.UserService, authService auth.AuthService, emailService email.EmailService) *EmailHandler { + return &EmailHandler{userService, authService, emailService} } func (h *EmailHandler) getName() string { return "email" } @@ -234,7 +236,7 @@ func (h *EmailHandler) createHttpHandler() http.Handler { } func (h *EmailHandler) handleListAccounts(w http.ResponseWriter, r *http.Request) { - requester, err := h.authService.GetUserFromRequest(r) + requester, err := h.authService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -242,7 +244,7 @@ func (h *EmailHandler) handleListAccounts(w http.ResponseWriter, r *http.Request var user *repository.User if username := r.URL.Query().Get("user"); username != "" { - user, err = h.authService.GetUserFromUsername(r.Context(), username) + user, err = h.userService.GetUserByUsername(r.Context(), username) if err != nil { errors.HandleError(w, r, err) return @@ -295,7 +297,7 @@ func (h *EmailHandler) handleListAccounts(w http.ResponseWriter, r *http.Request } func (h *EmailHandler) handleAddAccount(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromRequest(r) + user, err := h.authService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -320,7 +322,7 @@ func (h *EmailHandler) handleAddAccount(w http.ResponseWriter, r *http.Request) } func (h *EmailHandler) handleAccountInfo(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromRequest(r) + user, err := h.authService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -362,7 +364,7 @@ func (h *EmailHandler) handleAccountInfo(w http.ResponseWriter, r *http.Request) } func (h *EmailHandler) handleRemoveAccount(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromRequest(r) + user, err := h.authService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -391,7 +393,7 @@ func (h *EmailHandler) handleRemoveAccount(w http.ResponseWriter, r *http.Reques } func (h *EmailHandler) handleShareAccount(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromRequest(r) + user, err := h.authService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -413,7 +415,7 @@ func (h *EmailHandler) handleShareAccount(w http.ResponseWriter, r *http.Request return } - sharingUser, err := h.authService.GetUserFromUsername(r.Context(), r.URL.Query().Get("user")) + sharingUser, err := h.userService.GetUserByUsername(r.Context(), r.URL.Query().Get("user")) if err != nil { errors.HandleError(w, r, err) return @@ -432,7 +434,7 @@ func (h *EmailHandler) handleShareAccount(w http.ResponseWriter, r *http.Request } func (h *EmailHandler) handleRemoveAccountShare(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromRequest(r) + user, err := h.authService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -454,7 +456,7 @@ func (h *EmailHandler) handleRemoveAccountShare(w http.ResponseWriter, r *http.R return } - sharingUser, err := h.authService.GetUserFromUsername(r.Context(), r.URL.Query().Get("user")) + sharingUser, err := h.userService.GetUserByUsername(r.Context(), r.URL.Query().Get("user")) if err != nil { errors.HandleError(w, r, err) return diff --git a/api/handler.go b/api/handler.go index dab8bc8..3914a72 100644 --- a/api/handler.go +++ b/api/handler.go @@ -29,7 +29,7 @@ func CreateRouter(userService users.UserService, authService auth.AuthService, e // should be added to createProtectedRouter router := http.NewServeMux() router.HandleFunc("/{$}", rootHandler) - router.Handle("/auth/", http.StripPrefix("/auth", CreateAuthHandler(authService).createHttpHandler())) + router.Handle("/auth/", http.StripPrefix("/auth", CreateAuthHandler(userService, authService).createHttpHandler())) configHandler, err := configHandler() if err != nil { @@ -39,8 +39,8 @@ func CreateRouter(userService users.UserService, authService auth.AuthService, e handlers := []Handler{ CreateUserHandler(userService, authService), - CreateProfileHandler(authService), - CreateEmailHandler(authService, emailService), + CreateProfileHandler(userService, authService), + CreateEmailHandler(userService, authService, emailService), } for _, handler := range handlers { @@ -55,7 +55,7 @@ func CreateRouter(userService users.UserService, authService auth.AuthService, e // bind the protected router protectedRouter := createProtectedRouter(handlers) - protectedRouter = middleware.AddMiddleware(protectedRouter, middleware.AuthMiddleware(authService)) + protectedRouter = middleware.AddMiddleware(protectedRouter, authService.AuthMiddleware) router.Handle("/", protectedRouter) return middleware.AddMiddleware(router, middleware.CORSMiddleware), nil diff --git a/api/profile.go b/api/profile.go index e671bbb..6b7beae 100644 --- a/api/profile.go +++ b/api/profile.go @@ -9,16 +9,18 @@ import ( "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/services/auth" + "github.com/piquel-fr/api/services/users" "github.com/piquel-fr/api/utils/errors" "github.com/piquel-fr/api/utils/middleware" ) type ProfileHandler struct { + userService users.UserService authService auth.AuthService } -func CreateProfileHandler(authService auth.AuthService) *ProfileHandler { - return &ProfileHandler{authService} +func CreateProfileHandler(userService users.UserService, authService auth.AuthService) *ProfileHandler { + return &ProfileHandler{userService, authService} } func (h *ProfileHandler) getName() string { return "profile" } @@ -148,12 +150,7 @@ func (h *ProfileHandler) handleGetProfile(w http.ResponseWriter, r *http.Request func (h *ProfileHandler) handleGetProfileQuery(w http.ResponseWriter, r *http.Request) { username := r.URL.Query().Get("username") if username == "" { - id, err := h.authService.GetUserId(r) - if err != nil { - errors.HandleError(w, r, err) - return - } - user, err := h.authService.GetUserFromUserId(r.Context(), id) + user, err := h.authService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -165,7 +162,7 @@ func (h *ProfileHandler) handleGetProfileQuery(w http.ResponseWriter, r *http.Re } func (h *ProfileHandler) writeProfile(w http.ResponseWriter, r *http.Request, username string) { - user, err := h.authService.GetUserFromUsername(r.Context(), username) + user, err := h.userService.GetUserByUsername(r.Context(), username) if err != nil { errors.HandleError(w, r, err) return @@ -178,7 +175,7 @@ func (h *ProfileHandler) writeProfile(w http.ResponseWriter, r *http.Request, us func (h *ProfileHandler) handleUpdateProfile(w http.ResponseWriter, r *http.Request) { username := r.PathValue("user") - user, err := h.authService.GetUserFromUsername(r.Context(), username) + user, err := h.userService.GetUserByUsername(r.Context(), username) if err != nil { errors.HandleError(w, r, err) return diff --git a/main.go b/main.go index dd67889..f551813 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ func main() { defer database.Connection.Close() userService := users.NewRealUserService() - authService := auth.NewRealAuthService() + authService := auth.NewRealAuthService(userService) emailService := email.NewRealEmailService() config.UsernameBlacklist = userService.GetUsernameBlacklist() diff --git a/services/users/users.go b/services/users/users.go index fb8e0f5..7a396f4 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -119,6 +119,9 @@ func (s *realUserService) FormatAndValidateUsername(username string) (string, er if slices.Contains(config.UsernameBlacklist, username) { return "", fmt.Errorf("username %s is not legal", username) } + + // TODO: make sure no one already has the username + return username, nil } From db2ecffbd8ad87cd81057b3c1f92842fef4dbe81 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Mon, 26 Jan 2026 20:20:50 +0100 Subject: [PATCH 24/45] add errors.Is wrapper --- utils/errors/errors.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/errors/errors.go b/utils/errors/errors.go index bc6e19b..4914249 100644 --- a/utils/errors/errors.go +++ b/utils/errors/errors.go @@ -29,6 +29,10 @@ func (e *Error) Error() string { return e.message } +func Is(err, target error) bool { + return errors.Is(err, target) +} + func getError(err error) *Error { if err == nil { panic("nil error being handled") From 6e82ef189d7feb263172d65d088ee1cf27672006 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Mon, 26 Jan 2026 20:21:10 +0100 Subject: [PATCH 25/45] add user creation if doesn't exist in auth callback --- api/auth.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/auth.go b/api/auth.go index 67bc7d5..487baa4 100644 --- a/api/auth.go +++ b/api/auth.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/jackc/pgx/v5" "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/users" @@ -74,13 +75,14 @@ func (h *AuthHandler) handleAuthCallback(w http.ResponseWriter, r *http.Request) } user, err := h.userService.GetUserByEmail(r.Context(), oauthUser.Email) + if errors.Is(err, pgx.ErrNoRows) { + user, err = h.userService.RegisterUser(r.Context(), oauthUser.Username, oauthUser.Email, oauthUser.Name, oauthUser.Image, auth.RoleDefault) + } if err != nil { errors.HandleError(w, r, err) return } - // TODO: if user doesn't exist, create it - tokenString, err := h.authService.SignToken(h.authService.GenerateToken(user)) if err != nil { errors.HandleError(w, r, err) From 8812e73b398aaaa2a9433ffa0c20ea05c665ef83 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Mon, 26 Jan 2026 20:35:29 +0100 Subject: [PATCH 26/45] add admin update to policy --- services/auth/policy.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/services/auth/policy.go b/services/auth/policy.go index 26c15a0..ae1e5e8 100644 --- a/services/auth/policy.go +++ b/services/auth/policy.go @@ -17,14 +17,15 @@ const ( ) const ( - ActionView string = "view" - ActionCreate string = "create" - ActionUpdate string = "update" - ActionDelete string = "delete" - ActionShare string = "share" + ActionView = "view" + ActionCreate = "create" + ActionUpdate = "update" + ActionDelete = "delete" + ActionShare = "share" - ActionViewEmail string = "view_email" - ActionListEmailAccounts string = "list_email_accounts" + ActionUpdateAdmin = "update_admin" + ActionViewEmail = "view_email" + ActionListEmailAccounts = "list_email_accounts" ) func own(request *config.AuthRequest) error { @@ -58,6 +59,7 @@ var policy = config.PolicyConfiguration{ {Action: ActionUpdate}, {Action: ActionDelete}, {Action: ActionViewEmail}, + {Action: ActionUpdateAdmin}, }, repository.ResourceMailAccount: { {Action: ActionView}, From d5f0f68901e107c1fe6fee5865d40c5682864f5e Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Mon, 26 Jan 2026 20:43:17 +0100 Subject: [PATCH 27/45] implement getself user handler --- api/users.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/api/users.go b/api/users.go index 3369f0f..f28a40a 100644 --- a/api/users.go +++ b/api/users.go @@ -1,11 +1,13 @@ package api import ( + "encoding/json" "net/http" "github.com/getkin/kin-openapi/openapi3" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/users" + "github.com/piquel-fr/api/utils/errors" "github.com/piquel-fr/api/utils/middleware" ) @@ -151,7 +153,17 @@ func (h *UserHandler) createHttpHandler() http.Handler { return handler } -func (h *UserHandler) handleGetSelf(w http.ResponseWriter, r *http.Request) {} +func (h *UserHandler) handleGetSelf(w http.ResponseWriter, r *http.Request) { + user, err := h.authService.GetUserFromContext(r.Context()) + if err != nil { + errors.HandleError(w, r, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} + func (h *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {} // TODO: check for view email address permission, if false return empty email address func (h *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {} // TODO: implement loads of validation for username (like make blacklist) func (h *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {} From 6b3ad52034eaba66a4645ea669f3044bd781e617 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Mon, 26 Jan 2026 20:44:27 +0100 Subject: [PATCH 28/45] setup get user endpoint --- api/users.go | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/api/users.go b/api/users.go index f28a40a..da775a6 100644 --- a/api/users.go +++ b/api/users.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/getkin/kin-openapi/openapi3" + "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/users" "github.com/piquel-fr/api/utils/errors" @@ -164,7 +165,38 @@ func (h *UserHandler) handleGetSelf(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(user) } -func (h *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {} // TODO: check for view email address permission, if false return empty email address +func (h *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) { + username := r.PathValue("user") + user, err := h.userService.GetUserByUsername(r.Context(), username) + if err != nil { + errors.HandleError(w, r, err) + return + } + + requester, err := h.authService.GetUserFromContext(r.Context()) + if err != nil { + errors.HandleError(w, r, err) + return + } + + request := &config.AuthRequest{ + User: requester, + Ressource: user, + Actions: []string{auth.ActionViewEmail}, + Context: r.Context(), + } + + if err := h.authService.Authorize(request); err == errors.ErrorForbidden { + user.Email = "" + } else if err != nil { + errors.HandleError(w, r, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} + func (h *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {} // TODO: implement loads of validation for username (like make blacklist) func (h *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {} func (h *UserHandler) handlePutUserAdmin(w http.ResponseWriter, r *http.Request) {} // TODO: allow updating role and email From 0057e660152f35d9601074ec5fc08f73842721df Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 18:56:27 +0100 Subject: [PATCH 29/45] document update user admin route --- api/users.go | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/api/users.go b/api/users.go index da775a6..29ef498 100644 --- a/api/users.go +++ b/api/users.go @@ -6,6 +6,8 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/piquel-fr/api/config" + "github.com/piquel-fr/api/database" + "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/users" "github.com/piquel-fr/api/utils/errors" @@ -42,9 +44,18 @@ func (h *UserHandler) getSpec() Spec { WithProperty("image", openapi3.NewStringSchema()). WithRequired([]string{"username", "name", "image"}) + updateUserAdminSchema := openapi3.NewObjectSchema(). + WithProperty("username", openapi3.NewStringSchema()). + WithProperty("name", openapi3.NewStringSchema()). + WithProperty("image", openapi3.NewStringSchema()). + WithProperty("email", openapi3.NewStringSchema()). + WithProperty("role", openapi3.NewStringSchema()). + WithRequired([]string{"username", "name", "image", "email", "role"}) + spec.Components.Schemas = openapi3.Schemas{ - "User": &openapi3.SchemaRef{Value: userSchema}, - "UpdateUserParams": &openapi3.SchemaRef{Value: updateUserSchema}, + "User": &openapi3.SchemaRef{Value: userSchema}, + "UpdateUserParams": &openapi3.SchemaRef{Value: updateUserSchema}, + "UpdateUserAdminParams": &openapi3.SchemaRef{Value: updateUserAdminSchema}, } spec.AddOperation("/self", http.MethodGet, &openapi3.Operation{ @@ -134,6 +145,36 @@ func (h *UserHandler) getSpec() Spec { ), }) + spec.AddOperation("/{user}/admin", http.MethodPut, &openapi3.Operation{ + Tags: []string{"users", "admin"}, + Summary: "Update user", + Description: "Update the profile of the specified user", + OperationID: "update-user-admin", + Parameters: openapi3.Parameters{ + &openapi3.ParameterRef{ + Value: &openapi3.Parameter{ + Name: "user", + In: "path", + Required: true, + Description: "The username to update", + Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}, + }, + }, + }, + RequestBody: &openapi3.RequestBodyRef{ + Value: &openapi3.RequestBody{ + Required: true, + Content: openapi3.NewContentWithJSONSchemaRef( + openapi3.NewSchemaRef("#/components/schemas/UpdateUserAdminParams", updateUserAdminSchema), + ), + }, + }, + Responses: openapi3.NewResponses( + openapi3.WithStatus(200, &openapi3.ResponseRef{Value: openapi3.NewResponse().WithDescription("User updated successfully")}), + openapi3.WithStatus(400, &openapi3.ResponseRef{Value: openapi3.NewResponse().WithContent(openapi3.NewContentWithSchema(openapi3.NewStringSchema(), []string{"text/plain"})).WithDescription("Invalid input or json")}), + ), + }) + return spec } @@ -144,8 +185,6 @@ func (h *UserHandler) createHttpHandler() http.Handler { handler.HandleFunc("GET /{user}", h.handleGetUser) handler.HandleFunc("PUT /{user}", h.handlePutUser) handler.HandleFunc("DELETE /{user}", h.handleDeleteUser) - - // TODO: add spec entry handler.HandleFunc("PUT /{user}/admin", h.handlePutUserAdmin) handler.Handle("OPTIONS /self", middleware.CreateOptionsHandler("GET")) From fc34608d37017bb5008b85f1423aff9ff5e4933b Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 18:56:44 +0100 Subject: [PATCH 30/45] implement last three users routes --- api/users.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 3 deletions(-) diff --git a/api/users.go b/api/users.go index 29ef498..7c138ba 100644 --- a/api/users.go +++ b/api/users.go @@ -236,6 +236,129 @@ func (h *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(user) } -func (h *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {} // TODO: implement loads of validation for username (like make blacklist) -func (h *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {} -func (h *UserHandler) handlePutUserAdmin(w http.ResponseWriter, r *http.Request) {} // TODO: allow updating role and email +func (h *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) { + username := r.PathValue("user") + + user, err := h.userService.GetUserByUsername(r.Context(), username) + if err != nil { + errors.HandleError(w, r, err) + return + } + + requester, err := h.authService.GetUserFromContext(r.Context()) + if err != nil { + errors.HandleError(w, r, err) + return + } + + request := &config.AuthRequest{ + User: requester, + Ressource: user, + Actions: []string{auth.ActionUpdate}, + Context: r.Context(), + } + + if err := h.authService.Authorize(request); err != nil { + errors.HandleError(w, r, err) + return + } + + if r.Header.Get("Content-Type") != "application/json" { + http.Error(w, "please submit your creation request with the required json payload", http.StatusBadRequest) + return + } + + params := repository.UpdateUserParams{} + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + errors.HandleError(w, r, err) + return + } + + params.ID = user.ID + + if err := database.Queries.UpdateUser(r.Context(), params); err != nil { + errors.HandleError(w, r, err) + return + } + w.WriteHeader(http.StatusOK) +} + +func (h *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) { + username := r.PathValue("user") + user, err := h.userService.GetUserByUsername(r.Context(), username) + if err != nil { + errors.HandleError(w, r, err) + return + } + + requester, err := h.authService.GetUserFromContext(r.Context()) + if err != nil { + errors.HandleError(w, r, err) + return + } + + request := &config.AuthRequest{ + User: requester, + Ressource: user, + Actions: []string{auth.ActionDelete}, + Context: r.Context(), + } + + if err := h.authService.Authorize(request); err != nil { + errors.HandleError(w, r, err) + return + } + + if err := h.userService.DeleteUser(r.Context(), user); err != nil { + errors.HandleError(w, r, err) + return + } + w.WriteHeader(http.StatusOK) +} + +func (h *UserHandler) handlePutUserAdmin(w http.ResponseWriter, r *http.Request) { + username := r.PathValue("user") + + user, err := h.userService.GetUserByUsername(r.Context(), username) + if err != nil { + errors.HandleError(w, r, err) + return + } + + requester, err := h.authService.GetUserFromContext(r.Context()) + if err != nil { + errors.HandleError(w, r, err) + return + } + + request := &config.AuthRequest{ + User: requester, + Ressource: user, + Actions: []string{auth.ActionUpdateAdmin}, + Context: r.Context(), + } + + if err := h.authService.Authorize(request); err != nil { + errors.HandleError(w, r, err) + return + } + + if r.Header.Get("Content-Type") != "application/json" { + http.Error(w, "please submit your creation request with the required json payload", http.StatusBadRequest) + return + } + + params := repository.UpdateUserAdminParams{} + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + errors.HandleError(w, r, err) + return + } + + params.ID = user.ID + + if err := database.Queries.UpdateUserAdmin(r.Context(), params); err != nil { + errors.HandleError(w, r, err) + return + } + w.WriteHeader(http.StatusOK) +} From 9f895e724dfc49d396d70f4e071d983e97be6810 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 18:57:03 +0100 Subject: [PATCH 31/45] fix user deletion --- services/users/users.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/users/users.go b/services/users/users.go index 7a396f4..de18444 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -23,7 +23,7 @@ type UserService interface { UpdateUser(ctx context.Context, id int32, username, name, image string) error UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error RegisterUser(ctx context.Context, username, email, name, image, role string) (*repository.User, error) - DeleteUser(ctx context.Context, id int32) error + DeleteUser(ctx context.Context, user *repository.User) error // other FormatAndValidateUsername(username string) (string, error) @@ -109,7 +109,7 @@ func (s *realUserService) RegisterUser(ctx context.Context, username, email, nam return &user, err } -func (s *realUserService) DeleteUser(ctx context.Context, id int32) error { +func (s *realUserService) DeleteUser(ctx context.Context, user *repository.User) error { // TODO: delete user return nil } From 018e5cee5f2271a920c1d3361d9e3bb19f1aefdb Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 18:59:18 +0100 Subject: [PATCH 32/45] remove username formatting util --- services/users/users.go | 4 ++-- utils/oauth/github.go | 3 +-- utils/oauth/google.go | 3 +-- utils/users.go | 7 ------- 4 files changed, 4 insertions(+), 13 deletions(-) delete mode 100644 utils/users.go diff --git a/services/users/users.go b/services/users/users.go index de18444..1ccf57a 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -4,11 +4,11 @@ import ( "context" "fmt" "slices" + "strings" "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" - "github.com/piquel-fr/api/utils" ) type UserService interface { @@ -115,7 +115,7 @@ func (s *realUserService) DeleteUser(ctx context.Context, user *repository.User) } func (s *realUserService) FormatAndValidateUsername(username string) (string, error) { - username = utils.FormatUsername(username) + username = strings.ReplaceAll(strings.ToLower(username), " ", "") if slices.Contains(config.UsernameBlacklist, username) { return "", fmt.Errorf("username %s is not legal", username) } diff --git a/utils/oauth/github.go b/utils/oauth/github.go index 5f0a8e9..dab0014 100644 --- a/utils/oauth/github.go +++ b/utils/oauth/github.go @@ -7,7 +7,6 @@ import ( "net/http" "strings" - "github.com/piquel-fr/api/utils" "github.com/piquel-fr/api/utils/errors" "golang.org/x/oauth2" ) @@ -55,7 +54,7 @@ func (gh *github) FetchUser(context context.Context, token *oauth2.Token) (*User Name: u.Name, Email: u.Email, Image: u.Picture, - Username: utils.FormatUsername(u.Login), + Username: u.Login, // will be formated by user service } if user.Email == "" { diff --git a/utils/oauth/google.go b/utils/oauth/google.go index 8fb7d0e..555e680 100644 --- a/utils/oauth/google.go +++ b/utils/oauth/google.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" - "github.com/piquel-fr/api/utils" "golang.org/x/oauth2" ) @@ -48,7 +47,7 @@ func (gg *google) FetchUser(context context.Context, token *oauth2.Token) (*User Name: u.Name, Email: u.Email, Image: u.Picture, - Username: utils.FormatUsername(u.Name), + Username: u.Name, // will be formated by the users service } return user, nil diff --git a/utils/users.go b/utils/users.go deleted file mode 100644 index f5d328b..0000000 --- a/utils/users.go +++ /dev/null @@ -1,7 +0,0 @@ -package utils - -import "strings" - -func FormatUsername(username string) string { - return strings.ReplaceAll(strings.ToLower(username), " ", "") -} From cb49e7905b60e704f9624c673df28baa00e2103b Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 19:04:03 +0100 Subject: [PATCH 33/45] add list usernames --- database/queries/users.sql | 3 +++ database/repository/users.sql.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/database/queries/users.sql b/database/queries/users.sql index bba3663..161b008 100644 --- a/database/queries/users.sql +++ b/database/queries/users.sql @@ -14,6 +14,9 @@ SELECT * FROM "users" WHERE "email" = $1; -- name: ListUsers :many SELECT * FROM "users" ORDER BY "id" ASC LIMIT $1 OFFSET $2; +-- name: ListUserNames :many +SELECT "username" FROM "users"; + -- name: UpdateUser :exec UPDATE "users" SET "username" = $2, "name" = $3, "image" = $4 WHERE "id" = $1; diff --git a/database/repository/users.sql.go b/database/repository/users.sql.go index 7348aa5..bffaddb 100644 --- a/database/repository/users.sql.go +++ b/database/repository/users.sql.go @@ -100,6 +100,30 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, return i, err } +const listUserNames = `-- name: ListUserNames :many +SELECT "username" FROM "users" +` + +func (q *Queries) ListUserNames(ctx context.Context) ([]string, error) { + rows, err := q.db.Query(ctx, listUserNames) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var username string + if err := rows.Scan(&username); err != nil { + return nil, err + } + items = append(items, username) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listUsers = `-- name: ListUsers :many SELECT id, username, name, image, email, role, "createdAt" FROM "users" ORDER BY "id" ASC LIMIT $1 OFFSET $2 ` From 70b2f7085c74e2a7fae212f0a364083fafb98f6f Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 19:25:29 +0100 Subject: [PATCH 34/45] update username validation --- services/users/users.go | 54 +++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/services/users/users.go b/services/users/users.go index 1ccf57a..f7e888e 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -2,13 +2,18 @@ package users import ( "context" + "crypto/rand" "fmt" + "log" + "net/http" + "regexp" "slices" "strings" "github.com/piquel-fr/api/config" "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" + "github.com/piquel-fr/api/utils/errors" ) type UserService interface { @@ -26,7 +31,6 @@ type UserService interface { DeleteUser(ctx context.Context, user *repository.User) error // other - FormatAndValidateUsername(username string) (string, error) ListUsers(ctx context.Context, offset, limit int32) ([]repository.User, error) } @@ -52,7 +56,7 @@ func (s *realUserService) GetUserByEmail(ctx context.Context, email string) (*re } func (s *realUserService) UpdateUser(ctx context.Context, id int32, username, name, image string) error { - username, err := s.FormatAndValidateUsername(username) + username, err := s.formatAndValidateUsername(ctx, username, false) if err != nil { return err } @@ -67,7 +71,7 @@ func (s *realUserService) UpdateUser(ctx context.Context, id int32, username, na } func (s *realUserService) UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error { - username, err := s.FormatAndValidateUsername(username) + username, err := s.formatAndValidateUsername(ctx, username, false) if err != nil { return err } @@ -88,7 +92,7 @@ func (s *realUserService) UpdateUserAdmin(ctx context.Context, id int32, usernam } func (s *realUserService) RegisterUser(ctx context.Context, username, email, name, image, role string) (*repository.User, error) { - username, err := s.FormatAndValidateUsername(username) + username, err := s.formatAndValidateUsername(ctx, username, true) if err != nil { return nil, err } @@ -114,13 +118,49 @@ func (s *realUserService) DeleteUser(ctx context.Context, user *repository.User) return nil } -func (s *realUserService) FormatAndValidateUsername(username string) (string, error) { +// @param force: if the validation can fail. When creating a new user through OAuth, user creation cannot fail. We will thus create a random one +func (s *realUserService) formatAndValidateUsername(ctx context.Context, username string, force bool) (string, error) { + log.Printf("formatting %s", username) + random := false username = strings.ReplaceAll(strings.ToLower(username), " ", "") + + matched, err := regexp.MatchString("[a-z0-9]+", username) + if !matched { + random = true + if !force { + return "", errors.NewError(fmt.Sprintf("username %s contains illegal characters. only letters and numbers are allowed", username), http.StatusBadRequest) + } + } + if slices.Contains(config.UsernameBlacklist, username) { - return "", fmt.Errorf("username %s is not legal", username) + random = true + if !force { + return "", errors.NewError(fmt.Sprintf("username %s is not legal", username), http.StatusBadRequest) + } } - // TODO: make sure no one already has the username + names, err := database.Queries.ListUserNames(ctx) + if err != nil { + random = true + if !force { + return "", nil + } + } + + if slices.Contains(names, username) { + random = true + if !force { + return "", errors.NewError(fmt.Sprintf("username %s is already taken", username), http.StatusBadRequest) + } + } + + if random { + username = rand.Text() + username, err = s.formatAndValidateUsername(ctx, username, true) + if err != nil { + return "", fmt.Errorf("something terrible happened in username validation:\n\tusername: %s\n\terror: %w", username, err) + } + } return username, nil } From 88b8eb76a93a5e35b4168e72625d8472b64bcc07 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 19:29:17 +0100 Subject: [PATCH 35/45] cleaned up user updating --- api/users.go | 5 ++--- services/users/users.go | 30 +++++++++--------------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/api/users.go b/api/users.go index 7c138ba..4d64f50 100644 --- a/api/users.go +++ b/api/users.go @@ -6,7 +6,6 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/piquel-fr/api/config" - "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/users" @@ -276,7 +275,7 @@ func (h *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) { params.ID = user.ID - if err := database.Queries.UpdateUser(r.Context(), params); err != nil { + if err := h.userService.UpdateUser(r.Context(), params); err != nil { errors.HandleError(w, r, err) return } @@ -356,7 +355,7 @@ func (h *UserHandler) handlePutUserAdmin(w http.ResponseWriter, r *http.Request) params.ID = user.ID - if err := database.Queries.UpdateUserAdmin(r.Context(), params); err != nil { + if err := h.userService.UpdateUserAdmin(r.Context(), params); err != nil { errors.HandleError(w, r, err) return } diff --git a/services/users/users.go b/services/users/users.go index f7e888e..32d2c54 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -25,8 +25,8 @@ type UserService interface { GetUserByEmail(ctx context.Context, email string) (*repository.User, error) // managing users - UpdateUser(ctx context.Context, id int32, username, name, image string) error - UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error + UpdateUser(ctx context.Context, params repository.UpdateUserParams) error + UpdateUserAdmin(ctx context.Context, params repository.UpdateUserAdminParams) error RegisterUser(ctx context.Context, username, email, name, image, role string) (*repository.User, error) DeleteUser(ctx context.Context, user *repository.User) error @@ -55,39 +55,27 @@ func (s *realUserService) GetUserByEmail(ctx context.Context, email string) (*re return &user, err } -func (s *realUserService) UpdateUser(ctx context.Context, id int32, username, name, image string) error { - username, err := s.formatAndValidateUsername(ctx, username, false) +func (s *realUserService) UpdateUser(ctx context.Context, params repository.UpdateUserParams) error { + username, err := s.formatAndValidateUsername(ctx, params.Username, false) if err != nil { return err } - params := repository.UpdateUserParams{ - ID: id, - Username: username, - Name: name, - Image: image, - } + params.Username = username return database.Queries.UpdateUser(ctx, params) } -func (s *realUserService) UpdateUserAdmin(ctx context.Context, id int32, username, email, name, image, role string) error { - username, err := s.formatAndValidateUsername(ctx, username, false) +func (s *realUserService) UpdateUserAdmin(ctx context.Context, params repository.UpdateUserAdminParams) error { + username, err := s.formatAndValidateUsername(ctx, params.Username, false) if err != nil { return err } - if err := config.Policy.ValidateRole(role); err != nil { + if err := config.Policy.ValidateRole(params.Role); err != nil { return err } - params := repository.UpdateUserAdminParams{ - ID: id, - Username: username, - Email: email, - Name: name, - Image: image, - Role: role, - } + params.Username = username return database.Queries.UpdateUserAdmin(ctx, params) } From c86346a03f7a265f3b662efdfeb991115f0f74eb Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 19:44:42 +0100 Subject: [PATCH 36/45] move user context function to users service --- api/email.go | 12 ++++++------ api/profile.go | 2 +- api/users.go | 10 +++++----- config/config.go | 1 + services/auth/auth.go | 15 +-------------- services/users/users.go | 9 +++++++++ 6 files changed, 23 insertions(+), 26 deletions(-) diff --git a/api/email.go b/api/email.go index 18e1d2a..69c0f90 100644 --- a/api/email.go +++ b/api/email.go @@ -236,7 +236,7 @@ func (h *EmailHandler) createHttpHandler() http.Handler { } func (h *EmailHandler) handleListAccounts(w http.ResponseWriter, r *http.Request) { - requester, err := h.authService.GetUserFromContext(r.Context()) + requester, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -297,7 +297,7 @@ func (h *EmailHandler) handleListAccounts(w http.ResponseWriter, r *http.Request } func (h *EmailHandler) handleAddAccount(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromContext(r.Context()) + user, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -322,7 +322,7 @@ func (h *EmailHandler) handleAddAccount(w http.ResponseWriter, r *http.Request) } func (h *EmailHandler) handleAccountInfo(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromContext(r.Context()) + user, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -364,7 +364,7 @@ func (h *EmailHandler) handleAccountInfo(w http.ResponseWriter, r *http.Request) } func (h *EmailHandler) handleRemoveAccount(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromContext(r.Context()) + user, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -393,7 +393,7 @@ func (h *EmailHandler) handleRemoveAccount(w http.ResponseWriter, r *http.Reques } func (h *EmailHandler) handleShareAccount(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromContext(r.Context()) + user, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -434,7 +434,7 @@ func (h *EmailHandler) handleShareAccount(w http.ResponseWriter, r *http.Request } func (h *EmailHandler) handleRemoveAccountShare(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromContext(r.Context()) + user, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return diff --git a/api/profile.go b/api/profile.go index 6b7beae..f4968e3 100644 --- a/api/profile.go +++ b/api/profile.go @@ -150,7 +150,7 @@ func (h *ProfileHandler) handleGetProfile(w http.ResponseWriter, r *http.Request func (h *ProfileHandler) handleGetProfileQuery(w http.ResponseWriter, r *http.Request) { username := r.URL.Query().Get("username") if username == "" { - user, err := h.authService.GetUserFromContext(r.Context()) + user, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return diff --git a/api/users.go b/api/users.go index 4d64f50..501691a 100644 --- a/api/users.go +++ b/api/users.go @@ -193,7 +193,7 @@ func (h *UserHandler) createHttpHandler() http.Handler { } func (h *UserHandler) handleGetSelf(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetUserFromContext(r.Context()) + user, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -211,7 +211,7 @@ func (h *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) { return } - requester, err := h.authService.GetUserFromContext(r.Context()) + requester, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -244,7 +244,7 @@ func (h *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) { return } - requester, err := h.authService.GetUserFromContext(r.Context()) + requester, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -290,7 +290,7 @@ func (h *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) { return } - requester, err := h.authService.GetUserFromContext(r.Context()) + requester, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return @@ -324,7 +324,7 @@ func (h *UserHandler) handlePutUserAdmin(w http.ResponseWriter, r *http.Request) return } - requester, err := h.authService.GetUserFromContext(r.Context()) + requester, err := h.userService.GetUserFromContext(r.Context()) if err != nil { errors.HandleError(w, r, err) return diff --git a/config/config.go b/config/config.go index f671d47..d5125f4 100644 --- a/config/config.go +++ b/config/config.go @@ -40,6 +40,7 @@ func GetPublicConfig() PublicConfig { var Envs EnvsConfig var MaxDocsInstanceCount int64 = 3 var JWTSigningMethod jwt.SigningMethod = jwt.SigningMethodHS256 +var UserContextKey = "user" // these are populated by external services var UsernameBlacklist []string diff --git a/services/auth/auth.go b/services/auth/auth.go index 2d6d6cd..ae05495 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -25,16 +25,11 @@ type AuthService interface { getTokenFromRequest(r *http.Request) (*jwt.Token, error) getUserFromToken(ctx context.Context, token *jwt.Token) (*repository.User, error) - // authentication - GetUserFromContext(ctx context.Context) (*repository.User, error) - // authorization Authorize(request *config.AuthRequest) error AuthMiddleware(next http.Handler) http.Handler } -var userKey = "user" - type realAuthService struct { userService users.UserService } @@ -93,14 +88,6 @@ func (s *realAuthService) getUserFromToken(ctx context.Context, token *jwt.Token return s.userService.GetUserById(ctx, int32(id)) } -func (s *realAuthService) GetUserFromContext(ctx context.Context) (*repository.User, error) { - user, ok := ctx.Value(userKey).(*repository.User) - if !ok { - return nil, fmt.Errorf("user is not in context") - } - return user, nil -} - func (s *realAuthService) AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodOptions { @@ -120,7 +107,7 @@ func (s *realAuthService) AuthMiddleware(next http.Handler) http.Handler { return } - newReq := r.WithContext(context.WithValue(r.Context(), userKey, user)) + newReq := r.WithContext(context.WithValue(r.Context(), config.UserContextKey, user)) next.ServeHTTP(w, newReq) }) } diff --git a/services/users/users.go b/services/users/users.go index 32d2c54..31e8300 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -23,6 +23,7 @@ type UserService interface { GetUserById(ctx context.Context, id int32) (*repository.User, error) GetUserByUsername(ctx context.Context, username string) (*repository.User, error) GetUserByEmail(ctx context.Context, email string) (*repository.User, error) + GetUserFromContext(ctx context.Context) (*repository.User, error) // managing users UpdateUser(ctx context.Context, params repository.UpdateUserParams) error @@ -55,6 +56,14 @@ func (s *realUserService) GetUserByEmail(ctx context.Context, email string) (*re return &user, err } +func (s *realUserService) GetUserFromContext(ctx context.Context) (*repository.User, error) { + user, ok := ctx.Value(config.UserContextKey).(*repository.User) + if !ok { + return nil, fmt.Errorf("user is not in context") + } + return user, nil +} + func (s *realUserService) UpdateUser(ctx context.Context, params repository.UpdateUserParams) error { username, err := s.formatAndValidateUsername(ctx, params.Username, false) if err != nil { From 0802561427d52204815b0217e24e2f0545702a98 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 19:44:56 +0100 Subject: [PATCH 37/45] make profile user new user api --- api/profile.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/profile.go b/api/profile.go index f4968e3..d27e6e8 100644 --- a/api/profile.go +++ b/api/profile.go @@ -6,7 +6,6 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/piquel-fr/api/config" - "github.com/piquel-fr/api/database" "github.com/piquel-fr/api/database/repository" "github.com/piquel-fr/api/services/auth" "github.com/piquel-fr/api/services/users" @@ -206,7 +205,7 @@ func (h *ProfileHandler) handleUpdateProfile(w http.ResponseWriter, r *http.Requ params.ID = user.ID - if err := database.Queries.UpdateUser(r.Context(), params); err != nil { + if err := h.userService.UpdateUser(r.Context(), params); err != nil { errors.HandleError(w, r, err) return } From f5fac14d00115980f0e3f41ad50e845bb616605e Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 19:46:11 +0100 Subject: [PATCH 38/45] fix error when not updating username --- services/users/users.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/users/users.go b/services/users/users.go index 31e8300..a6fb23b 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -117,6 +117,15 @@ func (s *realUserService) DeleteUser(ctx context.Context, user *repository.User) // @param force: if the validation can fail. When creating a new user through OAuth, user creation cannot fail. We will thus create a random one func (s *realUserService) formatAndValidateUsername(ctx context.Context, username string, force bool) (string, error) { + user, err := s.GetUserFromContext(ctx) + if err != nil { + return "", err + } + + if user.Username == username { + return username, nil + } + log.Printf("formatting %s", username) random := false username = strings.ReplaceAll(strings.ToLower(username), " ", "") From 7ce1630c4415b003e1856f83a827faaa9ce1b3dc Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 19:48:32 +0100 Subject: [PATCH 39/45] cleanup username validation --- services/users/users.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/services/users/users.go b/services/users/users.go index a6fb23b..adf8682 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -117,6 +117,7 @@ func (s *realUserService) DeleteUser(ctx context.Context, user *repository.User) // @param force: if the validation can fail. When creating a new user through OAuth, user creation cannot fail. We will thus create a random one func (s *realUserService) formatAndValidateUsername(ctx context.Context, username string, force bool) (string, error) { + // check if username actually changing user, err := s.GetUserFromContext(ctx) if err != nil { return "", err @@ -126,10 +127,19 @@ func (s *realUserService) formatAndValidateUsername(ctx context.Context, usernam return username, nil } - log.Printf("formatting %s", username) + // formatting random := false username = strings.ReplaceAll(strings.ToLower(username), " ", "") + // blacklist + if slices.Contains(config.UsernameBlacklist, username) { + random = true + if !force { + return "", errors.NewError(fmt.Sprintf("username %s is not legal", username), http.StatusBadRequest) + } + } + + // regex matched, err := regexp.MatchString("[a-z0-9]+", username) if !matched { random = true @@ -137,14 +147,14 @@ func (s *realUserService) formatAndValidateUsername(ctx context.Context, usernam return "", errors.NewError(fmt.Sprintf("username %s contains illegal characters. only letters and numbers are allowed", username), http.StatusBadRequest) } } - - if slices.Contains(config.UsernameBlacklist, username) { + if err != nil { random = true if !force { - return "", errors.NewError(fmt.Sprintf("username %s is not legal", username), http.StatusBadRequest) + return "", fmt.Errorf("error matching regex in username validation") } } + // already existing users names, err := database.Queries.ListUserNames(ctx) if err != nil { random = true @@ -160,6 +170,7 @@ func (s *realUserService) formatAndValidateUsername(ctx context.Context, usernam } } + // random generation if random { username = rand.Text() username, err = s.formatAndValidateUsername(ctx, username, true) From 67797658d17d2733f6f461ae5096eb5330990447 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 19:51:52 +0100 Subject: [PATCH 40/45] fix regex to match full string --- services/users/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/users/users.go b/services/users/users.go index adf8682..0159b8c 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -140,7 +140,7 @@ func (s *realUserService) formatAndValidateUsername(ctx context.Context, usernam } // regex - matched, err := regexp.MatchString("[a-z0-9]+", username) + matched, err := regexp.MatchString("^[a-z0-9]+$", username) if !matched { random = true if !force { From d47da989677b690d7022b377ccddae2fe9c73a65 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 19:52:08 +0100 Subject: [PATCH 41/45] fix build error --- services/users/users.go | 1 - 1 file changed, 1 deletion(-) diff --git a/services/users/users.go b/services/users/users.go index 0159b8c..283201f 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -4,7 +4,6 @@ import ( "context" "crypto/rand" "fmt" - "log" "net/http" "regexp" "slices" From 9a989cccf41b980d2afadc57c13812e8f3d42ac1 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 20:12:31 +0100 Subject: [PATCH 42/45] improve error handling --- services/users/users.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/users/users.go b/services/users/users.go index 283201f..01982ac 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -68,8 +68,8 @@ func (s *realUserService) UpdateUser(ctx context.Context, params repository.Upda if err != nil { return err } - params.Username = username + return database.Queries.UpdateUser(ctx, params) } @@ -78,12 +78,12 @@ func (s *realUserService) UpdateUserAdmin(ctx context.Context, params repository if err != nil { return err } + params.Username = username if err := config.Policy.ValidateRole(params.Role); err != nil { return err } - params.Username = username return database.Queries.UpdateUserAdmin(ctx, params) } @@ -149,7 +149,7 @@ func (s *realUserService) formatAndValidateUsername(ctx context.Context, usernam if err != nil { random = true if !force { - return "", fmt.Errorf("error matching regex in username validation") + return "", fmt.Errorf("error matching regex in username validation: %w", err) } } From f381d92a2275f68060a908aae1d4088fc6412fe1 Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 20:14:47 +0100 Subject: [PATCH 43/45] fix typo --- services/users/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/users/users.go b/services/users/users.go index 01982ac..2e201c6 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -158,7 +158,7 @@ func (s *realUserService) formatAndValidateUsername(ctx context.Context, usernam if err != nil { random = true if !force { - return "", nil + return "", err } } From dbae6b98a58c91242fb1cb65a9624e0029ccff4d Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 20:38:38 +0100 Subject: [PATCH 44/45] fix update user schema --- api/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/users.go b/api/users.go index 501691a..ab9f9f8 100644 --- a/api/users.go +++ b/api/users.go @@ -47,7 +47,7 @@ func (h *UserHandler) getSpec() Spec { WithProperty("username", openapi3.NewStringSchema()). WithProperty("name", openapi3.NewStringSchema()). WithProperty("image", openapi3.NewStringSchema()). - WithProperty("email", openapi3.NewStringSchema()). + WithProperty("email", openapi3.NewStringSchema().WithFormat("email")). WithProperty("role", openapi3.NewStringSchema()). WithRequired([]string{"username", "name", "image", "email", "role"}) From 5eae13888c2b284434359e296f059965dc13461b Mon Sep 17 00:00:00 2001 From: PiquelChips Date: Tue, 27 Jan 2026 20:43:43 +0100 Subject: [PATCH 45/45] add missing options handler --- api/users.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/users.go b/api/users.go index ab9f9f8..dd2c65f 100644 --- a/api/users.go +++ b/api/users.go @@ -188,6 +188,7 @@ func (h *UserHandler) createHttpHandler() http.Handler { handler.Handle("OPTIONS /self", middleware.CreateOptionsHandler("GET")) handler.Handle("OPTIONS /{user}", middleware.CreateOptionsHandler("GET", "PUT", "DELETE")) + handler.Handle("OPTIONS /{user}/admin", middleware.CreateOptionsHandler("PUT")) return handler }