feat: user_rework (todo -> GetAll)

This commit is contained in:
2025-06-11 12:30:22 +01:00
parent 6aa3870a5c
commit 189534031b
24 changed files with 647 additions and 142 deletions
+35 -17
View File
@@ -48,9 +48,10 @@ type config struct {
}
type mailConfig struct {
exp time.Duration
fromEmail string
sendGrid sendGridConfig
inviteExp time.Duration
passwordRefreshExp time.Duration
fromEmail string
sendGrid sendGridConfig
}
type authConfig struct {
@@ -64,10 +65,11 @@ type basicConfig struct {
}
type tokenConfig struct {
secret string
exp time.Duration
iss string
aud string
secret string
exp time.Duration
refreshExp time.Duration
iss string
aud string
}
type redisConfig struct {
@@ -89,6 +91,8 @@ type dbConfig struct {
maxIdleTime time.Duration
}
// This `mount` function is responsible for setting up the routing for your API
// using the Chi router. Here's a breakdown of what it does:
func (api *api) mount() http.Handler {
r := chi.NewRouter()
// *CORS
@@ -135,29 +139,37 @@ func (api *api) mount() http.Handler {
r.Route("/users", func(r chi.Router) {
r.Put("/activate/{token}", api.activateUserHandler)
//r.Put("/reset-password/{token}", api.resetPasswordHandler)
r.Route("/{id}", func(r chi.Router) {
r.Route("/me", func(r chi.Router) {
r.Use(api.AuthTokenMiddleware)
r.Get("/", api.getUserHandler)
r.Patch("/", api.checkProfileOwnership(api.updateUserHandler))
r.Patch("/password", api.checkProfileOwnership(api.updatePasswordHandler))
r.Put(("/follow"), api.followUserHandler)
r.Put(("/unfollow"), api.unfollowUserHandler)
})
r.Group(func(r chi.Router) {
r.Use(api.AuthTokenMiddleware)
r.Get("/feed", api.getUserFeedHandler)
})
})
r.Route("/authentication", func(r chi.Router) {
r.Post("/user", api.registerUserHandler)
r.Post("/register", api.registerUserHandler)
r.Post("/token", api.createTokenHandler)
r.Post("/forgot-password", api.forgotPasswordHandler)
r.Post("/refresh", api.refreshTokenHandler)
})
})
r.Route("/admin", func(r chi.Router) {
r.Use(api.AuthTokenMiddleware)
r.Route("/users", func(r chi.Router) {
r.Get("/", api.protectAdminRoutes("moderator", api.getUsersHandler))
r.Route("/{id}", func(r chi.Router) {
r.Get("/", api.protectAdminRoutes("moderator", api.getUserByIdHandler))
})
})
})
return r
}
@@ -204,6 +216,12 @@ func (api *api) run(mux http.Handler) error {
return err
}
if userStore, ok := api.store.Users.(*store.UserStore); ok {
if err := userStore.Close(); err != nil {
api.logger.Errorw("error closing userstore", err)
}
}
api.logger.Infow("server has been shutdown")
return nil
+88 -1
View File
@@ -27,6 +27,15 @@ type UserWithToken struct {
Token string `json:"token"`
}
type ForgotPasswordPayload struct {
Email string `json:"email" validate:"required,email,max=255"`
}
type AuthResponse struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
}
// RegisterUser godoc
//
// @Summary Register a new User
@@ -75,7 +84,7 @@ func (api *api) registerUserHandler(w http.ResponseWriter, r *http.Request) {
hashedToken := hex.EncodeToString(hash[:])
// Store the user
err := api.store.Users.CreateAndInvite(ctx, user, hashedToken, api.config.mail.exp)
err := api.store.Users.CreateAndInvite(ctx, user, hashedToken, api.config.mail.inviteExp)
if err != nil {
switch {
case errors.Is(err, store.ErrDuplicateEmail):
@@ -201,3 +210,81 @@ func (api *api) createTokenHandler(w http.ResponseWriter, r *http.Request) {
return
}
}
// ResetPasswordHandler godoc
//
// @Summary Forgot Password
// @Description Forgot Password
// @Tags authentication
// @Accept json
// @Produce json
// @Param payload body ForgotPasswordPayload true "User Credentials"
// @Success 201 {string} string "Token Created"
// @Failure 400 {object} error
// @Failure 401 {object} error
// @Failure 500 {object} error
// @Router /authentication/forgot-password [post]
func (api *api) forgotPasswordHandler(w http.ResponseWriter, r *http.Request) {
var payload ForgotPasswordPayload
if err := readJSON(w, r, &payload); err != nil {
api.badRequestError(w, r, err)
return
}
if err := Validate.Struct(payload); err != nil {
api.badRequestError(w, r, err)
return
}
user, err := api.store.Users.GetByEmail(r.Context(), payload.Email)
if err != nil {
switch err {
case store.ErrNotFound:
api.badRequestError(w, r, err)
default:
api.internalServerError(w, r, err)
}
}
plainToken := uuid.New().String()
hash := sha256.Sum256([]byte(plainToken))
hashedToken := hex.EncodeToString(hash[:])
//*Store The Request
err = api.store.Users.PasswordResetRequest(r.Context(), user.ID, hashedToken, api.config.mail.passwordRefreshExp)
if err != nil {
api.internalServerError(w, r, err)
return
}
updateURL := fmt.Sprintf("%s/reset-password/%s", api.config.frontEndURL, plainToken)
isProdEnv := api.config.env == "production"
vars := struct {
Username string
ServiceName string
UpdateURL string
Email string
CurrentYear string
}{
Username: user.Username,
ServiceName: "LK_API_Temp",
UpdateURL: updateURL,
Email: user.Email,
CurrentYear: time.Now().Format("2006"),
}
status, err := api.mailer.Send(mailer.ForgotPasswordTemplate, user.Username, user.Email, vars, !isProdEnv)
if err != nil {
api.internalServerError(w, r, err)
return
}
api.logger.Infow("email sent", "status code", status)
if err := api.jsonResponse(w, http.StatusCreated, plainToken); err != nil {
api.internalServerError(w, r, err)
return
}
}
-2
View File
@@ -1,7 +1,6 @@
package main
import (
"log"
"net/http"
"github.com/FernandoVideira/LK_API_Temp/internal/store"
@@ -48,7 +47,6 @@ func (api *api) getUserFeedHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := getUserFromContext(r)
log.Println(user)
feed, err := api.store.Posts.GetUserFeed(ctx, user.ID, fq)
if err != nil {
api.internalServerError(w, r, err)
+7 -3
View File
@@ -56,8 +56,9 @@ func main() {
},
env: env.GetString("ENV", "development"),
mail: mailConfig{
exp: time.Hour * 24 * 3,
fromEmail: env.GetString("FROM_EMAIL", ""),
inviteExp: time.Hour * 24 * 3,
passwordRefreshExp: time.Hour * 24 * 7,
fromEmail: env.GetString("FROM_EMAIL", ""),
sendGrid: sendGridConfig{
apiKey: env.GetString("SENDGRID_API_KEY", ""),
},
@@ -111,7 +112,10 @@ func main() {
cfg.rateLimiter.TimeFrame,
)
store := store.NewStorage(db)
store, err := store.NewStorage(db)
if err != nil {
logger.Fatal(err)
}
cacheStorage := cache.NewRedisStorage(redisDb)
mailer := mailer.NewSendGrid(cfg.mail.sendGrid.apiKey, cfg.mail.fromEmail)
+26 -1
View File
@@ -5,6 +5,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"strings"
@@ -104,17 +105,20 @@ func (api *api) postsContextMiddleware(next http.Handler) http.Handler {
api.badRequestError(w, r, err)
return
}
ctx := r.Context()
post, err := api.store.Posts.GetByID(ctx, id)
log.Println("post", post)
if err != nil {
switch {
case errors.Is(err, store.ErrNotFound):
api.notFoundError(w, r, err)
return
default:
api.internalServerError(w, r, err)
return
}
return
}
ctx = context.WithValue(ctx, postCtx, post)
@@ -161,6 +165,27 @@ func (api *api) checkProfileOwnership(next http.HandlerFunc) http.HandlerFunc {
})
}
func (api *api) protectAdminRoutes(requiredRole string, next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := getUserFromContext(r)
allowed, err := api.checkRolePrecedence(r.Context(), user, requiredRole)
if err != nil {
api.internalServerError(w, r, err)
return
}
if !allowed {
api.forbiddenError(w, r, fmt.Errorf("forbidden"))
return
}
ctx := r.Context()
ctx = context.WithValue(ctx, roleContextKey, user.Role)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (api *api) checkRolePrecedence(ctx context.Context, user *store.User, roleName string) (bool, error) {
role, err := api.store.Roles.GetByName(ctx, roleName)
if err != nil {
+39 -3
View File
@@ -3,7 +3,6 @@ package main
import (
"context"
"errors"
"log"
"net/http"
"strconv"
@@ -12,8 +11,39 @@ import (
)
type userKey string
type roleKey string
const userContextKey userKey = "user"
const roleContextKey roleKey = "role"
// GetUser godoc
//
// @Summary Fetches all users
// @Description Fetches all users
// @Tags admin
// @Accept json
// @Produce json
// @Success 200 {object} store.User
// @Failure 400 {object} error
// @Failure 404 {object} error
// @Failure 500 {object} error
// @Security ApiKeyAuth
// @Router /admin/users [get]
func (api *api) getUsersHandler(w http.ResponseWriter, r *http.Request) {
role := getRoleFromContext(r)
isAdmin := false
if role.Level == 3 {
isAdmin = true
}
users, err := api.store.Users.GetAll(r.Context(), isAdmin)
if err != nil {
api.internalServerError(w, r, err)
return
}
api.jsonResponse(w, http.StatusOK, users)
}
// GetUser godoc
//
@@ -78,7 +108,6 @@ type updateUserPayload struct {
// @Router /users/{id} [patch]
func (api *api) updateUserHandler(w http.ResponseWriter, r *http.Request) {
user := getUserFromContext(r)
log.Println(user)
var payload updateUserPayload
if err := readJSON(w, r, &payload); err != nil {
@@ -103,7 +132,9 @@ func (api *api) updateUser(ctx context.Context, user *store.User) error {
return err
}
api.cacheStore.Users.Delete(ctx, user.ID)
if api.config.redisCfg.enabled {
api.cacheStore.Users.Delete(ctx, user.ID)
}
return nil
}
@@ -300,3 +331,8 @@ func getUserFromContext(r *http.Request) *store.User {
user, _ := r.Context().Value(userContextKey).(*store.User)
return user
}
func getRoleFromContext(r *http.Request) *store.Role {
role, _ := r.Context().Value(roleContextKey).(*store.Role)
return role
}