diff --git a/api/auth.go b/api/auth.go
index cee390f..487baa4 100644
--- a/api/auth.go
+++ b/api/auth.go
@@ -5,25 +5,28 @@ 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"
"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 {
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)
@@ -71,13 +74,16 @@ 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 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
}
- tokenString, err := h.authService.GenerateTokenString(user.ID)
+ 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 98fcdcb..69c0f90 100644
--- a/api/email.go
+++ b/api/email.go
@@ -6,21 +6,24 @@ 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"
"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" }
@@ -40,11 +43,9 @@ 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 = openapi3.Schemas{
+ "MailAccount": &openapi3.SchemaRef{Value: accountSchema},
+ "AddAccountPayload": &openapi3.SchemaRef{Value: addAccountSchema},
}
spec.AddOperation("/", http.MethodGet, &openapi3.Operation{
@@ -235,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.userService.GetUserFromContext(r.Context())
if err != nil {
errors.HandleError(w, r, err)
return
@@ -243,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
@@ -252,7 +253,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(),
@@ -296,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.userService.GetUserFromContext(r.Context())
if err != nil {
errors.HandleError(w, r, err)
return
@@ -321,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.userService.GetUserFromContext(r.Context())
if err != nil {
errors.HandleError(w, r, err)
return
@@ -339,7 +340,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},
@@ -363,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.userService.GetUserFromContext(r.Context())
if err != nil {
errors.HandleError(w, r, err)
return
@@ -375,7 +376,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},
@@ -392,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.userService.GetUserFromContext(r.Context())
if err != nil {
errors.HandleError(w, r, err)
return
@@ -404,7 +405,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},
@@ -414,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
@@ -433,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.userService.GetUserFromContext(r.Context())
if err != nil {
errors.HandleError(w, r, err)
return
@@ -445,7 +446,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},
@@ -455,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 74f8e20..3914a72 100644
--- a/api/handler.go
+++ b/api/handler.go
@@ -2,13 +2,16 @@ 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"
"github.com/piquel-fr/api/utils/middleware"
)
@@ -20,17 +23,24 @@ 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
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 {
+ return nil, err
+ }
+ router.HandleFunc("/config.json", configHandler)
handlers := []Handler{
- CreateProfileHandler(authService),
- CreateEmailHandler(authService, emailService),
+ CreateUserHandler(userService, authService),
+ CreateProfileHandler(userService, authService),
+ CreateEmailHandler(userService, authService, emailService),
}
for _, handler := range handlers {
@@ -45,7 +55,7 @@ func CreateRouter(authService auth.AuthService, emailService email.EmailService)
// 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
@@ -68,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",
@@ -86,6 +108,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 622e11b..d27e6e8 100644
--- a/api/profile.go
+++ b/api/profile.go
@@ -5,25 +5,28 @@ import (
"net/http"
"github.com/getkin/kin-openapi/openapi3"
- "github.com/piquel-fr/api/database"
+ "github.com/piquel-fr/api/config"
"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" }
func (h *ProfileHandler) getSpec() Spec {
spec := newSpecBase(h)
+ spec.Info.Description = "DEPRECATED " + spec.Info.Description
userSchema := openapi3.NewObjectSchema().
WithProperty("id", openapi3.NewInt32Schema()).
@@ -41,11 +44,9 @@ 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 = openapi3.Schemas{
+ "User": &openapi3.SchemaRef{Value: userSchema},
+ "UpdateUserParams": &openapi3.SchemaRef{Value: updateUserSchema},
}
spec.AddOperation("/{user}", http.MethodGet, &openapi3.Operation{
@@ -148,12 +149,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.userService.GetUserFromContext(r.Context())
if err != nil {
errors.HandleError(w, r, err)
return
@@ -165,7 +161,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,13 +174,13 @@ 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
}
- request := &auth.Request{
+ request := &config.AuthRequest{
User: user,
Ressource: user,
Actions: []string{auth.ActionUpdate},
@@ -209,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
}
diff --git a/api/users.go b/api/users.go
new file mode 100644
index 0000000..dd2c65f
--- /dev/null
+++ b/api/users.go
@@ -0,0 +1,364 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/getkin/kin-openapi/openapi3"
+ "github.com/piquel-fr/api/config"
+ "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 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"})
+
+ updateUserAdminSchema := openapi3.NewObjectSchema().
+ WithProperty("username", openapi3.NewStringSchema()).
+ WithProperty("name", openapi3.NewStringSchema()).
+ WithProperty("image", openapi3.NewStringSchema()).
+ WithProperty("email", openapi3.NewStringSchema().WithFormat("email")).
+ 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},
+ "UpdateUserAdminParams": &openapi3.SchemaRef{Value: updateUserAdminSchema},
+ }
+
+ 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")}),
+ ),
+ })
+
+ 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
+}
+
+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)
+ handler.HandleFunc("PUT /{user}/admin", h.handlePutUserAdmin)
+
+ 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
+}
+
+func (h *UserHandler) handleGetSelf(w http.ResponseWriter, r *http.Request) {
+ user, err := h.userService.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) {
+ username := r.PathValue("user")
+ user, err := h.userService.GetUserByUsername(r.Context(), username)
+ if err != nil {
+ errors.HandleError(w, r, err)
+ return
+ }
+
+ requester, err := h.userService.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) {
+ username := r.PathValue("user")
+
+ user, err := h.userService.GetUserByUsername(r.Context(), username)
+ if err != nil {
+ errors.HandleError(w, r, err)
+ return
+ }
+
+ requester, err := h.userService.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 := h.userService.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.userService.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.userService.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 := h.userService.UpdateUserAdmin(r.Context(), params); err != nil {
+ errors.HandleError(w, r, err)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/config/config.go b/config/config.go
index 5240af4..d5125f4 100644
--- a/config/config.go
+++ b/config/config.go
@@ -28,9 +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
+var UserContextKey = "user"
+
+// these are populated by external services
+var UsernameBlacklist []string
+var Policy *PolicyConfiguration
func LoadConfig() {
godotenv.Load()
diff --git a/services/auth/models.go b/config/policy.go
similarity index 60%
rename from services/auth/models.go
rename to config/policy.go
index 06af735..5fb19b3 100644
--- a/services/auth/models.go
+++ b/config/policy.go
@@ -1,14 +1,25 @@
-package auth
+package config
import (
"context"
+ "fmt"
+ "net/http"
"github.com/piquel-fr/api/database/repository"
+ "github.com/piquel-fr/api/utils/errors"
)
type PolicyConfiguration struct {
Presets map[string]*Permission `json:"presets"`
- Roles Roles `json:"roles"`
+ Roles map[string]*Role `json:"roles"`
+}
+
+func (p *PolicyConfiguration) ValidateRole(role string) error {
+ _, ok := p.Roles[role]
+ if !ok {
+ return errors.NewError(fmt.Sprintf("role %s does not exist in current policy", role), http.StatusBadRequest)
+ }
+ return nil
}
type Permission struct {
@@ -17,16 +28,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
diff --git a/database/queries/users.sql b/database/queries/users.sql
index 607073f..161b008 100644
--- a/database/queries/users.sql
+++ b/database/queries/users.sql
@@ -11,5 +11,14 @@ 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: ListUserNames :many
+SELECT "username" FROM "users";
+
-- 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..bffaddb 100644
--- a/database/repository/users.sql.go
+++ b/database/repository/users.sql.go
@@ -100,6 +100,67 @@ 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
+`
+
+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
`
@@ -120,3 +181,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
+}
diff --git a/main.go b/main.go
index b9b92af..f551813 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,14 @@ func main() {
database.InitDatabase()
defer database.Connection.Close()
- authService := auth.NewRealAuthService()
+ userService := users.NewRealUserService()
+ authService := auth.NewRealAuthService(userService)
emailService := email.NewRealEmailService()
- router, err := api.CreateRouter(authService, emailService)
+ 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 58ee9dc..ae05495 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -8,48 +8,60 @@ 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 *Request) error
+ GetPolicy() *config.PolicyConfiguration
GetProvider(name string) (oauth.Provider, error)
- GetPolicy() *PolicyConfiguration
+ // 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)
+ getUserFromToken(ctx context.Context, token *jwt.Token) (*repository.User, error)
+
+ // authorization
+ Authorize(request *config.AuthRequest) error
+ AuthMiddleware(next http.Handler) http.Handler
}
-// auth service has no state
-type realAuthService struct{}
+type realAuthService struct {
+ userService users.UserService
+}
-func NewRealAuthService() *realAuthService {
- return &realAuthService{}
+func NewRealAuthService(userService users.UserService) *realAuthService {
+ return &realAuthService{userService}
}
-func (s *realAuthService) GetPolicy() *PolicyConfiguration { return &policy }
+func (s *realAuthService) GetPolicy() *config.PolicyConfiguration { return &policy }
+
+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
+}
-func (s *realAuthService) GenerateTokenString(userId int32) (string, error) {
- idString := strconv.Itoa(int(userId))
- token := jwt.NewWithClaims(config.JWTSigningMethod,
+// 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" {
@@ -62,74 +74,40 @@ 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)
+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
}
- return nil, err
- }
-
- 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
-
- user, err := database.Queries.AddUser(ctx, params)
- 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
-}
+ token, err := s.getTokenFromRequest(r)
+ if err != nil {
+ errors.HandleError(w, r, err)
+ return
+ }
-func (s *realAuthService) GetUserFromUserId(ctx context.Context, userId int32) (*repository.User, error) {
- user, err := database.Queries.GetUserById(ctx, userId)
- return &user, err
-}
+ user, err := s.getUserFromToken(r.Context(), token)
+ 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
+ newReq := r.WithContext(context.WithValue(r.Context(), config.UserContextKey, user))
+ next.ServeHTTP(w, newReq)
+ })
}
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/auth/policy.go b/services/auth/policy.go
index 4928ef7..ae1e5e8 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"
@@ -16,45 +17,49 @@ 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"
- ActionListEmailAccounts string = "list_email_accounts"
+ ActionUpdateAdmin = "update_admin"
+ ActionViewEmail = "view_email"
+ ActionListEmailAccounts = "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},
+ {Action: ActionViewEmail},
+ {Action: ActionUpdateAdmin},
},
repository.ResourceMailAccount: {
{Action: ActionView},
@@ -69,12 +74,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,10 +108,11 @@ var policy = PolicyConfiguration{
RoleDefault: {
Name: "",
Color: "gray",
- Permissions: map[string][]*Permission{
+ Permissions: map[string][]*config.Permission{
repository.ResourceUser: {
makeOwn(ActionUpdate),
makeOwn(ActionDelete),
+ makeOwn(ActionViewEmail),
},
},
},
diff --git a/services/users/users.go b/services/users/users.go
new file mode 100644
index 0000000..2e201c6
--- /dev/null
+++ b/services/users/users.go
@@ -0,0 +1,193 @@
+package users
+
+import (
+ "context"
+ "crypto/rand"
+ "fmt"
+ "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 {
+ GetUsernameBlacklist() []string
+
+ // 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)
+ GetUserFromContext(ctx context.Context) (*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, username, email, name, image, role string) (*repository.User, error)
+ DeleteUser(ctx context.Context, user *repository.User) error
+
+ // other
+ ListUsers(ctx context.Context, offset, limit int32) ([]repository.User, error)
+}
+
+type realUserService struct{}
+
+func NewRealUserService() *realUserService {
+ return &realUserService{}
+}
+
+func (s *realUserService) GetUserById(ctx context.Context, id int32) (*repository.User, error) {
+ user, err := database.Queries.GetUserById(ctx, id)
+ return &user, err
+}
+
+func (s *realUserService) GetUserByUsername(ctx context.Context, username string) (*repository.User, error) {
+ user, err := database.Queries.GetUserByUsername(ctx, username)
+ return &user, err
+}
+
+func (s *realUserService) GetUserByEmail(ctx context.Context, email string) (*repository.User, error) {
+ user, err := database.Queries.GetUserByEmail(ctx, email)
+ 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 {
+ return err
+ }
+ params.Username = username
+
+ return database.Queries.UpdateUser(ctx, params)
+}
+
+func (s *realUserService) UpdateUserAdmin(ctx context.Context, params repository.UpdateUserAdminParams) error {
+ username, err := s.formatAndValidateUsername(ctx, params.Username, false)
+ if err != nil {
+ return err
+ }
+ params.Username = username
+
+ if err := config.Policy.ValidateRole(params.Role); err != nil {
+ return err
+ }
+
+ return database.Queries.UpdateUserAdmin(ctx, params)
+}
+
+func (s *realUserService) RegisterUser(ctx context.Context, username, email, name, image, role string) (*repository.User, error) {
+ username, err := s.formatAndValidateUsername(ctx, username, true)
+ 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, user *repository.User) error {
+ // TODO: delete user
+ return nil
+}
+
+// @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
+ }
+
+ if user.Username == username {
+ return username, nil
+ }
+
+ // 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
+ if !force {
+ return "", errors.NewError(fmt.Sprintf("username %s contains illegal characters. only letters and numbers are allowed", username), http.StatusBadRequest)
+ }
+ }
+ if err != nil {
+ random = true
+ if !force {
+ return "", fmt.Errorf("error matching regex in username validation: %w", err)
+ }
+ }
+
+ // already existing users
+ names, err := database.Queries.ListUserNames(ctx)
+ if err != nil {
+ random = true
+ if !force {
+ return "", err
+ }
+ }
+
+ if slices.Contains(names, username) {
+ random = true
+ if !force {
+ return "", errors.NewError(fmt.Sprintf("username %s is already taken", username), http.StatusBadRequest)
+ }
+ }
+
+ // random generation
+ 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
+}
+
+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})
+}
+
+func (s *realUserService) GetUsernameBlacklist() []string {
+ return []string{"self", "root", "users", "admin", "system"} // TODO: add more
+}
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")
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) {
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), " ", "")
-}