Add invite user feature for admin
This commit is contained in:
@@ -10,5 +10,3 @@ image-folder-path = "/tmp"
|
||||
|
||||
# The port to listen on for the server.
|
||||
port = "8080"
|
||||
|
||||
book-description-from-babelio = true
|
||||
|
||||
78
internal/apitest/post_inviteuser_test.go
Normal file
78
internal/apitest/post_inviteuser_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInviteUserHandler_OK(t *testing.T) {
|
||||
userJson := `{
|
||||
"username": "ferdinand"
|
||||
}`
|
||||
status, token := testInviteUserHandler(t, userJson)
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestInviteUserHandler_UsernameMissing(t *testing.T) {
|
||||
userJson := `{
|
||||
"gssgg": "d"
|
||||
}`
|
||||
status, _ := testInviteUserHandler(t, userJson)
|
||||
assert.Equal(t, http.StatusBadRequest, status)
|
||||
}
|
||||
|
||||
func TestInviteUserHandler_UsernameTooLong(t *testing.T) {
|
||||
userJson := `{
|
||||
"username": "thisusernameistoolong"
|
||||
}`
|
||||
status, _ := testInviteUserHandler(t, userJson)
|
||||
assert.Equal(t, http.StatusBadRequest, status)
|
||||
}
|
||||
|
||||
func TestInviteUserHandler_Forbidden(t *testing.T) {
|
||||
userJson := `{
|
||||
"username": "ferdinand"
|
||||
}`
|
||||
router := testutils.TestSetup()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
token := testutils.ConnectDemoUser(router)
|
||||
req, _ := http.NewRequest("POST", "/ws/admin/inviteuser",
|
||||
strings.NewReader(userJson))
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func testInviteUserHandler(t *testing.T, userJson string) (int, string) {
|
||||
router := testutils.TestSetup()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
token := testutils.ConnectAdminUser(router)
|
||||
req, _ := http.NewRequest("POST", "/ws/admin/inviteuser",
|
||||
strings.NewReader(userJson))
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
return w.Code, ""
|
||||
}
|
||||
var parsed struct {
|
||||
ID uint `json:"id"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(w.Body.Bytes(), &parsed)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return w.Code, parsed.Token
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package createuser
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -14,6 +16,31 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func CreateUserToActivate(ac appcontext.AppContext, username string) (model.User, error) {
|
||||
token := generateRandomToken()
|
||||
user := model.User{
|
||||
Name: username,
|
||||
Admin: false,
|
||||
InviteToken: token,
|
||||
Activated: false,
|
||||
}
|
||||
err := CreateUserWithHashedPassword(ac, &user)
|
||||
return user, err
|
||||
}
|
||||
|
||||
func generateRandomToken() string {
|
||||
tokenByte := make([]byte, 64)
|
||||
var tokenBuilder strings.Builder
|
||||
rand.Read(tokenByte)
|
||||
encoder := base64.NewEncoder(base64.StdEncoding, &tokenBuilder)
|
||||
encoder.Write(tokenByte)
|
||||
// Must close the encoder when finished to flush any partial blocks.
|
||||
// If you comment out the following line, the last partial block "r"
|
||||
// won't be encoded.
|
||||
encoder.Close()
|
||||
return tokenBuilder.String()
|
||||
}
|
||||
|
||||
// this method will hash the password
|
||||
func CreateUser(ac appcontext.AppContext, username string, password string) error {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
@@ -22,9 +49,10 @@ func CreateUser(ac appcontext.AppContext, username string, password string) erro
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Name: username,
|
||||
Password: string(hashedPassword),
|
||||
Admin: false,
|
||||
Name: username,
|
||||
Password: string(hashedPassword),
|
||||
Admin: false,
|
||||
Activated: true,
|
||||
}
|
||||
return CreateUserWithHashedPassword(ac, &user)
|
||||
}
|
||||
@@ -103,9 +131,10 @@ func createDefaultUsersFromMap(
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Name: username,
|
||||
Password: password,
|
||||
Admin: isAdmin,
|
||||
Name: username,
|
||||
Password: password,
|
||||
Admin: isAdmin,
|
||||
Activated: true,
|
||||
}
|
||||
err = CreateUserWithHashedPassword(ac, &user)
|
||||
if err != nil {
|
||||
|
||||
@@ -70,3 +70,7 @@ type UserSignup struct {
|
||||
Username string `json:"username" binding:"required,min=2,max=20"`
|
||||
Password string `json:"password" binding:"required,min=6,max=100"`
|
||||
}
|
||||
|
||||
type UserToInvite struct {
|
||||
Username string `json:"username" binding:"required,min=2,max=20"`
|
||||
}
|
||||
|
||||
@@ -83,6 +83,13 @@ type UserGet struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Admin bool `json:"admin"`
|
||||
Activated bool `json:"activated"`
|
||||
InviteToken string `json:"invitetoken"`
|
||||
AddedBooksCount int64 `json:"addedbookscount"`
|
||||
UserBooksCount int64 `json:"userbookscount"`
|
||||
}
|
||||
|
||||
type InviteUserPost struct {
|
||||
ID uint `json:"id"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
InvalidCredentials = "Invalid credentials."
|
||||
UserNotActivated = "User is not activated."
|
||||
AuthenticationSuccess = "Authentication was a success."
|
||||
ValidationRequired = "This field is required."
|
||||
ValidationTooShort = "This field is too short. It should be at least %s characters."
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
InvalidCredentials = "Identifiants invalides."
|
||||
UserNotActivated = "L'utilisateur n'est pas activé."
|
||||
AuthenticationSuccess = "Connexion réussie."
|
||||
ValidationRequired = "Ce champ est requis."
|
||||
ValidationTooShort = "Ce champ est trop court. Il devrait contenir au moins %s caractères."
|
||||
|
||||
@@ -4,7 +4,9 @@ import "gorm.io/gorm"
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"index;uniqueIndex"`
|
||||
Password string
|
||||
Admin bool
|
||||
Name string `gorm:"index;uniqueIndex"`
|
||||
Password string
|
||||
Admin bool
|
||||
InviteToken string
|
||||
Activated bool
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func FetchAllUsersCount(db *gorm.DB) (int64, error) {
|
||||
|
||||
func fetchAllUserQuery(db *gorm.DB) *gorm.DB {
|
||||
query := db.Model(&model.User{})
|
||||
query = query.Select("users.id, users.name, users.admin, count(distinct books.id) as added_books_count, count(distinct user_books.id) as user_books_count")
|
||||
query = query.Select("users.id, users.name, users.admin, users.activated, users.invite_token, count(distinct books.id) as added_books_count, count(distinct user_books.id) as user_books_count")
|
||||
query = query.Joins("left join books on (books.added_by_id = users.id)")
|
||||
query = query.Joins("left join user_books on user_books.user_id = users.id")
|
||||
query = query.Group("users.id, users.name")
|
||||
|
||||
30
internal/routes/inviteuserpost.go
Normal file
30
internal/routes/inviteuserpost.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||
"git.artlef.fr/bibliomane/internal/createuser"
|
||||
"git.artlef.fr/bibliomane/internal/dto"
|
||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||
)
|
||||
|
||||
func InviteUserHandler(ac appcontext.AppContext) {
|
||||
var user dto.UserToInvite
|
||||
|
||||
err := ac.C.ShouldBindJSON(&user)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
userDb, err := createuser.CreateUserToActivate(ac, user.Username)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
ac.C.JSON(http.StatusOK, dto.InviteUserPost{
|
||||
ID: userDb.ID,
|
||||
Token: userDb.InviteToken,
|
||||
})
|
||||
}
|
||||
@@ -35,6 +35,11 @@ func PostLoginHandler(ac appcontext.AppContext) {
|
||||
gin.H{"error": i18nresource.GetTranslatedMessage(&ac, "InvalidCredentials")})
|
||||
return
|
||||
}
|
||||
if !userDb.Activated {
|
||||
ac.C.JSON(http.StatusUnauthorized,
|
||||
gin.H{"error": i18nresource.GetTranslatedMessage(&ac, "UserNotActivated")})
|
||||
return
|
||||
}
|
||||
username = user.Username
|
||||
admin = userDb.Admin
|
||||
} else {
|
||||
|
||||
@@ -111,6 +111,9 @@ func Setup(config *config.Config) *gin.Engine {
|
||||
ws.GET("/admin/users", func(c *gin.Context) {
|
||||
routes.GetUsersHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
ws.POST("/admin/inviteuser", func(c *gin.Context) {
|
||||
routes.InviteUserHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
|
||||
r.Static("/static/bookcover", config.ImageFolderPath)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user