Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a8d52616f
|
||
|
|
da42646bbf
|
||
|
|
f85191b918
|
||
|
|
57730270a0
|
||
|
|
189534031b
|
@@ -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"
|
|
||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
.envrc
|
.envrc
|
||||||
|
/bin/
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ migrate-down:
|
|||||||
|
|
||||||
.PHONY: seed
|
.PHONY: seed
|
||||||
seed:
|
seed:
|
||||||
@go run cmd/migrate/seed/main.go
|
@DB_ADDR=$(DB_ADDR) go run cmd/migrate/seed/main.go
|
||||||
|
|
||||||
.PHONY: gen-docs
|
.PHONY: gen-docs
|
||||||
gen-docs:
|
gen-docs:
|
||||||
@swag init -g ./api/main.go -d cmd,internal && swag fmt
|
@swag init -g ./api/main.go -d cmd,internal && swag fmt
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+33
-17
@@ -48,9 +48,10 @@ type config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mailConfig struct {
|
type mailConfig struct {
|
||||||
exp time.Duration
|
inviteExp time.Duration
|
||||||
fromEmail string
|
passwordRefreshExp time.Duration
|
||||||
sendGrid sendGridConfig
|
fromEmail string
|
||||||
|
sendGrid sendGridConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type authConfig struct {
|
type authConfig struct {
|
||||||
@@ -64,10 +65,11 @@ type basicConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type tokenConfig struct {
|
type tokenConfig struct {
|
||||||
secret string
|
secret string
|
||||||
exp time.Duration
|
exp time.Duration
|
||||||
iss string
|
refreshExp time.Duration
|
||||||
aud string
|
iss string
|
||||||
|
aud string
|
||||||
}
|
}
|
||||||
|
|
||||||
type redisConfig struct {
|
type redisConfig struct {
|
||||||
@@ -89,6 +91,8 @@ type dbConfig struct {
|
|||||||
maxIdleTime time.Duration
|
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 {
|
func (api *api) mount() http.Handler {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
// *CORS
|
// *CORS
|
||||||
@@ -106,7 +110,7 @@ func (api *api) mount() http.Handler {
|
|||||||
AllowCredentials: false,
|
AllowCredentials: false,
|
||||||
MaxAge: 300,
|
MaxAge: 300,
|
||||||
}))
|
}))
|
||||||
r.Use(api.RateLimiterMiddleware)
|
r.Use(api.rateLimiterMiddleware)
|
||||||
|
|
||||||
r.Use(middleware.Timeout(60 * time.Second))
|
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.Route("/users", func(r chi.Router) {
|
||||||
r.Put("/activate/{token}", api.activateUserHandler)
|
r.Put("/activate/{token}", api.activateUserHandler)
|
||||||
|
r.Put("/reset-password/{token}", api.resetPasswordHandler)
|
||||||
|
|
||||||
r.Route("/{id}", func(r chi.Router) {
|
r.Route("/me", func(r chi.Router) {
|
||||||
r.Use(api.AuthTokenMiddleware)
|
r.Use(api.AuthTokenMiddleware)
|
||||||
|
|
||||||
r.Get("/", api.getUserHandler)
|
r.Get("/", api.getUserHandler)
|
||||||
r.Patch("/", api.checkProfileOwnership(api.updateUserHandler))
|
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.Route("/authentication", func(r chi.Router) {
|
||||||
r.Post("/user", api.registerUserHandler)
|
r.Post("/register", api.registerUserHandler)
|
||||||
r.Post("/token", api.createTokenHandler)
|
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
|
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")
|
api.logger.Infow("server has been shutdown")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ func TestRateLimiterMiddleware(t *testing.T) {
|
|||||||
mockIP := "192.168.1.1"
|
mockIP := "192.168.1.1"
|
||||||
marginOfError := 2
|
marginOfError := 2
|
||||||
|
|
||||||
for i := 0; i < cfg.rateLimiter.RequestsPerTimeFrame+marginOfError; i++ {
|
for i := range cfg.rateLimiter.RequestsPerTimeFrame + marginOfError {
|
||||||
req, err := http.NewRequest("GET", ts.URL+"/v1/health", nil)
|
req, err := http.NewRequest("GET", ts.URL+"/v1/health", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create request: %v", err)
|
t.Fatalf("failed to create request: %v", err)
|
||||||
|
|||||||
+184
-9
@@ -5,7 +5,10 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/FernandoVideira/LK_API_Temp/internal/mailer"
|
"github.com/FernandoVideira/LK_API_Temp/internal/mailer"
|
||||||
@@ -27,6 +30,15 @@ type UserWithToken struct {
|
|||||||
Token string `json:"token"`
|
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
|
// RegisterUser godoc
|
||||||
//
|
//
|
||||||
// @Summary Register a new User
|
// @Summary Register a new User
|
||||||
@@ -38,7 +50,7 @@ type UserWithToken struct {
|
|||||||
// @Success 201 {object} UserWithToken "User Registered"
|
// @Success 201 {object} UserWithToken "User Registered"
|
||||||
// @Failure 400 {object} error "User payload error"
|
// @Failure 400 {object} error "User payload error"
|
||||||
// @Failure 500 {object} error "Internal Server Error"
|
// @Failure 500 {object} error "Internal Server Error"
|
||||||
// @Router /authentication/user [post]
|
// @Router /authentication/register [post]
|
||||||
func (api *api) registerUserHandler(w http.ResponseWriter, r *http.Request) {
|
func (api *api) registerUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var payload RegisterUserPayload
|
var payload RegisterUserPayload
|
||||||
if err := readJSON(w, r, &payload); err != nil {
|
if err := readJSON(w, r, &payload); err != nil {
|
||||||
@@ -75,7 +87,7 @@ func (api *api) registerUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
hashedToken := hex.EncodeToString(hash[:])
|
hashedToken := hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
// Store the user
|
// 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 {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, store.ErrDuplicateEmail):
|
case errors.Is(err, store.ErrDuplicateEmail):
|
||||||
@@ -178,6 +190,160 @@ func (api *api) createTokenHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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{
|
claims := &jwt.MapClaims{
|
||||||
"sub": user.ID,
|
"sub": user.ID,
|
||||||
"exp": time.Now().Add(api.config.auth.token.exp).Unix(),
|
"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
|
//* generate a new token -> add the claims
|
||||||
token, err := api.authenticator.GenerateToken(claims)
|
token, err := api.authenticator.GenerateToken(claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.internalServerError(w, r, err)
|
return AuthResponse{}, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//* send the token back to the user
|
refreshClaims := &jwt.MapClaims{
|
||||||
|
"sub": user.ID,
|
||||||
if err := api.jsonResponse(w, http.StatusCreated, token); err != nil {
|
"exp": time.Now().Add(api.config.auth.token.refreshExp).Unix(),
|
||||||
api.internalServerError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/FernandoVideira/LK_API_Temp/internal/store"
|
"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()
|
ctx := r.Context()
|
||||||
user := getUserFromContext(r)
|
user := getUserFromContext(r)
|
||||||
log.Println(user)
|
|
||||||
feed, err := api.store.Posts.GetUserFeed(ctx, user.ID, fq)
|
feed, err := api.store.Posts.GetUserFeed(ctx, user.ID, fq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.internalServerError(w, r, err)
|
api.internalServerError(w, r, err)
|
||||||
|
|||||||
+12
-7
@@ -56,8 +56,9 @@ func main() {
|
|||||||
},
|
},
|
||||||
env: env.GetString("ENV", "development"),
|
env: env.GetString("ENV", "development"),
|
||||||
mail: mailConfig{
|
mail: mailConfig{
|
||||||
exp: time.Hour * 24 * 3,
|
inviteExp: time.Hour * 24 * 3,
|
||||||
fromEmail: env.GetString("FROM_EMAIL", ""),
|
passwordRefreshExp: time.Hour * 24 * 7,
|
||||||
|
fromEmail: env.GetString("FROM_EMAIL", ""),
|
||||||
sendGrid: sendGridConfig{
|
sendGrid: sendGridConfig{
|
||||||
apiKey: env.GetString("SENDGRID_API_KEY", ""),
|
apiKey: env.GetString("SENDGRID_API_KEY", ""),
|
||||||
},
|
},
|
||||||
@@ -68,10 +69,11 @@ func main() {
|
|||||||
password: env.GetString("BASIC_AUTH_PASSWORD", "admin"),
|
password: env.GetString("BASIC_AUTH_PASSWORD", "admin"),
|
||||||
},
|
},
|
||||||
token: tokenConfig{
|
token: tokenConfig{
|
||||||
secret: env.GetString("AUTH_TOKEN_SECRET", "secret"),
|
secret: env.GetString("AUTH_TOKEN_SECRET", "secret"),
|
||||||
exp: env.GetDuration("AUTH_TOKEN_EXP", "24h"),
|
exp: env.GetDuration("AUTH_TOKEN_EXP", "24h"),
|
||||||
iss: env.GetString("AUTH_TOKEN_ISS", "lkapi"),
|
refreshExp: time.Hour * 24 * 7,
|
||||||
aud: env.GetString("AUTH_TOKEN_AUD", "lkapi"),
|
iss: env.GetString("AUTH_TOKEN_ISS", "lkapi"),
|
||||||
|
aud: env.GetString("AUTH_TOKEN_AUD", "lkapi"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rateLimiter: ratelimiter.Config{
|
rateLimiter: ratelimiter.Config{
|
||||||
@@ -111,7 +113,10 @@ func main() {
|
|||||||
cfg.rateLimiter.TimeFrame,
|
cfg.rateLimiter.TimeFrame,
|
||||||
)
|
)
|
||||||
|
|
||||||
store := store.NewStorage(db)
|
store, err := store.NewStorage(db)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err)
|
||||||
|
}
|
||||||
cacheStorage := cache.NewRedisStorage(redisDb)
|
cacheStorage := cache.NewRedisStorage(redisDb)
|
||||||
mailer := mailer.NewSendGrid(cfg.mail.sendGrid.apiKey, cfg.mail.fromEmail)
|
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)
|
api.badRequestError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
post, err := api.store.Posts.GetByID(ctx, id)
|
post, err := api.store.Posts.GetByID(ctx, id)
|
||||||
@@ -111,10 +112,11 @@ func (api *api) postsContextMiddleware(next http.Handler) http.Handler {
|
|||||||
switch {
|
switch {
|
||||||
case errors.Is(err, store.ErrNotFound):
|
case errors.Is(err, store.ErrNotFound):
|
||||||
api.notFoundError(w, r, err)
|
api.notFoundError(w, r, err)
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
api.internalServerError(w, r, err)
|
api.internalServerError(w, r, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, postCtx, post)
|
ctx = context.WithValue(ctx, postCtx, post)
|
||||||
@@ -122,6 +124,19 @@ func (api *api) postsContextMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *api) rateLimiterMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if api.config.rateLimiter.Enabled {
|
||||||
|
if allow, retryAfter := api.rateLimiter.Allow(r.RemoteAddr); !allow {
|
||||||
|
api.rateLimitExceededError(w, r, retryAfter.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (api *api) checkPostOwnership(requiredRole string, next http.HandlerFunc) http.HandlerFunc {
|
func (api *api) checkPostOwnership(requiredRole string, next http.HandlerFunc) http.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUserFromContext(r)
|
user := getUserFromContext(r)
|
||||||
@@ -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) {
|
func (api *api) checkRolePrecedence(ctx context.Context, user *store.User, roleName string) (bool, error) {
|
||||||
role, err := api.store.Roles.GetByName(ctx, roleName)
|
role, err := api.store.Roles.GetByName(ctx, roleName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,16 +226,3 @@ func (api *api) getUser(ctx context.Context, id int64) (*store.User, error) {
|
|||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *api) RateLimiterMiddleware(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if api.config.rateLimiter.Enabled {
|
|
||||||
if allow, retryAfter := api.rateLimiter.Allow(r.RemoteAddr); !allow {
|
|
||||||
api.rateLimitExceededError(w, r, retryAfter.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
+85
-3
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -12,8 +11,39 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type userKey string
|
type userKey string
|
||||||
|
type roleKey string
|
||||||
|
|
||||||
const userContextKey userKey = "user"
|
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
|
// 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 {
|
type updateUserPayload struct {
|
||||||
FirstName *string `json:"first_name" validate:"omitempty,max=100"`
|
FirstName *string `json:"first_name" validate:"omitempty,max=100"`
|
||||||
LastName *string `json:"last_name" validate:"omitempty,max=100"`
|
LastName *string `json:"last_name" validate:"omitempty,max=100"`
|
||||||
@@ -78,7 +159,6 @@ type updateUserPayload struct {
|
|||||||
// @Router /users/{id} [patch]
|
// @Router /users/{id} [patch]
|
||||||
func (api *api) updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
func (api *api) updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUserFromContext(r)
|
user := getUserFromContext(r)
|
||||||
log.Println(user)
|
|
||||||
|
|
||||||
var payload updateUserPayload
|
var payload updateUserPayload
|
||||||
if err := readJSON(w, r, &payload); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
api.cacheStore.Users.Delete(ctx, user.ID)
|
if api.config.redisCfg.enabled {
|
||||||
|
api.cacheStore.Users.Delete(ctx, user.ID)
|
||||||
|
}
|
||||||
return nil
|
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()
|
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)
|
db.Seed(store, conn)
|
||||||
}
|
}
|
||||||
|
|||||||
+254
-42
@@ -24,6 +24,178 @@ const docTemplate = `{
|
|||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/admin/users": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Fetches all users",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"summary": "Fetches all users",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/store.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/authentication/forgot-password": {
|
||||||
|
"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": {
|
"/authentication/token": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Create a new token for the user",
|
"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": {
|
"/health": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Health Check",
|
"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}": {
|
"/users/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -795,6 +980,18 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"main.ForgotPasswordPayload": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"main.RegisterUserPayload": {
|
"main.RegisterUserPayload": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"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": {
|
"main.updateUserPasswordPayload": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
+254
-42
@@ -16,6 +16,178 @@
|
|||||||
},
|
},
|
||||||
"basePath": "/v1",
|
"basePath": "/v1",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/admin/users": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Fetches all users",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"summary": "Fetches all users",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/store.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/authentication/forgot-password": {
|
||||||
|
"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": {
|
"/authentication/token": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Create a new token for the user",
|
"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": {
|
"/health": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Health Check",
|
"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}": {
|
"/users/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -787,6 +972,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"main.ForgotPasswordPayload": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"main.RegisterUserPayload": {
|
"main.RegisterUserPayload": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"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": {
|
"main.updateUserPasswordPayload": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
+169
-28
@@ -29,6 +29,14 @@ definitions:
|
|||||||
- email
|
- email
|
||||||
- password
|
- password
|
||||||
type: object
|
type: object
|
||||||
|
main.ForgotPasswordPayload:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
maxLength: 255
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
type: object
|
||||||
main.RegisterUserPayload:
|
main.RegisterUserPayload:
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
@@ -86,6 +94,16 @@ definitions:
|
|||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
main.resetPasswordPayload:
|
||||||
|
properties:
|
||||||
|
new_password:
|
||||||
|
type: string
|
||||||
|
new_password_confirmation:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- new_password
|
||||||
|
- new_password_confirmation
|
||||||
|
type: object
|
||||||
main.updateUserPasswordPayload:
|
main.updateUserPasswordPayload:
|
||||||
properties:
|
properties:
|
||||||
current_password:
|
current_password:
|
||||||
@@ -230,6 +248,121 @@ info:
|
|||||||
termsOfService: http://swagger.io/terms/
|
termsOfService: http://swagger.io/terms/
|
||||||
title: LK API Template
|
title: LK API Template
|
||||||
paths:
|
paths:
|
||||||
|
/admin/users:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Fetches all users
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/store.User'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema: {}
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema: {}
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema: {}
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Fetches all users
|
||||||
|
tags:
|
||||||
|
- admin
|
||||||
|
/authentication/forgot-password:
|
||||||
|
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:
|
/authentication/token:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -261,34 +394,6 @@ paths:
|
|||||||
summary: Create a new token
|
summary: Create a new token
|
||||||
tags:
|
tags:
|
||||||
- authentication
|
- authentication
|
||||||
/authentication/user:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Register a new User
|
|
||||||
parameters:
|
|
||||||
- description: User Credentials
|
|
||||||
in: body
|
|
||||||
name: payload
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/main.RegisterUserPayload'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"201":
|
|
||||||
description: User Registered
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/main.UserWithToken'
|
|
||||||
"400":
|
|
||||||
description: User payload error
|
|
||||||
schema: {}
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema: {}
|
|
||||||
summary: Register a new User
|
|
||||||
tags:
|
|
||||||
- authentication
|
|
||||||
/health:
|
/health:
|
||||||
get:
|
get:
|
||||||
description: Health Check
|
description: Health Check
|
||||||
@@ -717,6 +822,42 @@ paths:
|
|||||||
summary: Fetches the user feed
|
summary: Fetches the user feed
|
||||||
tags:
|
tags:
|
||||||
- feed
|
- feed
|
||||||
|
/users/reset-password/{token}:
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Reset user password with verification
|
||||||
|
parameters:
|
||||||
|
- description: Reset Password
|
||||||
|
in: body
|
||||||
|
name: payload
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/main.resetPasswordPayload'
|
||||||
|
- description: Reset Password Token
|
||||||
|
in: path
|
||||||
|
name: token
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Password reset successfully
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: Invalid request
|
||||||
|
schema: {}
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema: {}
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema: {}
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Reset user password
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
securityDefinitions:
|
securityDefinitions:
|
||||||
ApiKeyAuth:
|
ApiKeyAuth:
|
||||||
in: header
|
in: header
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ module github.com/FernandoVideira/LK_API_Temp
|
|||||||
go 1.23.4
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Masterminds/squirrel v1.5.4
|
||||||
github.com/go-chi/chi/v5 v5.2.0
|
github.com/go-chi/chi/v5 v5.2.0
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/go-playground/validator/v10 v10.24.0
|
github.com/go-playground/validator/v10 v10.24.0
|
||||||
@@ -31,6 +32,8 @@ require (
|
|||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // 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/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.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 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
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 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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=
|
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/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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
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=
|
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/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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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=
|
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 {
|
type Authenticator interface {
|
||||||
GenerateToken(jwt.Claims) (string, error)
|
GenerateToken(jwt.Claims) (string, error)
|
||||||
|
GenerateRefreshToken(jwt.Claims) (string, error)
|
||||||
ValidateToken(string) (*jwt.Token, error)
|
ValidateToken(string) (*jwt.Token, error)
|
||||||
|
ValidateRefreshToken(string) (*jwt.Token, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ func (j *JWTAuthenticator) GenerateToken(claims jwt.Claims) (string, error) {
|
|||||||
return tokenString, nil
|
return tokenString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *JWTAuthenticator) GenerateRefreshToken(claims jwt.Claims) (string, error) {
|
||||||
|
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
refreshTokenString, err := refreshToken.SignedString([]byte(j.secret))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshTokenString, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (j *JWTAuthenticator) ValidateToken(tokenString string) (*jwt.Token, error) {
|
func (j *JWTAuthenticator) ValidateToken(tokenString string) (*jwt.Token, error) {
|
||||||
return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
|
return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
@@ -45,3 +56,16 @@ func (j *JWTAuthenticator) ValidateToken(tokenString string) (*jwt.Token, error)
|
|||||||
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}),
|
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *JWTAuthenticator) ValidateRefreshToken(tokenString string) (*jwt.Token, error) {
|
||||||
|
return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(j.secret), nil
|
||||||
|
},
|
||||||
|
jwt.WithExpirationRequired(),
|
||||||
|
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,8 +24,20 @@ func (a *TestAuthenticator) GenerateToken(claims jwt.Claims) (string, error) {
|
|||||||
return token.SignedString([]byte(testToken))
|
return token.SignedString([]byte(testToken))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *TestAuthenticator) GenerateRefreshToken(claims jwt.Claims) (string, error) {
|
||||||
|
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, testClaims)
|
||||||
|
|
||||||
|
return refreshToken.SignedString([]byte(testToken))
|
||||||
|
}
|
||||||
|
|
||||||
func (a *TestAuthenticator) ValidateToken(token string) (*jwt.Token, error) {
|
func (a *TestAuthenticator) ValidateToken(token string) (*jwt.Token, error) {
|
||||||
return jwt.Parse(token, func(token *jwt.Token) (any, error) {
|
return jwt.Parse(token, func(token *jwt.Token) (any, error) {
|
||||||
return []byte(testToken), nil
|
return []byte(testToken), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *TestAuthenticator) ValidateRefreshToken(token string) (*jwt.Token, error) {
|
||||||
|
return jwt.Parse(token, func(token *jwt.Token) (any, error) {
|
||||||
|
return []byte(testToken), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ package mailer
|
|||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FromName = "LK API"
|
FromName = "LK API"
|
||||||
maxRetries = 3
|
maxRetries = 3
|
||||||
UserWelcomeTemplate = "user_activation.tmpl"
|
UserWelcomeTemplate = "user_activation.tmpl"
|
||||||
|
ForgotPasswordTemplate = "forgot_password.tmpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed "templates"
|
//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
|
package ratelimiter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -26,8 +25,6 @@ func (rl *FixedWindowLimiter) Allow(ip string) (bool, time.Duration) {
|
|||||||
count, exists := rl.clients[ip]
|
count, exists := rl.clients[ip]
|
||||||
rl.RUnlock()
|
rl.RUnlock()
|
||||||
|
|
||||||
log.Println(count, exists)
|
|
||||||
|
|
||||||
if !exists || count < rl.limit {
|
if !exists || count < rl.limit {
|
||||||
rl.Lock()
|
rl.Lock()
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ func (m *MockUserStore) Activate(ctx context.Context, token string) error {
|
|||||||
return args.Error(0)
|
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) {
|
func (m *MockUserStore) GetByID(ctx context.Context, id int64) (*User, error) {
|
||||||
args := m.Called(id)
|
args := m.Called(id)
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
@@ -48,6 +53,16 @@ func (m *MockUserStore) CreateAndInvite(ctx context.Context, user *User, token s
|
|||||||
return args.Error(0)
|
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 {
|
func (m *MockUserStore) Delete(ctx context.Context, id int64) error {
|
||||||
args := m.Called(id)
|
args := m.Called(id)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
|
|||||||
+16
-4
@@ -14,21 +14,33 @@ type Role struct {
|
|||||||
|
|
||||||
type RoleStore struct {
|
type RoleStore struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
|
|
||||||
|
getByNameStmt *sql.Stmt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RoleStore) GetByName(ctx context.Context, name string) (*Role, error) {
|
func NewRoleStore(db *sql.DB) (*RoleStore, error) {
|
||||||
query := `
|
getByNameStmt, err := db.PrepareContext(context.Background(), `
|
||||||
SELECT id, name, level, description
|
SELECT id, name, level, description
|
||||||
FROM roles
|
FROM roles
|
||||||
WHERE name = $1
|
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)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
role := &Role{}
|
role := &Role{}
|
||||||
|
|
||||||
err := s.db.QueryRowContext(ctx, query, name).Scan(
|
err := s.getByNameStmt.QueryRowContext(ctx, name).Scan(
|
||||||
&role.ID,
|
&role.ID,
|
||||||
&role.Name,
|
&role.Name,
|
||||||
&role.Level,
|
&role.Level,
|
||||||
|
|||||||
@@ -27,10 +27,13 @@ type Storage struct {
|
|||||||
Users interface {
|
Users interface {
|
||||||
Create(context.Context, *sql.Tx, *User) error
|
Create(context.Context, *sql.Tx, *User) error
|
||||||
Activate(context.Context, string) error
|
Activate(context.Context, string) error
|
||||||
|
GetAll(context.Context, int64, int64) ([]*User, error)
|
||||||
GetByID(context.Context, int64) (*User, error)
|
GetByID(context.Context, int64) (*User, error)
|
||||||
GetByEmail(context.Context, string) (*User, error)
|
GetByEmail(context.Context, string) (*User, error)
|
||||||
Update(context.Context, *User) error
|
Update(context.Context, *User) error
|
||||||
CreateAndInvite(context.Context, *User, string, time.Duration) 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
|
Delete(context.Context, int64) error
|
||||||
DeleteUser(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{
|
return Storage{
|
||||||
Posts: &PostStore{db},
|
Posts: &PostStore{db},
|
||||||
Users: &UserStore{db},
|
Users: userStore,
|
||||||
Comments: &CommentStore{db},
|
Comments: &CommentStore{db},
|
||||||
Followers: &FollowerStore{db},
|
Followers: &FollowerStore{db},
|
||||||
Roles: &RoleStore{db},
|
Roles: roleStore,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func withTx(db *sql.DB, ctx context.Context, f func(*sql.Tx) error) error {
|
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()
|
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"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
adminLevel = 3
|
||||||
|
modLevel = 2
|
||||||
|
userLevel = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -29,6 +37,8 @@ type password struct {
|
|||||||
hash []byte
|
hash []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var psql = sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||||
|
|
||||||
func (p *password) Set(password string) error {
|
func (p *password) Set(password string) error {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -42,30 +52,163 @@ func (p *password) Set(password string) error {
|
|||||||
|
|
||||||
type UserStore struct {
|
type UserStore struct {
|
||||||
db *sql.DB
|
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 {
|
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
|
role := user.Role.Name
|
||||||
if role == "" {
|
if role == "" {
|
||||||
role = "user"
|
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,
|
ctx,
|
||||||
query,
|
query,
|
||||||
user.FirstName,
|
args...,
|
||||||
user.LastName,
|
|
||||||
user.Username,
|
|
||||||
user.Email,
|
|
||||||
user.Password.hash,
|
|
||||||
role,
|
|
||||||
).Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt)
|
).Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
@@ -81,28 +224,76 @@ func (s *UserStore) Create(ctx context.Context, tx *sql.Tx, user *User) error {
|
|||||||
return nil
|
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) {
|
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)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
user := &User{}
|
user := &User{}
|
||||||
err := s.db.QueryRowContext(ctx, query, id).Scan(
|
err := s.getByIDStmt.QueryRowContext(ctx, id).Scan(
|
||||||
&user.ID,
|
&user.ID,
|
||||||
&user.FirstName,
|
&user.FirstName,
|
||||||
&user.LastName,
|
&user.LastName,
|
||||||
&user.Username,
|
&user.Username,
|
||||||
&user.Email,
|
&user.Email,
|
||||||
&user.Password.hash,
|
&user.Password.hash,
|
||||||
|
&user.IsActive,
|
||||||
&user.CreatedAt,
|
&user.CreatedAt,
|
||||||
&user.UpdatedAt,
|
&user.UpdatedAt,
|
||||||
&user.Role.ID,
|
&user.RoleID,
|
||||||
&user.Role.Name,
|
&user.Role.Name,
|
||||||
&user.Role.Level,
|
&user.Role.Level,
|
||||||
&user.Role.Description,
|
&user.Role.Description,
|
||||||
@@ -115,34 +306,26 @@ func (s *UserStore) GetByID(ctx context.Context, id int64) (*User, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error) {
|
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)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
user := &User{}
|
user := &User{}
|
||||||
|
|
||||||
err := s.db.QueryRowContext(ctx, query, email).Scan(
|
err := s.getByEmailStmt.QueryRowContext(ctx, email).Scan(
|
||||||
&user.ID,
|
&user.ID,
|
||||||
&user.FirstName,
|
&user.FirstName,
|
||||||
&user.LastName,
|
&user.LastName,
|
||||||
&user.Username,
|
&user.Username,
|
||||||
&user.Email,
|
&user.Email,
|
||||||
&user.Password.hash,
|
&user.Password.hash,
|
||||||
|
&user.IsActive,
|
||||||
&user.CreatedAt,
|
&user.CreatedAt,
|
||||||
&user.UpdatedAt,
|
&user.UpdatedAt,
|
||||||
&user.Role.ID,
|
&user.RoleID,
|
||||||
&user.Role.Name,
|
&user.Role.Name,
|
||||||
&user.Role.Level,
|
&user.Role.Level,
|
||||||
&user.Role.Description,
|
&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 {
|
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)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := s.db.ExecContext(
|
_, err := s.updateStmt.ExecContext(
|
||||||
ctx,
|
ctx,
|
||||||
query,
|
|
||||||
user.FirstName,
|
user.FirstName,
|
||||||
user.LastName,
|
user.LastName,
|
||||||
user.Username,
|
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 {
|
func (s *UserStore) Activate(ctx context.Context, token string) error {
|
||||||
return withTx(s.db, ctx, func(tx *sql.Tx) error {
|
return withTx(s.db, ctx, func(tx *sql.Tx) error {
|
||||||
// Find the user that this token belongs to
|
// Find the user that this token belongs to
|
||||||
user, err := getUserFromInvitation(ctx, tx, token)
|
user, err := s.getUserFromInvitation(ctx, tx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -238,12 +445,18 @@ func (s *UserStore) Delete(ctx context.Context, id int64) error {
|
|||||||
|
|
||||||
// * Soft delete user
|
// * Soft delete user
|
||||||
func (s *UserStore) DeleteUser(ctx context.Context, id int64) error {
|
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)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := s.db.ExecContext(ctx, query, id)
|
_, err = s.db.ExecContext(ctx, query, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -255,22 +468,18 @@ func (p *password) Compare(password string) error {
|
|||||||
return bcrypt.CompareHashAndPassword(p.hash, []byte(password))
|
return bcrypt.CompareHashAndPassword(p.hash, []byte(password))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User, error) {
|
func (s *UserStore) 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
|
|
||||||
`
|
|
||||||
|
|
||||||
hashed := sha256.Sum256([]byte(token))
|
hashed := sha256.Sum256([]byte(token))
|
||||||
hashedToken := hex.EncodeToString(hashed[:])
|
hashedToken := hex.EncodeToString(hashed[:])
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
stmt := tx.Stmt(s.getByInvitationStmt)
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
user := &User{}
|
user := &User{}
|
||||||
err := tx.QueryRowContext(ctx, query, hashedToken, time.Now()).Scan(
|
err := stmt.QueryRowContext(ctx, hashedToken, time.Now()).Scan(
|
||||||
&user.ID,
|
&user.ID,
|
||||||
&user.FirstName,
|
&user.FirstName,
|
||||||
&user.LastName,
|
&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 {
|
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)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -308,18 +516,63 @@ func (s *UserStore) createUserInvitation(ctx context.Context, tx *sql.Tx, userID
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserStore) update(ctx context.Context, tx *sql.Tx, user *User) error {
|
func (s *UserStore) createPasswordResetRequest(ctx context.Context, tx *sql.Tx, userID int64, token string, expiry time.Duration) error {
|
||||||
query := `
|
|
||||||
UPDATE users
|
|
||||||
SET first_name = $1, last_name = $2, username = $3, email = $4, is_active = $5
|
|
||||||
WHERE id = $6
|
|
||||||
`
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
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,
|
ctx,
|
||||||
query,
|
|
||||||
user.FirstName,
|
user.FirstName,
|
||||||
user.LastName,
|
user.LastName,
|
||||||
user.Username,
|
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 {
|
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)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
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)
|
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := tx.ExecContext(ctx, query, id)
|
stmt := tx.Stmt(s.deleteStmt)
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
_, err := stmt.ExecContext(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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