feat: recover password & refresh token

This commit is contained in:
FernandoJVideira committed 2025-07-20 18:06:05 +01:00
1 parent 57730270a0
commit f85191b918
26 files changed
+739 -323

No files matched your search

+1 -1
View File
@@ -1 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit stLine truncated exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit stLine truncated
BIN
View File
Binary file not shown.
+3 -5
View File
@@ -139,7 +139,7 @@ func (api *api) mount() http.Handler {
r.Route("/users", func(r chi.Router) { r.Route("/users", func(r chi.Router) {
r.Put("/activate/{token}", api.activateUserHandler) 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.Route("/me", func(r chi.Router) {
r.Use(api.AuthTokenMiddleware) r.Use(api.AuthTokenMiddleware)
@@ -155,19 +155,17 @@ func (api *api) mount() http.Handler {
r.Post("/forgot-password", api.forgotPasswordHandler) r.Post("/forgot-password", api.forgotPasswordHandler)
r.Post("/refresh", api.refreshTokenHandler) r.Post("/refresh", api.refreshTokenHandler)
}) })
})
r.Route("/admin", func(r chi.Router) { r.Route("/admin", func(r chi.Router) {
r.Use(api.AuthTokenMiddleware) r.Use(api.AuthTokenMiddleware)
r.Route("/users", func(r chi.Router) { r.Route("/users", func(r chi.Router) {
r.Get("/", api.protectAdminRoutes("moderator", api.getUsersHandler)) r.Get("/", api.protectAdminRoutes("moderator", api.getUsersHandler))
r.Route("/{id}", func(r chi.Router) { r.Route("/{id}", func(r chi.Router) {
r.Get("/", api.protectAdminRoutes("moderator", api.getUserByIdHandler)) //r.Get("/", api.protectAdminRoutes("moderator", api.getUserByIdHandler))
})
}) })
}) })
}) })
return r return r
+1 -1
View File
@@ -28,7 +28,7 @@ func TestRateLimiterMiddleware(t *testing.T) {
mockIP := "192.168.1.1" mockIP := "192.168.1.1"
marginOfError := 2 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) req, err := http.NewRequest("GET", ts.URL+"/v1/health", nil)
if err != nil { if err != nil {
t.Fatalf("failed to create request: %v", err) t.Fatalf("failed to create request: %v", err)
+102 -14
View File
@@ -5,7 +5,10 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"log"
"net/http" "net/http"
"strconv"
"strings"
"time" "time"
"github.com/FernandoVideira/LK_API_Temp/internal/mailer" "github.com/FernandoVideira/LK_API_Temp/internal/mailer"
@@ -47,7 +50,7 @@ type AuthResponse struct {
// @Success 201 {object} UserWithToken "User Registered" // @Success 201 {object} UserWithToken "User Registered"
// @Failure 400 {object} error "User payload error" // @Failure 400 {object} error "User payload error"
// @Failure 500 {object} error "Internal Server 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) { func (api *api) registerUserHandler(w http.ResponseWriter, r *http.Request) {
var payload RegisterUserPayload var payload RegisterUserPayload
if err := readJSON(w, r, &payload); err != nil { if err := readJSON(w, r, &payload); err != nil {
@@ -187,25 +190,76 @@ func (api *api) createTokenHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
claims := &jwt.MapClaims{ //* send the token back to the user
"sub": user.ID, authResponse, err := api.createTokens(user)
"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 { if err != nil {
api.internalServerError(w, r, err) api.internalServerError(w, r, err)
return 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) api.internalServerError(w, r, err)
return return
} }
@@ -288,3 +342,37 @@ func (api *api) forgotPasswordHandler(w http.ResponseWriter, r *http.Request) {
return 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
}
+1
View File
@@ -71,6 +71,7 @@ func main() {
token: tokenConfig{ token: tokenConfig{
secret: env.GetString("AUTH_TOKEN_SECRET", "secret"), secret: env.GetString("AUTH_TOKEN_SECRET", "secret"),
exp: env.GetDuration("AUTH_TOKEN_EXP", "24h"), exp: env.GetDuration("AUTH_TOKEN_EXP", "24h"),
refreshExp: time.Hour * 24 * 7,
iss: env.GetString("AUTH_TOKEN_ISS", "lkapi"), iss: env.GetString("AUTH_TOKEN_ISS", "lkapi"),
aud: env.GetString("AUTH_TOKEN_AUD", "lkapi"), aud: env.GetString("AUTH_TOKEN_AUD", "lkapi"),
}, },
+14 -19
View File
@@ -5,7 +5,6 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"log"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@@ -109,7 +108,6 @@ func (api *api) postsContextMiddleware(next http.Handler) http.Handler {
ctx := r.Context() ctx := r.Context()
post, err := api.store.Posts.GetByID(ctx, id) post, err := api.store.Posts.GetByID(ctx, id)
log.Println("post", post)
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, store.ErrNotFound): 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 { func (api *api) checkPostOwnership(requiredRole string, next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := getUserFromContext(r) user := getUserFromContext(r)
@@ -179,10 +190,7 @@ func (api *api) protectAdminRoutes(requiredRole string, next http.HandlerFunc) h
return return
} }
ctx := r.Context() next.ServeHTTP(w, r)
ctx = context.WithValue(ctx, roleContextKey, user.Role)
next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
@@ -218,16 +226,3 @@ func (api *api) getUser(ctx context.Context, id int64) (*store.User, error) {
return user, nil 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 // @Security ApiKeyAuth
// @Router /admin/users [get] // @Router /admin/users [get]
func (api *api) getUsersHandler(w http.ResponseWriter, r *http.Request) { func (api *api) getUsersHandler(w http.ResponseWriter, r *http.Request) {
role := getRoleFromContext(r) user := getUserFromContext(r)
isAdmin := false if user == nil {
if role.Level == 3 { api.unauthorizedError(w, r, errors.New("user not found"))
isAdmin = true 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 { if err != nil {
api.internalServerError(w, r, err) api.internalServerError(w, r, err)
return 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 { type updateUserPayload struct {
FirstName *string `json:"first_name" validate:"omitempty,max=100"` FirstName *string `json:"first_name" validate:"omitempty,max=100"`
LastName *string `json:"last_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) user, _ := r.Context().Value(userContextKey).(*store.User)
return user return user
} }
func getRoleFromContext(r *http.Request) *store.Role {
role, _ := r.Context().Value(roleContextKey).(*store.Role)
return role
}
+196 -42
View File
@@ -24,6 +24,46 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/admin/users": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Fetches all users",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Fetches all users",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/store.User"
}
},
"400": {
"description": "Bad Request",
"schema": {}
},
"404": {
"description": "Not Found",
"schema": {}
},
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/authentication/forgot-password": { "/authentication/forgot-password": {
"post": { "post": {
"description": "Forgot Password", "description": "Forgot Password",
@@ -70,6 +110,92 @@ const docTemplate = `{
} }
} }
}, },
"/authentication/refresh": {
"post": {
"description": "Refresh a token for the user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"authentication"
],
"summary": "Refresh a token",
"parameters": [
{
"type": "string",
"description": "Refresh Token",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"201": {
"description": "Token Created",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {}
},
"401": {
"description": "Unauthorized",
"schema": {}
},
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/authentication/register": {
"post": {
"description": "Register a new User",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"authentication"
],
"summary": "Register a new User",
"parameters": [
{
"description": "User Credentials",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.RegisterUserPayload"
}
}
],
"responses": {
"201": {
"description": "User Registered",
"schema": {
"$ref": "#/definitions/main.UserWithToken"
}
},
"400": {
"description": "User payload error",
"schema": {}
},
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/authentication/token": { "/authentication/token": {
"post": { "post": {
"description": "Create a new token for the user", "description": "Create a new token for the user",
@@ -116,48 +242,6 @@ const docTemplate = `{
} }
} }
}, },
"/authentication/user": {
"post": {
"description": "Register a new User",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"authentication"
],
"summary": "Register a new User",
"parameters": [
{
"description": "User Credentials",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.RegisterUserPayload"
}
}
],
"responses": {
"201": {
"description": "User Registered",
"schema": {
"$ref": "#/definitions/main.UserWithToken"
}
},
"400": {
"description": "User payload error",
"schema": {}
},
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/health": { "/health": {
"get": { "get": {
"description": "Health Check", "description": "Health Check",
@@ -555,6 +639,61 @@ const docTemplate = `{
} }
} }
}, },
"/users/reset-password/{token}": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Reset user password with verification",
"consumes": [
"application/json"
],
"tags": [
"users"
],
"summary": "Reset user password",
"parameters": [
{
"description": "Reset Password",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.resetPasswordPayload"
}
},
{
"type": "string",
"description": "Reset Password Token",
"name": "token",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "Password reset successfully",
"schema": {
"type": "string"
}
},
"400": {
"description": "Invalid request",
"schema": {}
},
"401": {
"description": "Unauthorized",
"schema": {}
},
"500": {
"description": "Internal server error",
"schema": {}
}
}
}
},
"/users/{id}": { "/users/{id}": {
"get": { "get": {
"security": [ "security": [
@@ -935,6 +1074,21 @@ const docTemplate = `{
} }
} }
}, },
"main.resetPasswordPayload": {
"type": "object",
"required": [
"new_password",
"new_password_confirmation"
],
"properties": {
"new_password": {
"type": "string"
},
"new_password_confirmation": {
"type": "string"
}
}
},
"main.updateUserPasswordPayload": { "main.updateUserPasswordPayload": {
"type": "object", "type": "object",
"required": [ "required": [
+196 -42
View File
@@ -16,6 +16,46 @@
}, },
"basePath": "/v1", "basePath": "/v1",
"paths": { "paths": {
"/admin/users": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Fetches all users",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Fetches all users",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/store.User"
}
},
"400": {
"description": "Bad Request",
"schema": {}
},
"404": {
"description": "Not Found",
"schema": {}
},
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/authentication/forgot-password": { "/authentication/forgot-password": {
"post": { "post": {
"description": "Forgot Password", "description": "Forgot Password",
@@ -62,6 +102,92 @@
} }
} }
}, },
"/authentication/refresh": {
"post": {
"description": "Refresh a token for the user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"authentication"
],
"summary": "Refresh a token",
"parameters": [
{
"type": "string",
"description": "Refresh Token",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"201": {
"description": "Token Created",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {}
},
"401": {
"description": "Unauthorized",
"schema": {}
},
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/authentication/register": {
"post": {
"description": "Register a new User",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"authentication"
],
"summary": "Register a new User",
"parameters": [
{
"description": "User Credentials",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.RegisterUserPayload"
}
}
],
"responses": {
"201": {
"description": "User Registered",
"schema": {
"$ref": "#/definitions/main.UserWithToken"
}
},
"400": {
"description": "User payload error",
"schema": {}
},
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/authentication/token": { "/authentication/token": {
"post": { "post": {
"description": "Create a new token for the user", "description": "Create a new token for the user",
@@ -108,48 +234,6 @@
} }
} }
}, },
"/authentication/user": {
"post": {
"description": "Register a new User",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"authentication"
],
"summary": "Register a new User",
"parameters": [
{
"description": "User Credentials",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.RegisterUserPayload"
}
}
],
"responses": {
"201": {
"description": "User Registered",
"schema": {
"$ref": "#/definitions/main.UserWithToken"
}
},
"400": {
"description": "User payload error",
"schema": {}
},
"500": {
"description": "Internal Server Error",
"schema": {}
}
}
}
},
"/health": { "/health": {
"get": { "get": {
"description": "Health Check", "description": "Health Check",
@@ -547,6 +631,61 @@
} }
} }
}, },
"/users/reset-password/{token}": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Reset user password with verification",
"consumes": [
"application/json"
],
"tags": [
"users"
],
"summary": "Reset user password",
"parameters": [
{
"description": "Reset Password",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.resetPasswordPayload"
}
},
{
"type": "string",
"description": "Reset Password Token",
"name": "token",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "Password reset successfully",
"schema": {
"type": "string"
}
},
"400": {
"description": "Invalid request",
"schema": {}
},
"401": {
"description": "Unauthorized",
"schema": {}
},
"500": {
"description": "Internal server error",
"schema": {}
}
}
}
},
"/users/{id}": { "/users/{id}": {
"get": { "get": {
"security": [ "security": [
@@ -927,6 +1066,21 @@
} }
} }
}, },
"main.resetPasswordPayload": {
"type": "object",
"required": [
"new_password",
"new_password_confirmation"
],
"properties": {
"new_password": {
"type": "string"
},
"new_password_confirmation": {
"type": "string"
}
}
},
"main.updateUserPasswordPayload": { "main.updateUserPasswordPayload": {
"type": "object", "type": "object",
"required": [ "required": [
+130 -28
View File
@@ -94,6 +94,16 @@ definitions:
username: username:
type: string type: string
type: object type: object
main.resetPasswordPayload:
properties:
new_password:
type: string
new_password_confirmation:
type: string
required:
- new_password
- new_password_confirmation
type: object
main.updateUserPasswordPayload: main.updateUserPasswordPayload:
properties: properties:
current_password: current_password:
@@ -238,6 +248,32 @@ info:
termsOfService: http://swagger.io/terms/ termsOfService: http://swagger.io/terms/
title: LK API Template title: LK API Template
paths: paths:
/admin/users:
get:
consumes:
- application/json
description: Fetches all users
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/store.User'
"400":
description: Bad Request
schema: {}
"404":
description: Not Found
schema: {}
"500":
description: Internal Server Error
schema: {}
security:
- ApiKeyAuth: []
summary: Fetches all users
tags:
- admin
/authentication/forgot-password: /authentication/forgot-password:
post: post:
consumes: consumes:
@@ -269,6 +305,64 @@ paths:
summary: Forgot Password summary: Forgot Password
tags: tags:
- authentication - authentication
/authentication/refresh:
post:
consumes:
- application/json
description: Refresh a token for the user
parameters:
- description: Refresh Token
in: header
name: Authorization
required: true
type: string
produces:
- application/json
responses:
"201":
description: Token Created
schema:
type: string
"400":
description: Bad Request
schema: {}
"401":
description: Unauthorized
schema: {}
"500":
description: Internal Server Error
schema: {}
summary: Refresh a token
tags:
- authentication
/authentication/register:
post:
consumes:
- application/json
description: Register a new User
parameters:
- description: User Credentials
in: body
name: payload
required: true
schema:
$ref: '#/definitions/main.RegisterUserPayload'
produces:
- application/json
responses:
"201":
description: User Registered
schema:
$ref: '#/definitions/main.UserWithToken'
"400":
description: User payload error
schema: {}
"500":
description: Internal Server Error
schema: {}
summary: Register a new User
tags:
- authentication
/authentication/token: /authentication/token:
post: post:
consumes: consumes:
@@ -300,34 +394,6 @@ paths:
summary: Create a new token summary: Create a new token
tags: tags:
- authentication - authentication
/authentication/user:
post:
consumes:
- application/json
description: Register a new User
parameters:
- description: User Credentials
in: body
name: payload
required: true
schema:
$ref: '#/definitions/main.RegisterUserPayload'
produces:
- application/json
responses:
"201":
description: User Registered
schema:
$ref: '#/definitions/main.UserWithToken'
"400":
description: User payload error
schema: {}
"500":
description: Internal Server Error
schema: {}
summary: Register a new User
tags:
- authentication
/health: /health:
get: get:
description: Health Check description: Health Check
@@ -756,6 +822,42 @@ paths:
summary: Fetches the user feed summary: Fetches the user feed
tags: tags:
- feed - feed
/users/reset-password/{token}:
put:
consumes:
- application/json
description: Reset user password with verification
parameters:
- description: Reset Password
in: body
name: payload
required: true
schema:
$ref: '#/definitions/main.resetPasswordPayload'
- description: Reset Password Token
in: path
name: token
required: true
type: string
responses:
"204":
description: Password reset successfully
schema:
type: string
"400":
description: Invalid request
schema: {}
"401":
description: Unauthorized
schema: {}
"500":
description: Internal server error
schema: {}
security:
- ApiKeyAuth: []
summary: Reset user password
tags:
- users
securityDefinitions: securityDefinitions:
ApiKeyAuth: ApiKeyAuth:
in: header in: header
+1 -1
View File
@@ -3,6 +3,7 @@ module github.com/FernandoVideira/LK_API_Temp
go 1.23.4 go 1.23.4
require ( require (
github.com/Masterminds/squirrel v1.5.4
github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/chi/v5 v5.2.0
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-playground/validator/v10 v10.24.0 github.com/go-playground/validator/v10 v10.24.0
@@ -20,7 +21,6 @@ require (
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+2
View File
@@ -4,5 +4,7 @@ import "github.com/golang-jwt/jwt/v5"
type Authenticator interface { type Authenticator interface {
GenerateToken(jwt.Claims) (string, error) GenerateToken(jwt.Claims) (string, error)
GenerateRefreshToken(jwt.Claims) (string, error)
ValidateToken(string) (*jwt.Token, error) ValidateToken(string) (*jwt.Token, error)
ValidateRefreshToken(string) (*jwt.Token, error)
} }
+24
View File
@@ -31,6 +31,17 @@ func (j *JWTAuthenticator) GenerateToken(claims jwt.Claims) (string, error) {
return tokenString, nil return tokenString, nil
} }
func (j *JWTAuthenticator) GenerateRefreshToken(claims jwt.Claims) (string, error) {
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
refreshTokenString, err := refreshToken.SignedString([]byte(j.secret))
if err != nil {
return "", err
}
return refreshTokenString, nil
}
func (j *JWTAuthenticator) ValidateToken(tokenString string) (*jwt.Token, error) { func (j *JWTAuthenticator) ValidateToken(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) { return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
@@ -45,3 +56,16 @@ func (j *JWTAuthenticator) ValidateToken(tokenString string) (*jwt.Token, error)
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}), jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}),
) )
} }
func (j *JWTAuthenticator) ValidateRefreshToken(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(j.secret), nil
},
jwt.WithExpirationRequired(),
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}),
)
}
+12
View File
@@ -24,8 +24,20 @@ func (a *TestAuthenticator) GenerateToken(claims jwt.Claims) (string, error) {
return token.SignedString([]byte(testToken)) return token.SignedString([]byte(testToken))
} }
func (a *TestAuthenticator) GenerateRefreshToken(claims jwt.Claims) (string, error) {
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, testClaims)
return refreshToken.SignedString([]byte(testToken))
}
func (a *TestAuthenticator) ValidateToken(token string) (*jwt.Token, error) { func (a *TestAuthenticator) ValidateToken(token string) (*jwt.Token, error) {
return jwt.Parse(token, func(token *jwt.Token) (any, error) { return jwt.Parse(token, func(token *jwt.Token) (any, error) {
return []byte(testToken), nil return []byte(testToken), nil
}) })
} }
func (a *TestAuthenticator) ValidateRefreshToken(token string) (*jwt.Token, error) {
return jwt.Parse(token, func(token *jwt.Token) (any, error) {
return []byte(testToken), nil
})
}
-24
View File
@@ -1,24 +0,0 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
-75
View File
@@ -1,75 +0,0 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
-6
View File
@@ -1,6 +0,0 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
</template>
BIN
View File
Binary file not shown.
-6
View File
@@ -1,6 +0,0 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
)
-15
View File
@@ -1,15 +0,0 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
modules: [
'@nuxt/content',
'@nuxt/eslint',
'@nuxt/fonts',
'@nuxt/icon',
'@nuxt/image',
'@nuxt/scripts',
'@nuxt/test-utils'
]
})
-26
View File
@@ -1,26 +0,0 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/content": "3.4.0",
"@nuxt/eslint": "1.3.0",
"@nuxt/fonts": "0.11.0",
"@nuxt/icon": "1.11.0",
"@nuxt/image": "1.10.0",
"@nuxt/scripts": "0.11.5",
"@nuxt/test-utils": "3.17.2",
"@unhead/vue": "^2.0.0-rc.8",
"eslint": "^9.0.0",
"nuxt": "^3.16.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

-1
View File
@@ -1 +0,0 @@
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}
-4
View File
@@ -1,4 +0,0 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}