Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a8d52616f
|
||
|
|
da42646bbf
|
||
|
|
f85191b918
|
||
|
|
57730270a0
|
||
|
|
189534031b
|
No files matched your search
@@ -1,35 +0,0 @@
|
||||
# Server Configuration
|
||||
export ADDR=":8080"
|
||||
export EXTERNAL_URL="localhost:8080"
|
||||
export FRONTEND_URL="http://localhost:5173"
|
||||
export ENV="development"
|
||||
export ALLOWED_ORIGINS="https://*,http://*" # TODO: Change to the actual allowed origins on production
|
||||
|
||||
# Database Configuration
|
||||
export DB_ADDR="postgres://postgres:postgres@localhost:5432/lk_tmpl?sslmode=disable"
|
||||
export DB_MAX_OPEN_CONNS="30"
|
||||
export DB_MAX_IDLE_CONNS="30"
|
||||
export DB_MAX_IDLE_TIME="15m"
|
||||
|
||||
# Redis Configuration
|
||||
export REDIS_ADDR="localhost:6379"
|
||||
export REDIS_PASSWORD=""
|
||||
export REDIS_DB="0"
|
||||
export REDIS_ENABLED="false"
|
||||
|
||||
# Authentication
|
||||
export BASIC_AUTH_USERNAME="admin"
|
||||
export BASIC_AUTH_PASSWORD="admin"
|
||||
export AUTH_TOKEN_SECRET="your-secret-key-here"
|
||||
export AUTH_TOKEN_EXP="24h"
|
||||
export AUTH_TOKEN_ISS="lkapi"
|
||||
export AUTH_TOKEN_AUD="lkapi"
|
||||
|
||||
# Email Configuration
|
||||
export FROM_EMAIL="[email protected]"
|
||||
export SENDGRID_API_KEY="your-sendgrid-api-key"
|
||||
|
||||
# Rate Limiter
|
||||
export RATE_LIMITER_REQS_COUNT="20"
|
||||
export RATE_LIMITER_TIME_FRAME="1m"
|
||||
export RATE_LIMITER_ENABLED="true"
|
||||
@@ -1 +1,2 @@
|
||||
.envrc
|
||||
/bin/
|
||||
@@ -27,7 +27,7 @@ migrate-down:
|
||||
|
||||
.PHONY: seed
|
||||
seed:
|
||||
@go run cmd/migrate/seed/main.go
|
||||
@DB_ADDR=$(DB_ADDR) go run cmd/migrate/seed/main.go
|
||||
|
||||
.PHONY: gen-docs
|
||||
gen-docs:
|
||||
|
||||
@@ -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
|
||||
+27
-11
@@ -48,7 +48,8 @@ type config struct {
|
||||
}
|
||||
|
||||
type mailConfig struct {
|
||||
exp time.Duration
|
||||
inviteExp time.Duration
|
||||
passwordRefreshExp time.Duration
|
||||
fromEmail string
|
||||
sendGrid sendGridConfig
|
||||
}
|
||||
@@ -66,6 +67,7 @@ type basicConfig struct {
|
||||
type tokenConfig struct {
|
||||
secret string
|
||||
exp time.Duration
|
||||
refreshExp time.Duration
|
||||
iss string
|
||||
aud string
|
||||
}
|
||||
@@ -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
|
||||
@@ -106,7 +110,7 @@ func (api *api) mount() http.Handler {
|
||||
AllowCredentials: false,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
r.Use(api.RateLimiterMiddleware)
|
||||
r.Use(api.rateLimiterMiddleware)
|
||||
|
||||
r.Use(middleware.Timeout(60 * time.Second))
|
||||
|
||||
@@ -135,26 +139,32 @@ 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))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -204,6 +214,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
|
||||
|
||||
+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)
|
||||
|
||||
+184
-9
@@ -5,7 +5,10 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/FernandoVideira/LK_API_Temp/internal/mailer"
|
||||
@@ -27,6 +30,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
|
||||
@@ -38,7 +50,7 @@ type UserWithToken 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 {
|
||||
@@ -75,7 +87,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):
|
||||
@@ -178,6 +190,160 @@ func (api *api) createTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//* send the token back to the user
|
||||
authResponse, err := api.createTokens(user)
|
||||
if err != nil {
|
||||
api.internalServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := api.jsonResponse(w, http.StatusCreated, authResponse); err != nil {
|
||||
api.internalServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
@@ -190,14 +356,23 @@ func (api *api) createTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
//* generate a new token -> add the claims
|
||||
token, err := api.authenticator.GenerateToken(claims)
|
||||
if err != nil {
|
||||
api.internalServerError(w, r, err)
|
||||
return
|
||||
return AuthResponse{}, err
|
||||
}
|
||||
|
||||
//* send the token back to the user
|
||||
|
||||
if err := api.jsonResponse(w, http.StatusCreated, token); err != nil {
|
||||
api.internalServerError(w, r, err)
|
||||
return
|
||||
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,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
-2
@@ -56,7 +56,8 @@ func main() {
|
||||
},
|
||||
env: env.GetString("ENV", "development"),
|
||||
mail: mailConfig{
|
||||
exp: time.Hour * 24 * 3,
|
||||
inviteExp: time.Hour * 24 * 3,
|
||||
passwordRefreshExp: time.Hour * 24 * 7,
|
||||
fromEmail: env.GetString("FROM_EMAIL", ""),
|
||||
sendGrid: sendGridConfig{
|
||||
apiKey: env.GetString("SENDGRID_API_KEY", ""),
|
||||
@@ -70,6 +71,7 @@ func main() {
|
||||
token: tokenConfig{
|
||||
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"),
|
||||
},
|
||||
@@ -111,7 +113,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)
|
||||
|
||||
|
||||
+34
-14
@@ -104,6 +104,7 @@ 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)
|
||||
@@ -111,17 +112,31 @@ func (api *api) postsContextMiddleware(next http.Handler) http.Handler {
|
||||
switch {
|
||||
case errors.Is(err, store.ErrNotFound):
|
||||
api.notFoundError(w, r, err)
|
||||
return
|
||||
default:
|
||||
api.internalServerError(w, r, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, postCtx, post)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -161,6 +176,24 @@ 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
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -193,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)
|
||||
})
|
||||
}
|
||||
+84
-2
@@ -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) {
|
||||
user := getUserFromContext(r)
|
||||
if user == nil {
|
||||
api.unauthorizedError(w, r, errors.New("user not found"))
|
||||
return
|
||||
}
|
||||
|
||||
users, err := api.store.Users.GetAll(r.Context(), user.ID, user.Role.Level)
|
||||
if err != nil {
|
||||
api.internalServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.jsonResponse(w, http.StatusOK, users)
|
||||
}
|
||||
|
||||
// GetUser godoc
|
||||
//
|
||||
@@ -54,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"`
|
||||
@@ -78,7 +159,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 +183,9 @@ func (api *api) updateUser(ctx context.Context, user *store.User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if api.config.redisCfg.enabled {
|
||||
api.cacheStore.Users.Delete(ctx, user.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS password_reset_requests;
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS password_reset_requests (
|
||||
token bytea PRIMARY KEY,
|
||||
user_id bigint NOT NULL,
|
||||
expiry TIMESTAMP(0)
|
||||
WITH
|
||||
TIME ZONE NOT NULL
|
||||
);
|
||||
@@ -16,6 +16,10 @@ func main() {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
store := store.NewStorage(conn)
|
||||
store, err := store.NewStorage(conn)
|
||||
if err != nil {
|
||||
log.Fatalf("could not create storage: %v", err)
|
||||
}
|
||||
|
||||
db.Seed(store, conn)
|
||||
}
|
||||
+254
-42
@@ -24,6 +24,178 @@ 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",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"authentication"
|
||||
],
|
||||
"summary": "Forgot Password",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User Credentials",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.ForgotPasswordPayload"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Token Created",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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",
|
||||
@@ -70,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",
|
||||
@@ -509,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": [
|
||||
@@ -795,6 +980,18 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.ForgotPasswordPayload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.RegisterUserPayload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -877,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": [
|
||||
|
||||
+254
-42
@@ -16,6 +16,178 @@
|
||||
},
|
||||
"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",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"authentication"
|
||||
],
|
||||
"summary": "Forgot Password",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User Credentials",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.ForgotPasswordPayload"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Token Created",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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",
|
||||
@@ -62,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",
|
||||
@@ -501,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": [
|
||||
@@ -787,6 +972,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.ForgotPasswordPayload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.RegisterUserPayload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -869,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": [
|
||||
|
||||
+169
-28
@@ -29,6 +29,14 @@ definitions:
|
||||
- email
|
||||
- password
|
||||
type: object
|
||||
main.ForgotPasswordPayload:
|
||||
properties:
|
||||
email:
|
||||
maxLength: 255
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
type: object
|
||||
main.RegisterUserPayload:
|
||||
properties:
|
||||
email:
|
||||
@@ -86,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:
|
||||
@@ -230,6 +248,121 @@ 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:
|
||||
- application/json
|
||||
description: Forgot Password
|
||||
parameters:
|
||||
- description: User Credentials
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/main.ForgotPasswordPayload'
|
||||
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: 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:
|
||||
@@ -261,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
|
||||
@@ -717,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
|
||||
@@ -31,6 +32,8 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -42,6 +44,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
@@ -64,6 +70,7 @@ github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBU
|
||||
github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,7 @@ const (
|
||||
FromName = "LK API"
|
||||
maxRetries = 3
|
||||
UserWelcomeTemplate = "user_activation.tmpl"
|
||||
ForgotPasswordTemplate = "forgot_password.tmpl"
|
||||
)
|
||||
|
||||
//go:embed "templates"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{{define "subject"}} Reset Your Password - {{.ServiceName}} {{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello {{.Username}},</p>
|
||||
|
||||
<p>We received a request to reset your password. If you didn't make this request, you can ignore this email.</p>
|
||||
|
||||
<p>To reset your password, click on the link below:</p>
|
||||
|
||||
<p><a href="{{.UpdateURL}}">{{.UpdateURL}}</a></p>
|
||||
|
||||
<p>If you want to reset your password manually, copy and paste the link above into your browser.</p>
|
||||
|
||||
<p>If you have any questions or need assistance, please contact our support team.</p>
|
||||
|
||||
<p>Thanks,</p>
|
||||
<p>The {{.ServiceName}} Team</p>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -1,7 +1,6 @@
|
||||
package ratelimiter
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -26,8 +25,6 @@ func (rl *FixedWindowLimiter) Allow(ip string) (bool, time.Duration) {
|
||||
count, exists := rl.clients[ip]
|
||||
rl.RUnlock()
|
||||
|
||||
log.Println(count, exists)
|
||||
|
||||
if !exists || count < rl.limit {
|
||||
rl.Lock()
|
||||
if !exists {
|
||||
|
||||
@@ -28,6 +28,11 @@ func (m *MockUserStore) Activate(ctx context.Context, token string) error {
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserStore) GetAll(ctx context.Context, userID int64, roleLevel int64) ([]*User, error) {
|
||||
args := m.Called(userID, roleLevel)
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserStore) GetByID(ctx context.Context, id int64) (*User, error) {
|
||||
args := m.Called(id)
|
||||
return nil, args.Error(1)
|
||||
@@ -48,6 +53,16 @@ func (m *MockUserStore) CreateAndInvite(ctx context.Context, user *User, token s
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserStore) PasswordResetRequest(ctx context.Context, userID int64, token string, expiration time.Duration) error {
|
||||
args := m.Called(ctx, userID, token, expiration)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserStore) ResetPassword(ctx context.Context, token string, password string) error {
|
||||
args := m.Called(ctx, token, password)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserStore) Delete(ctx context.Context, id int64) error {
|
||||
args := m.Called(id)
|
||||
return args.Error(0)
|
||||
|
||||
+16
-4
@@ -14,21 +14,33 @@ type Role struct {
|
||||
|
||||
type RoleStore struct {
|
||||
db *sql.DB
|
||||
|
||||
getByNameStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func (s *RoleStore) GetByName(ctx context.Context, name string) (*Role, error) {
|
||||
query := `
|
||||
func NewRoleStore(db *sql.DB) (*RoleStore, error) {
|
||||
getByNameStmt, err := db.PrepareContext(context.Background(), `
|
||||
SELECT id, name, level, description
|
||||
FROM roles
|
||||
WHERE name = $1
|
||||
`
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RoleStore{
|
||||
db: db,
|
||||
getByNameStmt: getByNameStmt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RoleStore) GetByName(ctx context.Context, name string) (*Role, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
role := &Role{}
|
||||
|
||||
err := s.db.QueryRowContext(ctx, query, name).Scan(
|
||||
err := s.getByNameStmt.QueryRowContext(ctx, name).Scan(
|
||||
&role.ID,
|
||||
&role.Name,
|
||||
&role.Level,
|
||||
|
||||
@@ -27,10 +27,13 @@ type Storage struct {
|
||||
Users interface {
|
||||
Create(context.Context, *sql.Tx, *User) error
|
||||
Activate(context.Context, string) error
|
||||
GetAll(context.Context, int64, int64) ([]*User, error)
|
||||
GetByID(context.Context, int64) (*User, error)
|
||||
GetByEmail(context.Context, string) (*User, error)
|
||||
Update(context.Context, *User) error
|
||||
CreateAndInvite(context.Context, *User, string, time.Duration) error
|
||||
PasswordResetRequest(context.Context, int64, string, time.Duration) error
|
||||
ResetPassword(context.Context, string, string) error
|
||||
Delete(context.Context, int64) error
|
||||
DeleteUser(context.Context, int64) error
|
||||
}
|
||||
@@ -47,14 +50,20 @@ type Storage struct {
|
||||
}
|
||||
}
|
||||
|
||||
func NewStorage(db *sql.DB) Storage {
|
||||
func NewStorage(db *sql.DB) (Storage, error) {
|
||||
|
||||
userStore, roleStore, err := initStores(db)
|
||||
if err != nil {
|
||||
return Storage{}, err
|
||||
}
|
||||
|
||||
return Storage{
|
||||
Posts: &PostStore{db},
|
||||
Users: &UserStore{db},
|
||||
Users: userStore,
|
||||
Comments: &CommentStore{db},
|
||||
Followers: &FollowerStore{db},
|
||||
Roles: &RoleStore{db},
|
||||
}
|
||||
Roles: roleStore,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func withTx(db *sql.DB, ctx context.Context, f func(*sql.Tx) error) error {
|
||||
@@ -70,3 +79,17 @@ func withTx(db *sql.DB, ctx context.Context, f func(*sql.Tx) error) error {
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func initStores(db *sql.DB) (*UserStore, *RoleStore, error) {
|
||||
userStore, err := NewUserStore(db)
|
||||
if err != nil {
|
||||
return &UserStore{}, &RoleStore{}, err
|
||||
}
|
||||
|
||||
roleStore, err := NewRoleStore(db)
|
||||
if err != nil {
|
||||
return &UserStore{}, &RoleStore{}, err
|
||||
}
|
||||
|
||||
return userStore, roleStore, nil
|
||||
}
|
||||
+341
-71
@@ -8,6 +8,14 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
const (
|
||||
adminLevel = 3
|
||||
modLevel = 2
|
||||
userLevel = 1
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@@ -29,6 +37,8 @@ type password struct {
|
||||
hash []byte
|
||||
}
|
||||
|
||||
var psql = sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
|
||||
func (p *password) Set(password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -42,30 +52,163 @@ func (p *password) Set(password string) error {
|
||||
|
||||
type UserStore struct {
|
||||
db *sql.DB
|
||||
|
||||
getByIDStmt *sql.Stmt
|
||||
getByEmailStmt *sql.Stmt
|
||||
getByInvitationStmt *sql.Stmt
|
||||
getUserFromPasswordTokenStmt *sql.Stmt
|
||||
updateStmt *sql.Stmt
|
||||
deleteStmt *sql.Stmt
|
||||
inviteCreateStmt *sql.Stmt
|
||||
invitationDeleteStmt *sql.Stmt
|
||||
deletePasswordResetRequestStmt *sql.Stmt
|
||||
passwordResetRequestCreateStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func NewUserStore(db *sql.DB) (*UserStore, error) {
|
||||
store := &UserStore{
|
||||
db: db,
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
store.getByIDStmt, err = db.Prepare(`
|
||||
SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.password, u.is_active, u.created_at, u.updated_at, roles.*
|
||||
FROM users u
|
||||
JOIN roles ON u.role_id = roles.id
|
||||
WHERE u.id = $1 AND u.is_active = true
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.getByEmailStmt, err = db.Prepare(`
|
||||
SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.password, u.is_active, u.created_at, u.updated_at, roles.*
|
||||
FROM users u
|
||||
JOIN roles ON u.role_id = roles.id
|
||||
WHERE u.email = $1 AND u.is_active = true
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.getByInvitationStmt, err = db.Prepare(`
|
||||
SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.created_at, u.updated_at, is_active
|
||||
FROM users u
|
||||
JOIN invitations i ON u.id = i.user_id
|
||||
WHERE i.token = $1 AND i.expiry > $2
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.getUserFromPasswordTokenStmt, err = db.Prepare(`
|
||||
SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.password, u.is_active, u.created_at, u.updated_at
|
||||
FROM users u
|
||||
JOIN password_reset_requests pr ON u.id = pr.user_id
|
||||
WHERE u.id = $1 AND pr.expiry > $2
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.updateStmt, err = db.Prepare(`
|
||||
UPDATE users
|
||||
SET first_name = $1, last_name = $2, username = $3, email = $4, is_active = $5, updated_at = $6
|
||||
WHERE id = $7
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.deleteStmt, err = db.Prepare(`
|
||||
DELETE FROM users WHERE id = $1
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.invitationDeleteStmt, err = db.Prepare(`
|
||||
DELETE FROM invitations WHERE user_id = $1
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.deletePasswordResetRequestStmt, err = db.Prepare(`
|
||||
DELETE FROM password_reset_requests WHERE user_id = $1
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.inviteCreateStmt, err = db.Prepare(`
|
||||
INSERT INTO invitations (user_id, token, expiry) VALUES ($1, $2, $3)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.passwordResetRequestCreateStmt, err = db.Prepare(`
|
||||
INSERT INTO password_reset_requests (user_id, token, expiry) VALUES ($1, $2, $3)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) Close() error {
|
||||
var errs []error
|
||||
if err := s.getByIDStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.getByEmailStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.getByInvitationStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.updateStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.deleteStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.invitationDeleteStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs[0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserStore) Create(ctx context.Context, tx *sql.Tx, user *User) error {
|
||||
query := `
|
||||
INSERT INTO users (first_name, last_name, username, email, password, role_id)
|
||||
VALUES ($1, $2, $3, $4, $5, (SELECT id FROM roles WHERE name = $6)) RETURNING id, created_at, updated_at
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
role := user.Role.Name
|
||||
if role == "" {
|
||||
role = "user"
|
||||
}
|
||||
|
||||
err := tx.QueryRowContext(
|
||||
query, args, err := psql.Insert("users").
|
||||
Columns("first_name", "last_name", "username", "email", "password", "role_id").
|
||||
Values(user.FirstName, user.LastName, user.Username, user.Email, user.Password.hash, sq.Expr("(SELECT id FROM roles WHERE name = ?)", role)).
|
||||
Suffix("RETURNING id, created_at, updated_at").ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = tx.QueryRowContext(
|
||||
ctx,
|
||||
query,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.Email,
|
||||
user.Password.hash,
|
||||
role,
|
||||
args...,
|
||||
).Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt)
|
||||
if err != nil {
|
||||
switch {
|
||||
@@ -81,28 +224,76 @@ func (s *UserStore) Create(ctx context.Context, tx *sql.Tx, user *User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserStore) GetAll(ctx context.Context, userID int64, roleLevel int64) ([]*User, error) {
|
||||
|
||||
var whereConditions []sq.Sqlizer
|
||||
|
||||
whereConditions = append(whereConditions, sq.Eq{"u.is_active": true})
|
||||
|
||||
if roleLevel == modLevel {
|
||||
whereConditions = append(whereConditions, sq.Or{
|
||||
sq.Lt{"roles.level": roleLevel},
|
||||
sq.Eq{"u.id": userID},
|
||||
})
|
||||
}
|
||||
|
||||
query, args, err := psql.Select("u.id", "u.first_name", "u.last_name", "u.username", "u.email", "u.is_active", "u.created_at", "u.updated_at", "roles.*").
|
||||
From("users u").Join("roles ON u.role_id = roles.id").Where(sq.And(
|
||||
whereConditions,
|
||||
)).OrderBy("u.created_at DESC").
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := make([]*User, 0)
|
||||
for rows.Next() {
|
||||
user := &User{}
|
||||
err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Username,
|
||||
&user.Email,
|
||||
&user.IsActive,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
&user.RoleID,
|
||||
&user.Role.Name,
|
||||
&user.Role.Level,
|
||||
&user.Role.Description,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) GetByID(ctx context.Context, id int64) (*User, error) {
|
||||
query := `
|
||||
SELECT users.id, first_name, last_name, username, email, password, created_at, updated_at, roles.*
|
||||
FROM users
|
||||
JOIN roles ON (users.role_id = roles.id)
|
||||
WHERE users.id = $1
|
||||
AND is_active = true
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
user := &User{}
|
||||
err := s.db.QueryRowContext(ctx, query, id).Scan(
|
||||
err := s.getByIDStmt.QueryRowContext(ctx, id).Scan(
|
||||
&user.ID,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Username,
|
||||
&user.Email,
|
||||
&user.Password.hash,
|
||||
&user.IsActive,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
&user.Role.ID,
|
||||
&user.RoleID,
|
||||
&user.Role.Name,
|
||||
&user.Role.Level,
|
||||
&user.Role.Description,
|
||||
@@ -115,34 +306,26 @@ func (s *UserStore) GetByID(ctx context.Context, id int64) (*User, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error) {
|
||||
query := `
|
||||
SELECT users.id, first_name, last_name, username, email, password, created_at, updated_at, roles.*
|
||||
FROM users
|
||||
JOIN roles ON (users.role_id = roles.id)
|
||||
WHERE email = $1
|
||||
AND is_active = true
|
||||
`
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
user := &User{}
|
||||
|
||||
err := s.db.QueryRowContext(ctx, query, email).Scan(
|
||||
err := s.getByEmailStmt.QueryRowContext(ctx, email).Scan(
|
||||
&user.ID,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Username,
|
||||
&user.Email,
|
||||
&user.Password.hash,
|
||||
&user.IsActive,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
&user.Role.ID,
|
||||
&user.RoleID,
|
||||
&user.Role.Name,
|
||||
&user.Role.Level,
|
||||
&user.Role.Description,
|
||||
@@ -160,17 +343,11 @@ func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
}
|
||||
|
||||
func (s *UserStore) Update(ctx context.Context, user *User) error {
|
||||
query := `
|
||||
UPDATE users
|
||||
SET first_name = $1, last_name = $2, username = $3, email = $4, password = $5
|
||||
WHERE id = $6
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := s.db.ExecContext(
|
||||
_, err := s.updateStmt.ExecContext(
|
||||
ctx,
|
||||
query,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
@@ -199,10 +376,40 @@ func (s *UserStore) CreateAndInvite(ctx context.Context, user *User, token strin
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserStore) PasswordResetRequest(ctx context.Context, userID int64, token string, expiration time.Duration) error {
|
||||
return withTx(s.db, ctx, func(tx *sql.Tx) error {
|
||||
if err := s.createPasswordResetRequest(ctx, tx, userID, token, expiration); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserStore) ResetPassword(ctx context.Context, token string, newPassword string) error {
|
||||
return withTx(s.db, ctx, func(tx *sql.Tx) error {
|
||||
user, err := s.getUserFromPaswordToken(ctx, tx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Password.Set(newPassword)
|
||||
if err := s.update(ctx, tx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.deletePasswordResetRequest(ctx, tx, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserStore) Activate(ctx context.Context, token string) error {
|
||||
return withTx(s.db, ctx, func(tx *sql.Tx) error {
|
||||
// Find the user that this token belongs to
|
||||
user, err := getUserFromInvitation(ctx, tx, token)
|
||||
user, err := s.getUserFromInvitation(ctx, tx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -238,12 +445,18 @@ func (s *UserStore) Delete(ctx context.Context, id int64) error {
|
||||
|
||||
// * Soft delete user
|
||||
func (s *UserStore) DeleteUser(ctx context.Context, id int64) error {
|
||||
query := `UPDATE users SET is_active = false WHERE id = $1`
|
||||
query, _, err := psql.Update("users").
|
||||
Set("is_active", false).
|
||||
Where(sq.Eq{"id": id}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := s.db.ExecContext(ctx, query, id)
|
||||
_, err = s.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -255,22 +468,18 @@ func (p *password) Compare(password string) error {
|
||||
return bcrypt.CompareHashAndPassword(p.hash, []byte(password))
|
||||
}
|
||||
|
||||
func getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User, error) {
|
||||
query := `
|
||||
SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.created_at, u.updated_at, is_active
|
||||
FROM users u
|
||||
JOIN invitations i ON u.id = i.user_id
|
||||
WHERE i.token = $1 AND i.expiry > $2
|
||||
`
|
||||
|
||||
func (s *UserStore) getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User, error) {
|
||||
hashed := sha256.Sum256([]byte(token))
|
||||
hashedToken := hex.EncodeToString(hashed[:])
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := tx.Stmt(s.getByInvitationStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
user := &User{}
|
||||
err := tx.QueryRowContext(ctx, query, hashedToken, time.Now()).Scan(
|
||||
err := stmt.QueryRowContext(ctx, hashedToken, time.Now()).Scan(
|
||||
&user.ID,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
@@ -293,14 +502,13 @@ func getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User
|
||||
}
|
||||
|
||||
func (s *UserStore) createUserInvitation(ctx context.Context, tx *sql.Tx, userID int64, exp time.Duration, token string) error {
|
||||
query := `
|
||||
INSERT INTO invitations (user_id, token, expiry)
|
||||
VALUES ($1, $2, $3)
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := tx.ExecContext(ctx, query, userID, token, time.Now().Add(exp))
|
||||
stmt := tx.Stmt(s.inviteCreateStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(ctx, userID, token, time.Now().Add(exp))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -308,18 +516,63 @@ func (s *UserStore) createUserInvitation(ctx context.Context, tx *sql.Tx, userID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserStore) update(ctx context.Context, tx *sql.Tx, user *User) error {
|
||||
query := `
|
||||
UPDATE users
|
||||
SET first_name = $1, last_name = $2, username = $3, email = $4, is_active = $5
|
||||
WHERE id = $6
|
||||
`
|
||||
func (s *UserStore) createPasswordResetRequest(ctx context.Context, tx *sql.Tx, userID int64, token string, expiry time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := tx.ExecContext(
|
||||
stmt := tx.Stmt(s.passwordResetRequestCreateStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(ctx, userID, token, time.Now().Add(expiry))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserStore) getUserFromPaswordToken(ctx context.Context, tx *sql.Tx, token string) (*User, error) {
|
||||
hashed := sha256.Sum256([]byte(token))
|
||||
hashedToken := hex.EncodeToString(hashed[:])
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := tx.Stmt(s.getUserFromPasswordTokenStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
user := &User{}
|
||||
err := stmt.QueryRowContext(ctx, hashedToken, time.Now()).Scan(
|
||||
&user.ID,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Username,
|
||||
&user.Email,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
&user.IsActive,
|
||||
)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case sql.ErrNoRows:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) update(ctx context.Context, tx *sql.Tx, user *User) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := tx.Stmt(s.updateStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(
|
||||
ctx,
|
||||
query,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
@@ -335,12 +588,28 @@ func (s *UserStore) update(ctx context.Context, tx *sql.Tx, user *User) error {
|
||||
}
|
||||
|
||||
func (s *UserStore) deleteUserInvitation(ctx context.Context, tx *sql.Tx, userID int64) error {
|
||||
query := `DELETE FROM invitations WHERE user_id = $1`
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := tx.ExecContext(ctx, query, userID)
|
||||
stmt := tx.Stmt(s.invitationDeleteStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserStore) deletePasswordResetRequest(ctx context.Context, tx *sql.Tx, userID int64) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := tx.Stmt(s.deletePasswordResetRequestStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -349,12 +618,13 @@ func (s *UserStore) deleteUserInvitation(ctx context.Context, tx *sql.Tx, userID
|
||||
}
|
||||
|
||||
func (s *UserStore) delete(ctx context.Context, tx *sql.Tx, id int64) error {
|
||||
query := `DELETE FROM users WHERE id = $1`
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := tx.ExecContext(ctx, query, id)
|
||||
stmt := tx.Stmt(s.deleteStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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