feat: recover password & refresh token
This commit is contained in:
File diff suppressed because one or more lines are too long
+8
-10
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
+196
-42
@@ -24,6 +24,46 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"get": {
|
||||
"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}": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
+196
-42
@@ -16,6 +16,46 @@
|
||||
},
|
||||
"basePath": "/v1",
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"get": {
|
||||
"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}": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
+130
-28
@@ -94,6 +94,16 @@ definitions:
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
main.resetPasswordPayload:
|
||||
properties:
|
||||
new_password:
|
||||
type: string
|
||||
new_password_confirmation:
|
||||
type: string
|
||||
required:
|
||||
- new_password
|
||||
- new_password_confirmation
|
||||
type: object
|
||||
main.updateUserPasswordPayload:
|
||||
properties:
|
||||
current_password:
|
||||
@@ -238,6 +248,32 @@ info:
|
||||
termsOfService: http://swagger.io/terms/
|
||||
title: LK API Template
|
||||
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:
|
||||
post:
|
||||
consumes:
|
||||
@@ -269,6 +305,64 @@ paths:
|
||||
summary: Forgot Password
|
||||
tags:
|
||||
- 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:
|
||||
post:
|
||||
consumes:
|
||||
@@ -300,34 +394,6 @@ paths:
|
||||
summary: Create a new token
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
description: Health Check
|
||||
@@ -756,6 +822,42 @@ paths:
|
||||
summary: Fetches the user feed
|
||||
tags:
|
||||
- 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:
|
||||
ApiKeyAuth:
|
||||
in: header
|
||||
|
||||
@@ -3,6 +3,7 @@ module github.com/FernandoVideira/LK_API_Temp
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/go-chi/chi/v5 v5.2.0
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-playground/validator/v10 v10.24.0
|
||||
@@ -20,7 +21,6 @@ require (
|
||||
|
||||
require (
|
||||
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/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
|
||||
@@ -4,5 +4,7 @@ import "github.com/golang-jwt/jwt/v5"
|
||||
|
||||
type Authenticator interface {
|
||||
GenerateToken(jwt.Claims) (string, error)
|
||||
GenerateRefreshToken(jwt.Claims) (string, error)
|
||||
ValidateToken(string) (*jwt.Token, error)
|
||||
ValidateRefreshToken(string) (*jwt.Token, error)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,17 @@ func (j *JWTAuthenticator) GenerateToken(claims jwt.Claims) (string, error) {
|
||||
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) {
|
||||
return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
|
||||
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}),
|
||||
)
|
||||
}
|
||||
|
||||
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}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,20 @@ func (a *TestAuthenticator) GenerateToken(claims jwt.Claims) (string, error) {
|
||||
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) {
|
||||
return jwt.Parse(token, func(token *jwt.Token) (any, error) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -1,6 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtWelcome />
|
||||
</div>
|
||||
</template>
|
||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
// @ts-check
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
|
||||
export default withNuxt(
|
||||
// Your custom configs here
|
||||
)
|
||||
@@ -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'
|
||||
]
|
||||
})
|
||||
@@ -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 +0,0 @@
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
Reference in New Issue
Block a user