feat: recover password & refresh token

This commit is contained in:
2025-07-20 18:06:05 +01:00
parent 57730270a0
commit f85191b918
26 changed files with 748 additions and 332 deletions
+8 -10
View File
@@ -139,7 +139,7 @@ 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.Put("/reset-password/{token}", api.resetPasswordHandler)
r.Route("/me", func(r chi.Router) {
r.Use(api.AuthTokenMiddleware)
@@ -155,19 +155,17 @@ func (api *api) mount() http.Handler {
r.Post("/forgot-password", api.forgotPasswordHandler)
r.Post("/refresh", api.refreshTokenHandler)
})
})
r.Route("/admin", func(r chi.Router) {
r.Use(api.AuthTokenMiddleware)
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))
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
+1 -1
View File
@@ -28,7 +28,7 @@ func TestRateLimiterMiddleware(t *testing.T) {
mockIP := "192.168.1.1"
marginOfError := 2
for i := 0; i < cfg.rateLimiter.RequestsPerTimeFrame+marginOfError; i++ {
for i := range cfg.rateLimiter.RequestsPerTimeFrame + marginOfError {
req, err := http.NewRequest("GET", ts.URL+"/v1/health", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
+102 -14
View File
@@ -5,7 +5,10 @@ import (
"encoding/hex"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/FernandoVideira/LK_API_Temp/internal/mailer"
@@ -47,7 +50,7 @@ type AuthResponse struct {
// @Success 201 {object} UserWithToken "User Registered"
// @Failure 400 {object} error "User payload error"
// @Failure 500 {object} error "Internal Server Error"
// @Router /authentication/user [post]
// @Router /authentication/register [post]
func (api *api) registerUserHandler(w http.ResponseWriter, r *http.Request) {
var payload RegisterUserPayload
if err := readJSON(w, r, &payload); err != nil {
@@ -187,25 +190,76 @@ func (api *api) createTokenHandler(w http.ResponseWriter, r *http.Request) {
return
}
claims := &jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(api.config.auth.token.exp).Unix(),
"iat": time.Now().Unix(),
"nbf": time.Now().Unix(),
"iss": api.config.auth.token.iss,
"aud": api.config.auth.token.aud,
}
//* generate a new token -> add the claims
token, err := api.authenticator.GenerateToken(claims)
//* send the token back to the user
authResponse, err := api.createTokens(user)
if err != nil {
api.internalServerError(w, r, err)
return
}
//* send the token back to the user
if err := api.jsonResponse(w, http.StatusCreated, authResponse); err != nil {
api.internalServerError(w, r, err)
return
}
}
if err := api.jsonResponse(w, http.StatusCreated, token); err != nil {
// RefreshTokenHandler godoc
//
// @Summary Refresh a token
// @Description Refresh a token for the user
// @Tags authentication
// @Accept json
// @Produce json
// @Param Authorization header string true "Refresh Token"
// @Success 201 {string} string "Token Created"
// @Failure 400 {object} error
// @Failure 401 {object} error
// @Failure 500 {object} error
// @Router /authentication/refresh [post]
func (api *api) refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
//* parse the refresh token from the request
refreshHeader := r.Header.Get("Authorization")
if refreshHeader == "" {
api.unauthorizedError(w, r, errors.New("refresh token is required"))
return
}
parts := strings.Split(refreshHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
api.unauthorizedError(w, r, fmt.Errorf("invalid Authorization header"))
return
}
refreshToken := parts[1]
token, err := api.authenticator.ValidateRefreshToken(refreshToken)
if err != nil {
api.unauthorizedError(w, r, err)
return
}
claims := token.Claims.(jwt.MapClaims)
//* get the user id from the token
userID, err := strconv.ParseInt(fmt.Sprintf("%.f", claims["sub"]), 10, 64)
if err != nil {
api.unauthorizedError(w, r, fmt.Errorf("invalid token"))
return
}
//* get the user from the database
user, err := api.store.Users.GetByID(r.Context(), userID)
if err != nil {
api.unauthorizedError(w, r, err)
return
}
authResponse, err := api.createTokens(user)
if err != nil {
api.unauthorizedError(w, r, err)
return
}
if err := api.jsonResponse(w, http.StatusCreated, authResponse); err != nil {
api.internalServerError(w, r, err)
return
}
@@ -288,3 +342,37 @@ func (api *api) forgotPasswordHandler(w http.ResponseWriter, r *http.Request) {
return
}
}
func (api *api) createTokens(user *store.User) (AuthResponse, error) {
claims := &jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(api.config.auth.token.exp).Unix(),
"iat": time.Now().Unix(),
"nbf": time.Now().Unix(),
"iss": api.config.auth.token.iss,
"aud": api.config.auth.token.aud,
}
//* generate a new token -> add the claims
token, err := api.authenticator.GenerateToken(claims)
if err != nil {
return AuthResponse{}, err
}
refreshClaims := &jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(api.config.auth.token.refreshExp).Unix(),
}
log.Println(time.Now().Add(api.config.auth.token.refreshExp).Unix())
refreshToken, err := api.authenticator.GenerateRefreshToken(refreshClaims)
if err != nil {
return AuthResponse{}, err
}
return AuthResponse{
Token: token,
RefreshToken: refreshToken,
}, nil
}
+5 -4
View File
@@ -69,10 +69,11 @@ func main() {
password: env.GetString("BASIC_AUTH_PASSWORD", "admin"),
},
token: tokenConfig{
secret: env.GetString("AUTH_TOKEN_SECRET", "secret"),
exp: env.GetDuration("AUTH_TOKEN_EXP", "24h"),
iss: env.GetString("AUTH_TOKEN_ISS", "lkapi"),
aud: env.GetString("AUTH_TOKEN_AUD", "lkapi"),
secret: env.GetString("AUTH_TOKEN_SECRET", "secret"),
exp: env.GetDuration("AUTH_TOKEN_EXP", "24h"),
refreshExp: time.Hour * 24 * 7,
iss: env.GetString("AUTH_TOKEN_ISS", "lkapi"),
aud: env.GetString("AUTH_TOKEN_AUD", "lkapi"),
},
},
rateLimiter: ratelimiter.Config{
+14 -19
View File
@@ -5,7 +5,6 @@ import (
"encoding/base64"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"strings"
@@ -109,7 +108,6 @@ func (api *api) postsContextMiddleware(next http.Handler) http.Handler {
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):
@@ -126,6 +124,19 @@ func (api *api) postsContextMiddleware(next http.Handler) http.Handler {
})
}
func (api *api) rateLimiterMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if api.config.rateLimiter.Enabled {
if allow, retryAfter := api.rateLimiter.Allow(r.RemoteAddr); !allow {
api.rateLimitExceededError(w, r, retryAfter.String())
return
}
}
next.ServeHTTP(w, r)
})
}
func (api *api) checkPostOwnership(requiredRole string, next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := getUserFromContext(r)
@@ -179,10 +190,7 @@ func (api *api) protectAdminRoutes(requiredRole string, next http.HandlerFunc) h
return
}
ctx := r.Context()
ctx = context.WithValue(ctx, roleContextKey, user.Role)
next.ServeHTTP(w, r.WithContext(ctx))
next.ServeHTTP(w, r)
})
}
@@ -218,16 +226,3 @@ func (api *api) getUser(ctx context.Context, id int64) (*store.User, error) {
return user, nil
}
func (api *api) RateLimiterMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if api.config.rateLimiter.Enabled {
if allow, retryAfter := api.rateLimiter.Allow(r.RemoteAddr); !allow {
api.rateLimitExceededError(w, r, retryAfter.String())
return
}
}
next.ServeHTTP(w, r)
})
}
+56 -10
View File
@@ -30,13 +30,13 @@ const roleContextKey roleKey = "role"
// @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
user := getUserFromContext(r)
if user == nil {
api.unauthorizedError(w, r, errors.New("user not found"))
return
}
users, err := api.store.Users.GetAll(r.Context(), isAdmin)
users, err := api.store.Users.GetAll(r.Context(), user.ID, user.Role.Level)
if err != nil {
api.internalServerError(w, r, err)
return
@@ -84,6 +84,57 @@ func (api *api) getUserHandler(w http.ResponseWriter, r *http.Request) {
}
}
type resetPasswordPayload struct {
NewPassword string `json:"new_password" validate:"required,password"`
NewPasswordConfirmation string `json:"new_password_confirmation" validate:"required,eqfield=NewPassword"`
}
// ResetPassword godoc
//
// @Summary Reset user password
// @Description Reset user password with verification
// @Tags users
// @Accept json
// @Param payload body resetPasswordPayload true "Reset Password"
// @Param token path string true "Reset Password Token"
// @Success 204 {string} string "Password reset successfully"
// @Failure 400 {object} error "Invalid request"
// @Failure 401 {object} error "Unauthorized"
// @Failure 500 {object} error "Internal server error"
// @Security ApiKeyAuth
// @Router /users/reset-password/{token} [put]
func (api *api) resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
token := chi.URLParam(r, "token")
var payload resetPasswordPayload
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
}
err := api.store.Users.ResetPassword(r.Context(), token, payload.NewPassword)
if err != nil {
switch err {
case store.ErrNotFound:
api.notFoundError(w, r, err)
return
default:
api.internalServerError(w, r, err)
return
}
}
w.WriteHeader(http.StatusNoContent)
if err := api.jsonResponse(w, http.StatusNoContent, ""); err != nil {
api.internalServerError(w, r, err)
}
}
type updateUserPayload struct {
FirstName *string `json:"first_name" validate:"omitempty,max=100"`
LastName *string `json:"last_name" validate:"omitempty,max=100"`
@@ -331,8 +382,3 @@ 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
}