feat: user_rework (todo -> GetAll)
This commit is contained in:
24 files changed
+636
-131
No files matched your search
@@ -1,35 +0,0 @@
|
||||
# Server Configuration
|
||||
export ADDR=":8080"
|
||||
export EXTERNAL_URL="localhost:8080"
|
||||
export FRONTEND_URL="http://localhost:5173"
|
||||
export ENV="development"
|
||||
export ALLOWED_ORIGINS="https://*,http://*" # TODO: Change to the actual allowed origins on production
|
||||
|
||||
# Database Configuration
|
||||
export DB_ADDR="postgres://postgres:postgres@localhost:5432/lk_tmpl?sslmode=disable"
|
||||
export DB_MAX_OPEN_CONNS="30"
|
||||
export DB_MAX_IDLE_CONNS="30"
|
||||
export DB_MAX_IDLE_TIME="15m"
|
||||
|
||||
# Redis Configuration
|
||||
export REDIS_ADDR="localhost:6379"
|
||||
export REDIS_PASSWORD=""
|
||||
export REDIS_DB="0"
|
||||
export REDIS_ENABLED="false"
|
||||
|
||||
# Authentication
|
||||
export BASIC_AUTH_USERNAME="admin"
|
||||
export BASIC_AUTH_PASSWORD="admin"
|
||||
export AUTH_TOKEN_SECRET="your-secret-key-here"
|
||||
export AUTH_TOKEN_EXP="24h"
|
||||
export AUTH_TOKEN_ISS="lkapi"
|
||||
export AUTH_TOKEN_AUD="lkapi"
|
||||
|
||||
# Email Configuration
|
||||
export FROM_EMAIL="[email protected]"
|
||||
export SENDGRID_API_KEY="your-sendgrid-api-key"
|
||||
|
||||
# Rate Limiter
|
||||
export RATE_LIMITER_REQS_COUNT="20"
|
||||
export RATE_LIMITER_TIME_FRAME="1m"
|
||||
export RATE_LIMITER_ENABLED="true"
|
||||
@@ -1 +1 @@
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit stLine truncated
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit stLine truncated
|
||||
+29
-11
@@ -48,7 +48,8 @@ type config struct {
|
||||
}
|
||||
|
||||
type mailConfig struct {
|
||||
exp time.Duration
|
||||
inviteExp time.Duration
|
||||
passwordRefreshExp time.Duration
|
||||
fromEmail string
|
||||
sendGrid sendGridConfig
|
||||
}
|
||||
@@ -66,6 +67,7 @@ type basicConfig struct {
|
||||
type tokenConfig struct {
|
||||
secret string
|
||||
exp time.Duration
|
||||
refreshExp time.Duration
|
||||
iss string
|
||||
aud string
|
||||
}
|
||||
@@ -89,6 +91,8 @@ type dbConfig struct {
|
||||
maxIdleTime time.Duration
|
||||
}
|
||||
|
||||
// This `mount` function is responsible for setting up the routing for your API
|
||||
// using the Chi router. Here's a breakdown of what it does:
|
||||
func (api *api) mount() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
// *CORS
|
||||
@@ -135,29 +139,37 @@ func (api *api) mount() http.Handler {
|
||||
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Put("/activate/{token}", api.activateUserHandler)
|
||||
//r.Put("/reset-password/{token}", api.resetPasswordHandler)
|
||||
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Route("/me", func(r chi.Router) {
|
||||
r.Use(api.AuthTokenMiddleware)
|
||||
|
||||
r.Get("/", api.getUserHandler)
|
||||
r.Patch("/", api.checkProfileOwnership(api.updateUserHandler))
|
||||
r.Patch("/password", api.checkProfileOwnership(api.updatePasswordHandler))
|
||||
r.Put(("/follow"), api.followUserHandler)
|
||||
r.Put(("/unfollow"), api.unfollowUserHandler)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(api.AuthTokenMiddleware)
|
||||
r.Get("/feed", api.getUserFeedHandler)
|
||||
})
|
||||
})
|
||||
|
||||
r.Route("/authentication", func(r chi.Router) {
|
||||
r.Post("/user", api.registerUserHandler)
|
||||
r.Post("/register", api.registerUserHandler)
|
||||
r.Post("/token", api.createTokenHandler)
|
||||
|
||||
r.Post("/forgot-password", api.forgotPasswordHandler)
|
||||
r.Post("/refresh", api.refreshTokenHandler)
|
||||
})
|
||||
})
|
||||
|
||||
r.Route("/admin", func(r chi.Router) {
|
||||
r.Use(api.AuthTokenMiddleware)
|
||||
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Get("/", api.protectAdminRoutes("moderator", api.getUsersHandler))
|
||||
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Get("/", api.protectAdminRoutes("moderator", api.getUserByIdHandler))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -204,6 +216,12 @@ func (api *api) run(mux http.Handler) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if userStore, ok := api.store.Users.(*store.UserStore); ok {
|
||||
if err := userStore.Close(); err != nil {
|
||||
api.logger.Errorw("error closing userstore", err)
|
||||
}
|
||||
}
|
||||
|
||||
api.logger.Infow("server has been shutdown")
|
||||
|
||||
return nil
|
||||
|
||||
+88
-1
@@ -27,6 +27,15 @@ type UserWithToken struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type ForgotPasswordPayload struct {
|
||||
Email string `json:"email" validate:"required,email,max=255"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// RegisterUser godoc
|
||||
//
|
||||
// @Summary Register a new User
|
||||
@@ -75,7 +84,7 @@ func (api *api) registerUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
hashedToken := hex.EncodeToString(hash[:])
|
||||
|
||||
// Store the user
|
||||
err := api.store.Users.CreateAndInvite(ctx, user, hashedToken, api.config.mail.exp)
|
||||
err := api.store.Users.CreateAndInvite(ctx, user, hashedToken, api.config.mail.inviteExp)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, store.ErrDuplicateEmail):
|
||||
@@ -201,3 +210,81 @@ func (api *api) createTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/FernandoVideira/LK_API_Temp/internal/store"
|
||||
@@ -48,7 +47,6 @@ func (api *api) getUserFeedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ctx := r.Context()
|
||||
user := getUserFromContext(r)
|
||||
log.Println(user)
|
||||
feed, err := api.store.Posts.GetUserFeed(ctx, user.ID, fq)
|
||||
if err != nil {
|
||||
api.internalServerError(w, r, err)
|
||||
|
||||
+6
-2
@@ -56,7 +56,8 @@ func main() {
|
||||
},
|
||||
env: env.GetString("ENV", "development"),
|
||||
mail: mailConfig{
|
||||
exp: time.Hour * 24 * 3,
|
||||
inviteExp: time.Hour * 24 * 3,
|
||||
passwordRefreshExp: time.Hour * 24 * 7,
|
||||
fromEmail: env.GetString("FROM_EMAIL", ""),
|
||||
sendGrid: sendGridConfig{
|
||||
apiKey: env.GetString("SENDGRID_API_KEY", ""),
|
||||
@@ -111,7 +112,10 @@ func main() {
|
||||
cfg.rateLimiter.TimeFrame,
|
||||
)
|
||||
|
||||
store := store.NewStorage(db)
|
||||
store, err := store.NewStorage(db)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
cacheStorage := cache.NewRedisStorage(redisDb)
|
||||
mailer := mailer.NewSendGrid(cfg.mail.sendGrid.apiKey, cfg.mail.fromEmail)
|
||||
|
||||
|
||||
+26
-1
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -104,18 +105,21 @@ func (api *api) postsContextMiddleware(next http.Handler) http.Handler {
|
||||
api.badRequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
post, err := api.store.Posts.GetByID(ctx, id)
|
||||
log.Println("post", post)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, store.ErrNotFound):
|
||||
api.notFoundError(w, r, err)
|
||||
return
|
||||
default:
|
||||
api.internalServerError(w, r, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, postCtx, post)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
@@ -161,6 +165,27 @@ 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
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
ctx = context.WithValue(ctx, roleContextKey, user.Role)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (api *api) checkRolePrecedence(ctx context.Context, user *store.User, roleName string) (bool, error) {
|
||||
role, err := api.store.Roles.GetByName(ctx, roleName)
|
||||
if err != nil {
|
||||
|
||||
+38
-2
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -12,8 +11,39 @@ import (
|
||||
)
|
||||
|
||||
type userKey string
|
||||
type roleKey string
|
||||
|
||||
const userContextKey userKey = "user"
|
||||
const roleContextKey roleKey = "role"
|
||||
|
||||
// GetUser godoc
|
||||
//
|
||||
// @Summary Fetches all users
|
||||
// @Description Fetches all users
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} store.User
|
||||
// @Failure 400 {object} error
|
||||
// @Failure 404 {object} error
|
||||
// @Failure 500 {object} error
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /admin/users [get]
|
||||
func (api *api) getUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
role := getRoleFromContext(r)
|
||||
isAdmin := false
|
||||
if role.Level == 3 {
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
users, err := api.store.Users.GetAll(r.Context(), isAdmin)
|
||||
if err != nil {
|
||||
api.internalServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.jsonResponse(w, http.StatusOK, users)
|
||||
}
|
||||
|
||||
// GetUser godoc
|
||||
//
|
||||
@@ -78,7 +108,6 @@ type updateUserPayload struct {
|
||||
// @Router /users/{id} [patch]
|
||||
func (api *api) updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user := getUserFromContext(r)
|
||||
log.Println(user)
|
||||
|
||||
var payload updateUserPayload
|
||||
if err := readJSON(w, r, &payload); err != nil {
|
||||
@@ -103,7 +132,9 @@ func (api *api) updateUser(ctx context.Context, user *store.User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if api.config.redisCfg.enabled {
|
||||
api.cacheStore.Users.Delete(ctx, user.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -300,3 +331,8 @@ func getUserFromContext(r *http.Request) *store.User {
|
||||
user, _ := r.Context().Value(userContextKey).(*store.User)
|
||||
return user
|
||||
}
|
||||
|
||||
func getRoleFromContext(r *http.Request) *store.Role {
|
||||
role, _ := r.Context().Value(roleContextKey).(*store.Role)
|
||||
return role
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS password_reset_requests;
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS password_reset_requests (
|
||||
token bytea PRIMARY KEY,
|
||||
user_id bigint NOT NULL,
|
||||
expiry TIMESTAMP(0)
|
||||
WITH
|
||||
TIME ZONE NOT NULL
|
||||
);
|
||||
@@ -16,6 +16,10 @@ func main() {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
store := store.NewStorage(conn)
|
||||
store, err := store.NewStorage(conn)
|
||||
if err != nil {
|
||||
log.Fatalf("could not create storage: %v", err)
|
||||
}
|
||||
|
||||
db.Seed(store, conn)
|
||||
}
|
||||
@@ -24,6 +24,52 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/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/token": {
|
||||
"post": {
|
||||
"description": "Create a new token for the user",
|
||||
@@ -795,6 +841,18 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.ForgotPasswordPayload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.RegisterUserPayload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -16,6 +16,52 @@
|
||||
},
|
||||
"basePath": "/v1",
|
||||
"paths": {
|
||||
"/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/token": {
|
||||
"post": {
|
||||
"description": "Create a new token for the user",
|
||||
@@ -787,6 +833,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.ForgotPasswordPayload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.RegisterUserPayload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -29,6 +29,14 @@ definitions:
|
||||
- email
|
||||
- password
|
||||
type: object
|
||||
main.ForgotPasswordPayload:
|
||||
properties:
|
||||
email:
|
||||
maxLength: 255
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
type: object
|
||||
main.RegisterUserPayload:
|
||||
properties:
|
||||
email:
|
||||
@@ -230,6 +238,37 @@ info:
|
||||
termsOfService: http://swagger.io/terms/
|
||||
title: LK API Template
|
||||
paths:
|
||||
/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/token:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -20,6 +20,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
@@ -31,6 +32,8 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -42,6 +44,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
@@ -64,6 +70,7 @@ github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBU
|
||||
github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
|
||||
@@ -6,6 +6,7 @@ const (
|
||||
FromName = "LK API"
|
||||
maxRetries = 3
|
||||
UserWelcomeTemplate = "user_activation.tmpl"
|
||||
ForgotPasswordTemplate = "forgot_password.tmpl"
|
||||
)
|
||||
|
||||
//go:embed "templates"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{{define "subject"}} Reset Your Password - {{.ServiceName}} {{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello {{.Username}},</p>
|
||||
|
||||
<p>We received a request to reset your password. If you didn't make this request, you can ignore this email.</p>
|
||||
|
||||
<p>To reset your password, click on the link below:</p>
|
||||
|
||||
<p><a href="{{.UpdateURL}}">{{.UpdateURL}}</a></p>
|
||||
|
||||
<p>If you want to reset your password manually, copy and paste the link above into your browser.</p>
|
||||
|
||||
<p>If you have any questions or need assistance, please contact our support team.</p>
|
||||
|
||||
<p>Thanks,</p>
|
||||
<p>The {{.ServiceName}} Team</p>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -1,7 +1,6 @@
|
||||
package ratelimiter
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -26,8 +25,6 @@ func (rl *FixedWindowLimiter) Allow(ip string) (bool, time.Duration) {
|
||||
count, exists := rl.clients[ip]
|
||||
rl.RUnlock()
|
||||
|
||||
log.Println(count, exists)
|
||||
|
||||
if !exists || count < rl.limit {
|
||||
rl.Lock()
|
||||
if !exists {
|
||||
|
||||
@@ -48,6 +48,11 @@ func (m *MockUserStore) CreateAndInvite(ctx context.Context, user *User, token s
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserStore) PasswordResetRequest(ctx context.Context, userID int64, token string, expiration time.Duration) error {
|
||||
args := m.Called(ctx, userID, token, expiration)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserStore) Delete(ctx context.Context, id int64) error {
|
||||
args := m.Called(id)
|
||||
return args.Error(0)
|
||||
|
||||
+16
-4
@@ -14,21 +14,33 @@ type Role struct {
|
||||
|
||||
type RoleStore struct {
|
||||
db *sql.DB
|
||||
|
||||
getByNameStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func (s *RoleStore) GetByName(ctx context.Context, name string) (*Role, error) {
|
||||
query := `
|
||||
func NewRoleStore(db *sql.DB) (*RoleStore, error) {
|
||||
getByNameStmt, err := db.PrepareContext(context.Background(), `
|
||||
SELECT id, name, level, description
|
||||
FROM roles
|
||||
WHERE name = $1
|
||||
`
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RoleStore{
|
||||
db: db,
|
||||
getByNameStmt: getByNameStmt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RoleStore) GetByName(ctx context.Context, name string) (*Role, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
role := &Role{}
|
||||
|
||||
err := s.db.QueryRowContext(ctx, query, name).Scan(
|
||||
err := s.getByNameStmt.QueryRowContext(ctx, name).Scan(
|
||||
&role.ID,
|
||||
&role.Name,
|
||||
&role.Level,
|
||||
|
||||
@@ -31,6 +31,7 @@ type Storage struct {
|
||||
GetByEmail(context.Context, string) (*User, error)
|
||||
Update(context.Context, *User) error
|
||||
CreateAndInvite(context.Context, *User, string, time.Duration) error
|
||||
PasswordResetRequest(context.Context, int64, string, time.Duration) error
|
||||
Delete(context.Context, int64) error
|
||||
DeleteUser(context.Context, int64) error
|
||||
}
|
||||
@@ -47,14 +48,20 @@ type Storage struct {
|
||||
}
|
||||
}
|
||||
|
||||
func NewStorage(db *sql.DB) Storage {
|
||||
func NewStorage(db *sql.DB) (Storage, error) {
|
||||
|
||||
userStore, roleStore, err := initStores(db)
|
||||
if err != nil {
|
||||
return Storage{}, err
|
||||
}
|
||||
|
||||
return Storage{
|
||||
Posts: &PostStore{db},
|
||||
Users: &UserStore{db},
|
||||
Users: userStore,
|
||||
Comments: &CommentStore{db},
|
||||
Followers: &FollowerStore{db},
|
||||
Roles: &RoleStore{db},
|
||||
}
|
||||
Roles: roleStore,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func withTx(db *sql.DB, ctx context.Context, f func(*sql.Tx) error) error {
|
||||
@@ -70,3 +77,17 @@ func withTx(db *sql.DB, ctx context.Context, f func(*sql.Tx) error) error {
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func initStores(db *sql.DB) (*UserStore, *RoleStore, error) {
|
||||
userStore, err := NewUserStore(db)
|
||||
if err != nil {
|
||||
return &UserStore{}, &RoleStore{}, err
|
||||
}
|
||||
|
||||
roleStore, err := NewRoleStore(db)
|
||||
if err != nil {
|
||||
return &UserStore{}, &RoleStore{}, err
|
||||
}
|
||||
|
||||
return userStore, roleStore, nil
|
||||
}
|
||||
+196
-64
@@ -8,6 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@@ -29,6 +31,8 @@ type password struct {
|
||||
hash []byte
|
||||
}
|
||||
|
||||
var psql = sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
|
||||
func (p *password) Set(password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -42,22 +46,141 @@ func (p *password) Set(password string) error {
|
||||
|
||||
type UserStore struct {
|
||||
db *sql.DB
|
||||
|
||||
getByIDStmt *sql.Stmt
|
||||
getByEmailStmt *sql.Stmt
|
||||
getByInvitationStmt *sql.Stmt
|
||||
updateStmt *sql.Stmt
|
||||
deleteStmt *sql.Stmt
|
||||
inviteCreateStmt *sql.Stmt
|
||||
invitationDeleteStmt *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.updateStmt, err = db.Prepare(`
|
||||
UPDATE users
|
||||
SET first_name = $1, last_name = $2, username = $3, email = $4, password = $5
|
||||
WHERE id = $6
|
||||
`)
|
||||
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.inviteCreateStmt, err = db.Prepare(`
|
||||
INSERT INTO invitations (user_id, token, expiry) VALUES ($1, $2, $3)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store.passwordResetRequestCreateStmt, err = db.Prepare(`
|
||||
INSERT INTO password_reset_requests (user_id, token, expiry) VALUES ($1, $2, $3)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) Close() error {
|
||||
var errs []error
|
||||
if err := s.getByIDStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.getByEmailStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.getByInvitationStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.updateStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.deleteStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := s.invitationDeleteStmt.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs[0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserStore) Create(ctx context.Context, tx *sql.Tx, user *User) error {
|
||||
query := `
|
||||
INSERT INTO users (first_name, last_name, username, email, password, role_id)
|
||||
VALUES ($1, $2, $3, $4, $5, (SELECT id FROM roles WHERE name = $6)) RETURNING id, created_at, updated_at
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
role := user.Role.Name
|
||||
if role == "" {
|
||||
role = "user"
|
||||
}
|
||||
|
||||
err := tx.QueryRowContext(
|
||||
query, _, err := psql.Insert("users").
|
||||
Columns("first_name", "last_name", "username", "email", "password", "role_id").
|
||||
Values(user.FirstName, user.LastName, user.Username, user.Email, user.Password.hash, sq.Expr("(SELECT id FROM roles WHERE name = ?)", role)).
|
||||
Suffix("RETURNING id, created_at, updated_at").ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = tx.QueryRowContext(
|
||||
ctx,
|
||||
query,
|
||||
user.FirstName,
|
||||
@@ -81,28 +204,26 @@ func (s *UserStore) Create(ctx context.Context, tx *sql.Tx, user *User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserStore) GetAll(ctx context.Context, isAdmin bool) ([]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)
|
||||
defer cancel()
|
||||
|
||||
user := &User{}
|
||||
err := s.db.QueryRowContext(ctx, query, id).Scan(
|
||||
err := s.getByIDStmt.QueryRowContext(ctx, id).Scan(
|
||||
&user.ID,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Username,
|
||||
&user.Email,
|
||||
&user.Password.hash,
|
||||
&user.IsActive,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
&user.Role.ID,
|
||||
&user.RoleID,
|
||||
&user.Role.Name,
|
||||
&user.Role.Level,
|
||||
&user.Role.Description,
|
||||
@@ -115,25 +236,16 @@ func (s *UserStore) GetByID(ctx context.Context, id int64) (*User, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error) {
|
||||
query := `
|
||||
SELECT users.id, first_name, last_name, username, email, password, created_at, updated_at, roles.*
|
||||
FROM users
|
||||
JOIN roles ON (users.role_id = roles.id)
|
||||
WHERE email = $1
|
||||
AND is_active = true
|
||||
`
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
user := &User{}
|
||||
|
||||
err := s.db.QueryRowContext(ctx, query, email).Scan(
|
||||
err := s.getByEmailStmt.QueryRowContext(ctx, email).Scan(
|
||||
&user.ID,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
@@ -142,7 +254,8 @@ func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
&user.Password.hash,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
&user.Role.ID,
|
||||
&user.IsActive,
|
||||
&user.RoleID,
|
||||
&user.Role.Name,
|
||||
&user.Role.Level,
|
||||
&user.Role.Description,
|
||||
@@ -160,17 +273,11 @@ func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
}
|
||||
|
||||
func (s *UserStore) Update(ctx context.Context, user *User) error {
|
||||
query := `
|
||||
UPDATE users
|
||||
SET first_name = $1, last_name = $2, username = $3, email = $4, password = $5
|
||||
WHERE id = $6
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := s.db.ExecContext(
|
||||
_, err := s.updateStmt.ExecContext(
|
||||
ctx,
|
||||
query,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
@@ -199,10 +306,20 @@ 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) Activate(ctx context.Context, token string) error {
|
||||
return withTx(s.db, ctx, func(tx *sql.Tx) error {
|
||||
// Find the user that this token belongs to
|
||||
user, err := getUserFromInvitation(ctx, tx, token)
|
||||
user, err := s.getUserFromInvitation(ctx, tx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -238,12 +355,18 @@ func (s *UserStore) Delete(ctx context.Context, id int64) error {
|
||||
|
||||
// * Soft delete user
|
||||
func (s *UserStore) DeleteUser(ctx context.Context, id int64) error {
|
||||
query := `UPDATE users SET is_active = false WHERE id = $1`
|
||||
query, _, err := psql.Update("users").
|
||||
Set("is_active", false).
|
||||
Where(sq.Eq{"id": id}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := s.db.ExecContext(ctx, query, id)
|
||||
_, err = s.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -255,22 +378,18 @@ func (p *password) Compare(password string) error {
|
||||
return bcrypt.CompareHashAndPassword(p.hash, []byte(password))
|
||||
}
|
||||
|
||||
func getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User, error) {
|
||||
query := `
|
||||
SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.created_at, u.updated_at, is_active
|
||||
FROM users u
|
||||
JOIN invitations i ON u.id = i.user_id
|
||||
WHERE i.token = $1 AND i.expiry > $2
|
||||
`
|
||||
|
||||
func (s *UserStore) getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User, error) {
|
||||
hashed := sha256.Sum256([]byte(token))
|
||||
hashedToken := hex.EncodeToString(hashed[:])
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := tx.Stmt(s.getByInvitationStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
user := &User{}
|
||||
err := tx.QueryRowContext(ctx, query, hashedToken, time.Now()).Scan(
|
||||
err := stmt.QueryRowContext(ctx, hashedToken, time.Now()).Scan(
|
||||
&user.ID,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
@@ -293,14 +412,28 @@ func getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User
|
||||
}
|
||||
|
||||
func (s *UserStore) createUserInvitation(ctx context.Context, tx *sql.Tx, userID int64, exp time.Duration, token string) error {
|
||||
query := `
|
||||
INSERT INTO invitations (user_id, token, expiry)
|
||||
VALUES ($1, $2, $3)
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := tx.ExecContext(ctx, query, userID, token, time.Now().Add(exp))
|
||||
stmt := tx.Stmt(s.inviteCreateStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(ctx, userID, token, time.Now().Add(exp))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserStore) createPasswordResetRequest(ctx context.Context, tx *sql.Tx, userID int64, token string, expiry time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := tx.Stmt(s.passwordResetRequestCreateStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(ctx, userID, token, time.Now().Add(expiry))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -309,17 +442,14 @@ func (s *UserStore) createUserInvitation(ctx context.Context, tx *sql.Tx, userID
|
||||
}
|
||||
|
||||
func (s *UserStore) update(ctx context.Context, tx *sql.Tx, user *User) error {
|
||||
query := `
|
||||
UPDATE users
|
||||
SET first_name = $1, last_name = $2, username = $3, email = $4, is_active = $5
|
||||
WHERE id = $6
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := tx.ExecContext(
|
||||
stmt := tx.Stmt(s.updateStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(
|
||||
ctx,
|
||||
query,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
@@ -335,12 +465,13 @@ func (s *UserStore) update(ctx context.Context, tx *sql.Tx, user *User) error {
|
||||
}
|
||||
|
||||
func (s *UserStore) deleteUserInvitation(ctx context.Context, tx *sql.Tx, userID int64) error {
|
||||
query := `DELETE FROM invitations WHERE user_id = $1`
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := tx.ExecContext(ctx, query, userID)
|
||||
stmt := tx.Stmt(s.invitationDeleteStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -349,12 +480,13 @@ func (s *UserStore) deleteUserInvitation(ctx context.Context, tx *sql.Tx, userID
|
||||
}
|
||||
|
||||
func (s *UserStore) delete(ctx context.Context, tx *sql.Tx, id int64) error {
|
||||
query := `DELETE FROM users WHERE id = $1`
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := tx.ExecContext(ctx, query, id)
|
||||
stmt := tx.Stmt(s.deleteStmt)
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.ExecContext(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user