Browse Source

Update vue project with new styles, add users views

Add user seeder
Add frontend validation with vee-validate
pull/3/head
Tovi Jaeschke-Rogers 3 years ago
parent
commit
39cbc5f476
21 changed files with 1408 additions and 367 deletions
  1. +8
    -8
      Api/JsonSerialization/DeserializeUserJson.go
  2. +1
    -1
      Api/Routes.go
  3. +11
    -2
      Api/Users.go
  4. +8
    -0
      Database/Seeder/Seed.go
  5. +97
    -0
      Database/Seeder/UserSeeder.go
  6. +24
    -9
      Database/Users.go
  7. +409
    -54
      Frontend/vue/package-lock.json
  8. +5
    -0
      Frontend/vue/package.json
  9. +116
    -0
      Frontend/vue/src/assets/css/admin.css
  10. +0
    -55
      Frontend/vue/src/components/admin/AdminList.vue
  11. +86
    -88
      Frontend/vue/src/components/admin/AdminLogin.vue
  12. +8
    -3
      Frontend/vue/src/components/admin/AdminNavbar.vue
  13. +152
    -127
      Frontend/vue/src/components/admin/AdminSignup.vue
  14. +253
    -0
      Frontend/vue/src/components/admin/users/AdminUsersForm.vue
  15. +149
    -15
      Frontend/vue/src/components/admin/users/AdminUsersList.vue
  16. +16
    -0
      Frontend/vue/src/main.js
  17. +11
    -2
      Frontend/vue/src/router/index.js
  18. +4
    -2
      Frontend/vue/src/utils/http/index.js
  19. +11
    -0
      Models/Users.go
  20. +21
    -0
      Util/Strings.go
  21. +18
    -1
      main.go

+ 8
- 8
Api/JsonSerialization/DeserializeUserJson.go View File

@ -13,7 +13,7 @@ import (
func DeserializeUser(data []byte, allowMissing []string, allowAllMissing bool) (Models.User, error) {
var (
postData Models.User = Models.User{}
userData Models.User = Models.User{}
jsonStructureTest map[string]interface{} = make(map[string]interface{})
jsonStructureTestResults *schema.CompareResults
field schema.FieldMissing
@ -26,18 +26,18 @@ func DeserializeUser(data []byte, allowMissing []string, allowAllMissing bool) (
// Verify the JSON has the correct structure
json.Unmarshal(data, &jsonStructureTest)
jsonStructureTestResults, err = schema.CompareMapToStruct(
&postData,
&userData,
jsonStructureTest,
&schema.CompareOpts{
ConvertibleFunc: CanConvert,
TypeNameFunc: schema.DetailedTypeName,
})
if err != nil {
return postData, err
return userData, err
}
if len(jsonStructureTestResults.MismatchedFields) > 0 {
return postData, errors.New(fmt.Sprintf(
return userData, errors.New(fmt.Sprintf(
"MismatchedFields found when deserializing data: %s",
jsonStructureTestResults.Errors().Error(),
))
@ -60,17 +60,17 @@ func DeserializeUser(data []byte, allowMissing []string, allowAllMissing bool) (
missingFields = append(missingFields, field.String())
}
return postData, errors.New(fmt.Sprintf(
return userData, errors.New(fmt.Sprintf(
"MissingFields found when deserializing data: %s",
strings.Join(missingFields, ", "),
))
}
// Deserialize the JSON into the struct
err = json.Unmarshal(data, &postData)
err = json.Unmarshal(data, &userData)
if err != nil {
return postData, err
return userData, err
}
return postData, err
return userData, err
}

+ 1
- 1
Api/Routes.go View File

@ -33,7 +33,7 @@ func InitApiEndpoints(router *mux.Router) {
api.HandleFunc("/admin/user", getUsers).Methods("GET")
api.HandleFunc("/admin/user", createUser).Methods("POST")
api.HandleFunc("/admin/user/{userID}", getUser).Methods("GET")
api.HandleFunc("/admin/user/{userID}", updatePost).Methods("PUT")
api.HandleFunc("/admin/user/{userID}", updateUser).Methods("PUT")
api.HandleFunc("/admin/user/{userID}", deletePost).Methods("DELETE")
api.HandleFunc("/admin/user/{userID}/update-password", Auth.UpdatePassword).Methods("PUT")


+ 11
- 2
Api/Users.go View File

@ -21,6 +21,7 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
returnJson []byte
values url.Values
page, pageSize int
search string
err error
)
@ -39,19 +40,27 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
return
}
page, err = strconv.Atoi(values.Get("pageSize"))
pageSize, err = strconv.Atoi(values.Get("pageSize"))
if err != nil {
log.Println("Could not parse pageSize url argument")
Util.JsonReturn(w, 500, "An error occured")
return
}
users, err = Database.GetUsers(page, pageSize)
search = values.Get("search")
users, err = Database.GetUsers(page, pageSize, search)
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
if len(users) == 0 {
Util.JsonReturn(w, 404, "No more data")
return
}
returnJson, err = json.MarshalIndent(users, "", " ")
if err != nil {
Util.JsonReturn(w, 500, "An error occured")


+ 8
- 0
Database/Seeder/Seed.go View File

@ -0,0 +1,8 @@
package Seeder
import "log"
func Seed() {
log.Println("Seeding users...")
SeedUsers()
}

+ 97
- 0
Database/Seeder/UserSeeder.go View File

@ -0,0 +1,97 @@
package Seeder
import (
"fmt"
"math/rand"
"time"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/Auth"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
)
var (
firstNames = []string{
"John",
"Mark",
"Annie",
"Hannah",
"Shane",
"Joe",
"Katara",
"Zuko",
"Aang",
"Sokka",
}
lastNames = []string{
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Lopez",
}
)
func randName(last bool) string {
var (
choices []string
)
choices = firstNames
if last {
choices = lastNames
}
return choices[rand.Intn(len(choices))]
}
func createUser() (Models.User, error) {
var (
userData Models.User
now time.Time
firstName, lastName string
email, password string
err error
)
now = time.Now()
firstName = randName(false)
lastName = randName(true)
email = fmt.Sprintf("%s%s+%s@email.com", firstName, lastName, Util.RandomString(4))
password, err = Auth.HashPassword("password")
if err != nil {
return Models.User{}, err
}
userData = Models.User{
Email: email,
Password: password,
LastLogin: &now,
FirstName: firstName,
LastName: lastName,
}
err = Database.CreateUser(&userData)
return userData, err
}
func SeedUsers() {
var (
i int
err error
)
for i = 0; i <= 20; i++ {
_, err = createUser()
if err != nil {
panic(err)
}
}
}

+ 24
- 9
Database/Users.go View File

@ -2,6 +2,7 @@ package Database
import (
"errors"
"fmt"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
@ -37,11 +38,13 @@ func GetUserByEmail(email string) (Models.User, error) {
return userData, err
}
func GetUsers(page, pageSize int) ([]Models.User, error) {
func GetUsers(page, pageSize int, search string) ([]Models.User, error) {
var (
users []Models.User
i int
err error
users []Models.User
query *gorm.DB
offset int
i int
err error
)
switch {
@ -51,9 +54,22 @@ func GetUsers(page, pageSize int) ([]Models.User, error) {
pageSize = 10
}
err = DB.Model(Models.User{}).
Offset(0).
Limit(10).
offset = page * pageSize
search = fmt.Sprintf("%%%s%%", search)
query = DB.Model(Models.User{}).
Offset(offset).
Limit(pageSize).
Order("created_at desc")
if search != "" {
query = query.
Where("CONCAT_WS(' ', first_name, last_name) LIKE ?", search).
Or("email LIKE ?", search)
}
err = query.
Find(&users).
Error
@ -105,8 +121,7 @@ func UpdateUser(id string, userData *Models.User) error {
var (
err error
)
err = DB.Model(&Models.User{}).
Select("*").
err = DB.Model(&userData).
Omit("id", "created_at", "updated_at", "deleted_at").
Where("id = ?", id).
Updates(userData).


+ 409
- 54
Frontend/vue/package-lock.json View File

@ -8,12 +8,17 @@
"name": "vue",
"version": "0.1.0",
"dependencies": {
"@meforma/vue-toaster": "^1.3.0",
"@vee-validate/rules": "^4.5.10",
"@vuepic/vue-datepicker": "^3.0.0",
"axios": "^0.26.1",
"bootstrap": "^5.1.3",
"core-js": "^3.8.3",
"vee-validate": "^4.5.10",
"vue": "^3.2.13",
"vue-axios": "^3.4.1",
"vue-router": "^4.0.13",
"vue-toastification": "^2.0.0-rc.5",
"vue3-cookies": "^1.0.6",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0"
@ -1798,6 +1803,15 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@meforma/vue-toaster": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@meforma/vue-toaster/-/vue-toaster-1.3.0.tgz",
"integrity": "sha512-jH0zOA/jTiT+UKHO9n5hjPTLkIfg7d66X4fnd7ssIbcXpZOoe+J8IY6Kf3nRW5iVD6/tkjeyp+tjVK8zk6zASg==",
"dependencies": {
"stylus": "~0.54.8",
"stylus-loader": "~3.0.2"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2163,6 +2177,11 @@
"@types/node": "*"
}
},
"node_modules/@vee-validate/rules": {
"version": "4.5.10",
"resolved": "https://registry.npmjs.org/@vee-validate/rules/-/rules-4.5.10.tgz",
"integrity": "sha512-8kFsXvCzP1JHuyzXyH7cIhLQOJnba6ySG5GzaKnWGIJnbT1itNJ6dACdedjW3bWWpYiOkxi76Nqj14UL0ukMFQ=="
},
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
@ -2894,6 +2913,20 @@
"integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==",
"dev": true
},
"node_modules/@vuepic/vue-datepicker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-3.0.0.tgz",
"integrity": "sha512-265f0/g57GHchKAfPJYTSTQwdJjKuZCWsBvqvVyLelnnTNQqb/xWvps2/yh0Vqk+L97Vd7aKvVZUS9ZvSxI5dg==",
"dependencies": {
"date-fns": "^2.28.0"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"vue": ">=3.2.0"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@ -3331,6 +3364,17 @@
"node": ">= 4.0.0"
}
},
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/autoprefixer": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz",
@ -3436,8 +3480,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-js": {
"version": "1.5.1",
@ -3469,7 +3512,6 @@
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"dev": true,
"engines": {
"node": "*"
}
@ -3581,7 +3623,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -4102,8 +4143,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"node_modules/connect-history-api-fallback": {
"version": "1.6.0",
@ -4313,6 +4353,17 @@
"semver": "bin/semver"
}
},
"node_modules/css": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
"dependencies": {
"inherits": "^2.0.3",
"source-map": "^0.6.1",
"source-map-resolve": "^0.5.2",
"urix": "^0.1.0"
}
},
"node_modules/css-declaration-sorter": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.4.tgz",
@ -4469,6 +4520,14 @@
"node": ">=0.10.0"
}
},
"node_modules/css-parse": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
"integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
"dependencies": {
"css": "^2.0.0"
}
},
"node_modules/css-select": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz",
@ -4519,6 +4578,14 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -4624,6 +4691,18 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
"integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
},
"node_modules/date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
@ -4641,6 +4720,14 @@
}
}
},
"node_modules/decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"engines": {
"node": ">=0.10"
}
},
"node_modules/deep-equal": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
@ -5051,7 +5138,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true,
"engines": {
"node": ">= 4"
}
@ -6070,8 +6156,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"node_modules/fsevents": {
"version": "2.3.2",
@ -6147,7 +6232,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -6595,7 +6679,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@ -6604,8 +6687,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ip": {
"version": "1.1.5",
@ -7117,7 +7199,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
@ -7131,7 +7212,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
},
@ -7157,6 +7237,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -7663,7 +7748,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -7674,8 +7758,7 @@
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"node_modules/minipass": {
"version": "3.1.6",
@ -8007,7 +8090,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@ -8329,7 +8411,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -9381,6 +9462,12 @@
"node": ">=4"
}
},
"node_modules/resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"deprecated": "https://github.com/lydell/resolve-url#deprecated"
},
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@ -9460,8 +9547,12 @@
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/schema-utils": {
"version": "2.7.1",
@ -9503,7 +9594,6 @@
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@ -9794,6 +9884,19 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
"integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
"deprecated": "See https://github.com/lydell/source-map-resolve#deprecated",
"dependencies": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0",
"resolve-url": "^0.2.1",
"source-map-url": "^0.4.0",
"urix": "^0.1.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@ -9813,6 +9916,12 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-url": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"deprecated": "See https://github.com/lydell/source-map-url#deprecated"
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
@ -10029,6 +10138,72 @@
"postcss": "^8.2.15"
}
},
"node_modules/stylus": {
"version": "0.54.8",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz",
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==",
"dependencies": {
"css-parse": "~2.0.0",
"debug": "~3.1.0",
"glob": "^7.1.6",
"mkdirp": "~1.0.4",
"safer-buffer": "^2.1.2",
"sax": "~1.2.4",
"semver": "^6.3.0",
"source-map": "^0.7.3"
},
"bin": {
"stylus": "bin/stylus"
},
"engines": {
"node": "*"
}
},
"node_modules/stylus-loader": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz",
"integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==",
"dependencies": {
"loader-utils": "^1.0.2",
"lodash.clonedeep": "^4.5.0",
"when": "~3.6.x"
},
"peerDependencies": {
"stylus": ">=0.52.4"
}
},
"node_modules/stylus/node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/stylus/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/stylus/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/stylus/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"engines": {
"node": ">= 8"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -10476,6 +10651,12 @@
"punycode": "^2.1.0"
}
},
"node_modules/urix": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"deprecated": "Please see https://github.com/lydell/urix#deprecated"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -10531,6 +10712,17 @@
"node": ">= 0.8"
}
},
"node_modules/vee-validate": {
"version": "4.5.10",
"resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.5.10.tgz",
"integrity": "sha512-7dZE0PZTNY3Ztp6Gz8iw+QS7Fz59vU1qkD0rBJkldkf9Faw2lFjWgE0tiCist5RQkLgt7/HaVbojzb/SCdutfA==",
"dependencies": {
"@vue/devtools-api": "^6.0.0-beta.15"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue": {
"version": "3.2.31",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz",
@ -10776,6 +10968,14 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"node_modules/vue-toastification": {
"version": "2.0.0-rc.5",
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
"integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
"peerDependencies": {
"vue": "^3.0.2"
}
},
"node_modules/vue3-cookies": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/vue3-cookies/-/vue3-cookies-1.0.6.tgz",
@ -11329,6 +11529,11 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/when": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz",
"integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404="
},
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -11409,8 +11614,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"node_modules/ws": {
"version": "7.5.7",
@ -12804,6 +13008,15 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@meforma/vue-toaster": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@meforma/vue-toaster/-/vue-toaster-1.3.0.tgz",
"integrity": "sha512-jH0zOA/jTiT+UKHO9n5hjPTLkIfg7d66X4fnd7ssIbcXpZOoe+J8IY6Kf3nRW5iVD6/tkjeyp+tjVK8zk6zASg==",
"requires": {
"stylus": "~0.54.8",
"stylus-loader": "~3.0.2"
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -13131,6 +13344,11 @@
"@types/node": "*"
}
},
"@vee-validate/rules": {
"version": "4.5.10",
"resolved": "https://registry.npmjs.org/@vee-validate/rules/-/rules-4.5.10.tgz",
"integrity": "sha512-8kFsXvCzP1JHuyzXyH7cIhLQOJnba6ySG5GzaKnWGIJnbT1itNJ6dACdedjW3bWWpYiOkxi76Nqj14UL0ukMFQ=="
},
"@vue/babel-helper-vue-jsx-merge-props": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
@ -13716,6 +13934,14 @@
"integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==",
"dev": true
},
"@vuepic/vue-datepicker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-3.0.0.tgz",
"integrity": "sha512-265f0/g57GHchKAfPJYTSTQwdJjKuZCWsBvqvVyLelnnTNQqb/xWvps2/yh0Vqk+L97Vd7aKvVZUS9ZvSxI5dg==",
"requires": {
"date-fns": "^2.28.0"
}
},
"@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@ -14071,6 +14297,11 @@
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"autoprefixer": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz",
@ -14147,8 +14378,7 @@
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"base64-js": {
"version": "1.5.1",
@ -14165,8 +14395,7 @@
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"dev": true
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="
},
"binary-extensions": {
"version": "2.2.0",
@ -14262,7 +14491,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -14655,8 +14883,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"connect-history-api-fallback": {
"version": "1.6.0",
@ -14807,6 +15034,24 @@
}
}
},
"css": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
"requires": {
"inherits": "^2.0.3",
"source-map": "^0.6.1",
"source-map-resolve": "^0.5.2",
"urix": "^0.1.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"css-declaration-sorter": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.4.tgz",
@ -14904,6 +15149,14 @@
}
}
},
"css-parse": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
"integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
"requires": {
"css": "^2.0.0"
}
},
"css-select": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz",
@ -15016,6 +15269,11 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
"integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
},
"date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
@ -15025,6 +15283,11 @@
"ms": "2.1.2"
}
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"deep-equal": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
@ -15343,8 +15606,7 @@
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
},
"encodeurl": {
"version": "1.0.2",
@ -16101,8 +16363,7 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "2.3.2",
@ -16159,7 +16420,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -16478,7 +16738,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@ -16487,8 +16746,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ip": {
"version": "1.1.5",
@ -16868,7 +17126,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
@ -16879,7 +17136,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
}
@ -16901,6 +17157,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -17292,7 +17553,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -17300,8 +17560,7 @@
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"minipass": {
"version": "3.1.6",
@ -17552,7 +17811,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
@ -17793,8 +18051,7 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-key": {
"version": "2.0.1",
@ -18517,6 +18774,11 @@
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true
},
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo="
},
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@ -18566,8 +18828,12 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"schema-utils": {
"version": "2.7.1",
@ -18598,8 +18864,7 @@
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"send": {
"version": "0.17.2",
@ -18847,6 +19112,18 @@
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
},
"source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
"integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
"requires": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0",
"resolve-url": "^0.2.1",
"source-map-url": "^0.4.0",
"urix": "^0.1.0"
}
},
"source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@ -18865,6 +19142,11 @@
}
}
},
"source-map-url": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw=="
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
@ -19033,6 +19315,56 @@
"postcss-selector-parser": "^6.0.4"
}
},
"stylus": {
"version": "0.54.8",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz",
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==",
"requires": {
"css-parse": "~2.0.0",
"debug": "~3.1.0",
"glob": "^7.1.6",
"mkdirp": "~1.0.4",
"safer-buffer": "^2.1.2",
"sax": "~1.2.4",
"semver": "^6.3.0",
"source-map": "^0.7.3"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
}
}
},
"stylus-loader": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz",
"integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==",
"requires": {
"loader-utils": "^1.0.2",
"lodash.clonedeep": "^4.5.0",
"when": "~3.6.x"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -19360,6 +19692,11 @@
"punycode": "^2.1.0"
}
},
"urix": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -19406,6 +19743,14 @@
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"dev": true
},
"vee-validate": {
"version": "4.5.10",
"resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.5.10.tgz",
"integrity": "sha512-7dZE0PZTNY3Ztp6Gz8iw+QS7Fz59vU1qkD0rBJkldkf9Faw2lFjWgE0tiCist5RQkLgt7/HaVbojzb/SCdutfA==",
"requires": {
"@vue/devtools-api": "^6.0.0-beta.15"
}
},
"vue": {
"version": "3.2.31",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz",
@ -19594,6 +19939,12 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"vue-toastification": {
"version": "2.0.0-rc.5",
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
"integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
"requires": {}
},
"vue3-cookies": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/vue3-cookies/-/vue3-cookies-1.0.6.tgz",
@ -19999,6 +20350,11 @@
"webidl-conversions": "^3.0.0"
}
},
"when": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz",
"integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404="
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -20060,8 +20416,7 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "7.5.7",


+ 5
- 0
Frontend/vue/package.json View File

@ -9,12 +9,17 @@
"watch": "vue-cli-service build --watch"
},
"dependencies": {
"@meforma/vue-toaster": "^1.3.0",
"@vee-validate/rules": "^4.5.10",
"@vuepic/vue-datepicker": "^3.0.0",
"axios": "^0.26.1",
"bootstrap": "^5.1.3",
"core-js": "^3.8.3",
"vee-validate": "^4.5.10",
"vue": "^3.2.13",
"vue-axios": "^3.4.1",
"vue-router": "^4.0.13",
"vue-toastification": "^2.0.0-rc.5",
"vue3-cookies": "^1.0.6",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0"


+ 116
- 0
Frontend/vue/src/assets/css/admin.css View File

@ -0,0 +1,116 @@
body {
min-height: 100vh;
}
#app, #admin-page-container {
min-height: 100vh;
height: 100%;
}
#admin-page-container {
background-color: #ccc;
}
.background-color {
background-color: #ccc;
}
.card-registration {
border-radius: 1.5rem !important;
}
.card-registration .select-input.form-control[readonly]:not([disabled]) {
font-size: 1rem;
line-height: 2.15;
padding-left: .75em;
padding-right: .75em;
}
.card-registration .select-arrow {
top: 13px;
}
.center-align {
text-align: center;
}
.center-align * {
display: inline-block;
}
.right-align {
text-align: right;
}
.right-align * {
margin-left: 1rem;
}
.page-nav-container {
background-color: #FFF;
border-radius: 1.5rem;
height: 3.4rem;
padding: 0.5rem;
}
.page-nav-container .btn-rounded {
border-radius: 1rem;
}
.page-nav-container input {
border-radius: 1rem;
}
.page-nav-container .input-group-append button {
border-radius: 0 1rem 1rem 0;
}
.float-right {
float: right;
}
table th,
table td {
text-align: center;
}
table td {
text-align: center;
}
input.invalid {
border-color: var(--bs-danger);
}
label[role=alert] {
color: var(--bs-danger);
}
.dp__input.dp__input_icon_pad {
min-height: calc(1.5em + 1rem + 2px);
padding: .5rem 1rem;
padding-left: 35px !important;
font-size: 1.25rem;
border-radius: .3rem;
display: block;
width: 100%;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.dp__input.dp__input_icon_pad:disabled {
background-color: #e9ecef;
opacity: 1;
}

+ 0
- 55
Frontend/vue/src/components/admin/AdminList.vue View File

@ -1,55 +0,0 @@
<template>
<div class="container table-responsive py-5">
<table class="table table-bordered table-hover">
<thead class="thead-dark">
<tr>
<th scope="col" v-for="tableHeader in tableHeaders">{{ tableHeader }}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items">
<td v-for="index in dataIndexes">{{ doThing(item, index) }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
props: {
sourceUrl: String,
listHeaders: {},
},
data() {
return {
tableHeaders: Object.values(this.listHeaders),
dataIndexes: Object.keys(this.listHeaders),
items: {},
}
},
created () {
this.getData()
},
methods: {
async getData () {
try {
const response = await this.axios.get(this.sourceUrl + '?page=0&pageSize=10')
if (response.status) {
this.items = response.data
}
} catch (error) {
console.log(error)
}
},
doThing(x, y) {
return x[y]
},
}
}
</script>

+ 86
- 88
Frontend/vue/src/components/admin/AdminLogin.vue View File

@ -1,107 +1,105 @@
<template>
<section class="vh-100 gradient-custom">
<div class="container py-5 h-100">
<div class="row justify-content-center align-items-center h-100">
<div class="col-12 col-lg-9 col-xl-7">
<div class="card shadow-2-strong card-registration" style="border-radius: 15px;">
<div class="card-body p-4 p-md-5">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Login</h3>
<form @submit.prevent>
<section class="vh-100 background-color">
<div class="container py-5 h-100">
<div class="row justify-content-center align-items-center h-100">
<div class="col-12 col-lg-9 col-xl-7">
<div class="card shadow-2-strong card-registration border-2" style="border-radius: 15px;">
<div class="card-body p-4 p-md-5">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Login</h3>
<Form @submit="login" v-slot="{ meta, errors }">
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="email"
type="email"
id="emailAddress"
name="Email"
class="form-control form-control-lg"
:class="errors['Email'] ? 'invalid' : ''"
rules="required|email"/>
<label v-if="!errors['Email']" class="form-label" for="email">Email</label>
<ErrorMessage name="Email" as="label" class="form-label" for="email"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="password"
type="password"
id="emailAddress"
name="Password"
class="form-control form-control-lg"
:class="errors['Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Password']" class="form-label" for="password">Password</label>
<ErrorMessage name="Password" as="label" class="form-label" for="password"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<input v-model="email" type="email" id="emailAddress" class="form-control form-control-lg" />
<label class="form-label" for="emailAddress">Email</label>
</div>
</div>
</div>
<div class="mt-2 pt-2 center-align">
<button
:disabled="!meta.touched || !meta.valid"
class="btn btn-primary btn-lg"
type="submit"
>
Login
</button>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<input v-model="password" type="password" id="password" class="form-control form-control-lg" />
<label class="form-label" for="password">Password</label>
</div>
<div class="mt-2 pt-2 center-align">
<p style="padding-right: 10px;">Don't have an account? </p><router-link :to='{"name": "AdminSignup"}'>Sign up</router-link>
</div>
</Form>
</div>
</div>
</div>
<div class="mt-2 pt-2 center-align">
<button class="btn btn-primary btn-lg" @click="login">Login</button>
</div>
<div class="mt-2 pt-2 center-align">
<p style="padding-right: 10px;">Don't have an account? </p><router-link :to='{"name": "AdminSignup"}'>Sign up</router-link>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
name: 'AdminLogin',
data() {
return {
email: '',
password: '',
}
},
import { Form, Field, ErrorMessage } from 'vee-validate'
methods: {
async login () {
try {
const response = await this.axios.post(
'/admin/login',
{
email: this.email,
password: this.password,
}
)
export default {
name: 'AdminLogin',
data() {
return {
email: '',
password: '',
}
},
components: {
Form,
Field,
ErrorMessage,
},
if (response.status === 200) {
this.$store.dispatch('setUser', response.data)
this.$router.push({ name: 'AdminUsersList' })
methods: {
async login () {
try {
const response = await this.axios.post(
'/admin/login',
{
email: this.email,
password: this.password,
}
} catch (error) {
console.log(error)
)
if (response.status === 200) {
this.$store.dispatch('setUser', response.data)
this.$router.push({ name: 'AdminUsersList' })
}
} catch (error) {
this.$toast.error('An error occured')
}
}
}
</script>
<style>
.gradient-custom {
/* fallback for old browsers */
background: #f093fb;
/* Chrome 10-25, Safari 5.1-6 */
background: -webkit-linear-gradient(to bottom right, rgba(240, 147, 251, 1), rgba(245, 87, 108, 1));
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
background: linear-gradient(to bottom right, rgba(240, 147, 251, 1), rgba(245, 87, 108, 1))
}
.card-registration .select-input.form-control[readonly]:not([disabled]) {
font-size: 1rem;
line-height: 2.15;
padding-left: .75em;
padding-right: .75em;
}
.card-registration .select-arrow {
top: 13px;
}
.center-align {
text-align: center;
}
.center-align * {
display: inline-block;
}
</style>
</script>

+ 8
- 3
Frontend/vue/src/components/admin/AdminNavbar.vue View File

@ -1,6 +1,6 @@
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<div class="container-fluid px-5">
<!-- TODO: Replace with logo -->
<router-link
@ -24,7 +24,7 @@
aria-current="page"
>
<i class="bi bi-house-fill me-2"></i>
Home
Users
</router-link>
</li>
@ -33,7 +33,12 @@
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li>
<router-link to="#" class="dropdown-item">Account</router-link>
<router-link
:to="{ name: 'AdminUsersForm', params: { id: $store.getters.getUser.id } }"
class="dropdown-item"
>
Account
</router-link>
</li>
<li>
<hr class="dropdown-divider">


+ 152
- 127
Frontend/vue/src/components/admin/AdminSignup.vue View File

@ -1,141 +1,166 @@
<template>
<section class="vh-100 gradient-custom">
<div class="container py-5 h-100">
<div class="row justify-content-center align-items-center h-100">
<div class="col-12 col-lg-9 col-xl-7">
<div class="card shadow-2-strong card-registration" style="border-radius: 15px;">
<div class="card-body p-4 p-md-5">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Sign Up</h3>
<form @submit.prevent>
<div class="row">
<div class="col-md-6 mb-4">
<div class="form-outline">
<input v-model="first_name" type="text" id="firstName" class="form-control form-control-lg" />
<label class="form-label" for="firstName">First Name</label>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="form-outline">
<input v-model="last_name" type="text" id="lastName" class="form-control form-control-lg" />
<label class="form-label" for="lastName">Last Name</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<input v-model="email" type="email" id="emailAddress" class="form-control form-control-lg" />
<label class="form-label" for="emailAddress">Email</label>
</div>
<section class="vh-100 background-color">
<div class="container py-5 h-100">
<div class="row justify-content-center align-items-center h-100">
<div class="col-12 col-lg-9 col-xl-7">
<div class="card shadow-2-strong card-registration border-2" style="border-radius: 15px;">
<div class="card-body p-4 p-md-5">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Sign Up</h3>
<Form @submit="signup" v-slot="{ errors, meta }">
<div class="row">
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="first_name"
type="text"
id="firstName"
name="First Name"
class="form-control form-control-lg"
:class="errors['First Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['First Name']" class="form-label" for="firstName">First Name</label>
<ErrorMessage name="First Name" as="label" class="form-label" for="firstName"/>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="last_name"
type="text"
id="lastName"
name="Last Name"
class="form-control form-control-lg"
:class="errors['Last Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['Last Name']" class="form-label" for="lastName">Last Name</label>
<ErrorMessage name="Last Name" as="label" class="form-label" for="lastName"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="email"
type="text"
id="email"
name="Email"
class="form-control form-control-lg"
:class="errors['Email'] ? 'invalid' : ''"
rules="required|email"/>
<label v-if="!errors['Email']" class="form-label" for="email">Email</label>
<ErrorMessage name="Email" as="label" class="form-label" for="email"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="password"
type="password"
id="password"
name="Password"
class="form-control form-control-lg"
:class="errors['Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Password']" class="form-label" for="password">Password</label>
<ErrorMessage name="Password" as="label" class="form-label" for="password"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="confirm_password"
type="password"
id="confirm_password"
name="Confirm Password"
class="form-control form-control-lg"
:class="errors['Confirm Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Confirm Password']" class="form-label" for="password">Confirm Password</label>
<ErrorMessage name="Confirm Password" as="label" class="form-label" for="password"/>
</div>
</div>
</div>
<div class="mt-2 pt-2 center-align">
<button
:disabled="!meta.touched || !meta.valid"
class="btn btn-primary btn-lg"
type="submit"
>
Sign Up
</button>
</div>
<div class="mt-2 pt-2 center-align">
<p style="padding-right: 10px;">Already have an account? </p><router-link :to='{"name": "AdminLogin"}'>Login</router-link>
</div>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<input v-model="password" type="password" id="password" class="form-control form-control-lg" />
<label class="form-label" for="password">Password</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<input v-model="confirm_password" type="password" id="confirmPassword" class="form-control form-control-lg" />
<label class="form-label" for="confirmPassword">ConfirmPassword</label>
</div>
</div>
</div>
<div class="mt-2 pt-2 center-align">
<button class="btn btn-primary btn-lg" @click="signup">Sign Up</button>
</div>
<div class="mt-2 pt-2 center-align">
<p style="padding-right: 10px;">Already have an account? </p><router-link :to='{"name": "AdminLogin"}'>Login</router-link>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
import '@/assets/css/admin.css'
export default {
name: 'AdminSignup',
data() {
return {
first_name: '',
last_name: '',
email: '',
password: '',
confirm_password: ''
}
},
methods: {
async signup () {
try {
const response = await this.axios.post(
'/api/v1/admin/user',
{
first_name: this.first_name,
last_name: this.last_name,
email: this.email,
password: this.password,
confirm_password: this.confirm_password,
}
)
if (response.status === 200) {
this.$router.push({ name: 'AdminLogin' })
import { Form, Field, ErrorMessage } from 'vee-validate'
export default {
name: 'AdminSignup',
data() {
return {
first_name: '',
last_name: '',
email: '',
password: '',
confirm_password: ''
}
},
components: {
Form,
Field,
ErrorMessage,
},
methods: {
async signup () {
try {
const response = await this.axios.post(
'/admin/user',
{
first_name: this.first_name,
last_name: this.last_name,
email: this.email,
password: this.password,
confirm_password: this.confirm_password,
}
} catch (error) {
console.log(error)
)
if (response.status === 200) {
this.$router.push({ name: 'AdminLogin' })
}
} catch (error) {
if (error.response.data.message === 'invalid_email') {
this.$toast.error('Email already exists.')
return
}
this.$toast.error('An error occured.')
}
}
}
</script>
<style>
.gradient-custom {
/* fallback for old browsers */
background: #f093fb;
/* Chrome 10-25, Safari 5.1-6 */
background: -webkit-linear-gradient(to bottom right, rgba(240, 147, 251, 1), rgba(245, 87, 108, 1));
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
background: linear-gradient(to bottom right, rgba(240, 147, 251, 1), rgba(245, 87, 108, 1))
}
.card-registration .select-input.form-control[readonly]:not([disabled]) {
font-size: 1rem;
line-height: 2.15;
padding-left: .75em;
padding-right: .75em;
}
.card-registration .select-arrow {
top: 13px;
}
.center-align {
text-align: center;
}
.center-align * {
display: inline-block;
}
</style>
</script>

+ 253
- 0
Frontend/vue/src/components/admin/users/AdminUsersForm.vue View File

@ -0,0 +1,253 @@
<template>
<div id="admin-page-container">
<admin-navbar/>
<section class="container mt-5">
<div class="row mb-3">
<div class="col-12">
<div class="page-nav-container">
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-rounded"
:class="tab === 'details' ? 'btn-dark' : 'btn-outline-dark'"
@click="tab = 'details'"
>
User Details
</button>
<button
type="button"
class="btn btn-rounded"
:class="tab === 'change_password' ? 'btn-dark' : 'btn-outline-dark'"
@click="tab = 'change_password'"
>
Change Password
</button>
</div>
</div>
</div>
</div>
<div class="card shadow-2-strong card-registration">
<div class="card-body p-4 p-md-5" v-if="tab === 'details'">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Update User</h3>
<Form @submit="updateUser" v-slot="{ meta, errors }">
<div class="row">
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="user.first_name"
type="text"
id="firstName"
name="First Name"
class="form-control form-control-lg"
:class="errors['First Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['First Name']" class="form-label" for="firstName">First Name</label>
<ErrorMessage name="First Name" as="label" class="form-label" for="firstName"/>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="user.last_name"
type="text"
id="lastName"
name="Last Name"
class="form-control form-control-lg"
:class="errors['Last Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['Last Name']" class="form-label" for="lastName">Last Name</label>
<ErrorMessage name="Last Name" as="label" class="form-label" for="lastName"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="user.email"
type="email"
id="email"
name="Email"
class="form-control form-control-lg"
:class="errors['Email'] ? 'invalid' : ''"
rules="required|email"/>
<label v-if="!errors['Email']" class="form-label" for="email">Email</label>
<ErrorMessage name="Email" as="label" class="form-label" for="email"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4 pb-2">
<div class="form-outline">
<date-picker
v-model="user.last_login"
format="dd/MM/yyyy, HH:mm"
disabled="disabled"
id="last_login"
:month-year-component="monthYear"/>
<label class="form-label" for="last_login">Last Login</label>
</div>
</div>
</div>
<div class="mt-2 pt-2 right-align">
<button class="btn btn-danger btn-md" type="button">
Delete
</button>
<button :disabled="!meta.touched || !meta.valid" class="btn btn-primary btn-md" type="submit">
Update
</button>
</div>
</Form>
</div>
<div class="card-body p-4 p-md-5" v-if="tab === 'change_password'">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Change Password</h3>
<Form @submit="updatePassword" v-slot="{ meta, errors }">
<div class="row">
<div class="col-12 col-md-6 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="password.password"
type="password"
id="password"
name="Password"
class="form-control form-control-lg"
:class="errors['Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Password']" class="form-label" for="password">Password</label>
<ErrorMessage name="Password" as="label" class="form-label" for="email"/>
</div>
</div>
<div class="col-12 col-md-6 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="password.confirm_password"
type="password"
id="confirm_password"
name="Confirm Password"
class="form-control form-control-lg"
:class="errors['Confirm Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Confirm Password']" class="form-label" for="confirm_password">Confirm Password</label>
<ErrorMessage name="Confirm Password" as="label" class="form-label" for="confirm_password"/>
</div>
</div>
</div>
<div class="mt-2 pt-2 right-align">
<button
type="submit"
:disabled="!meta.touched || !meta.valid"
class="btn btn-primary btn-md"
>
Update Password
</button>
</div>
</Form>
</div>
</div>
</section>
</div>
</template>
<script>
import AdminNavbar from '@/components/admin/AdminNavbar'
import { Form, Field, ErrorMessage } from 'vee-validate'
export default {
data() {
return {
tab: 'details',
user: {
first_name: null,
last_name: null,
email: null,
last_login: null,
},
password: {
password: null,
confirm_password: null,
}
}
},
components: {
AdminNavbar,
Form,
Field,
ErrorMessage,
},
mounted () {
this.getUser()
},
methods: {
setUserFromResponse (response) {
this.user = {
first_name: response.data.first_name,
last_name: response.data.last_name,
email: response.data.email,
last_login: response.data.last_login,
}
},
async getUser () {
try {
const response = await this.axios.get(`/admin/user/${this.$route.params.id}`)
if (response.status === 200) {
this.setUserFromResponse(response)
}
} catch (error) {
console.log(error)
}
},
async updateUser () {
try {
let response = await this.axios.put(
`/admin/user/${this.$route.params.id}`,
this.user,
)
if (response.status === 200) {
this.$toast.success('Successfully updated user details.');
this.setUserFromResponse(response)
}
} catch (error) {
this.$toast.error('An error occured');
}
},
async updatePassword () {
try {
let response = await this.axios.put(
`/admin/user/${this.$route.params.id}/update-password`,
this.password,
)
if (response.status === 200) {
this.$toast.success('Successfully updated user password.');
}
} catch (error) {
this.$toast.error('An error occured');
}
}
}
}
</script>

+ 149
- 15
Frontend/vue/src/components/admin/users/AdminUsersList.vue View File

@ -1,32 +1,166 @@
<template>
<div>
<div id="admin-page-container">
<admin-navbar/>
<admin-list
:listHeaders="listHeaders"
:sourceUrl="sourceUrl"
/>
<div class="container table-responsive mt-5 pb-5">
<div class="row mb-3">
<div class="col-12">
<div class="page-nav-container">
<div class="row">
<div class="col-sm-6 col-9">
<div class="input-group">
<input
type="text"
class="form-control"
placeholder="Search..."
ref="search"
>
<div class="input-group-append">
<button
class="btn btn-dark"
type="button"
@click="searchUsers"
>
Search
</button>
</div>
</div>
</div>
<div class="col-sm-6 float-right col-3">
<div class="btn-group float-right" role="group">
<button
type="button"
class="btn btn-rounded btn-dark"
>
<!-- TODO: Change this to + sign on small screens -->
Add User
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-2-strong card-registration">
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th scope="col">Name</th>
<th scope="col" class="d-none d-sm-table-cell">Email</th>
<th scope="col" class="d-none d-sm-table-cell">Last Login</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td class="align-middle">{{ user.first_name }} {{ user.last_name }}</td>
<td class="align-middle d-none d-sm-table-cell">{{ user.email }}</td>
<td class="align-middle d-none d-sm-table-cell">{{ formatDate(user.last_login) }}</td>
<td class="align-middle">
<router-link
:to="{ name: 'AdminUsersForm', params: { id: user.id } }"
>
<button
class="btn btn-outline-dark"
>
Open
</button>
</router-link>
</td>
</tr>
</tbody>
</table>
<p v-if="dataEnd" class="py-2 center-align text-muted">No more data</p>
</div>
</div>
</div>
</template>
<script>
import AdminNavbar from '@/components/admin/AdminNavbar'
import AdminList from '@/components/admin/AdminList'
export default {
data () {
data() {
return {
sourceUrl: '/admin/user',
listHeaders: {
'first_name': 'First Name',
'last_name': 'Last Name',
'email': 'Email',
'last_login': 'Last Login',
}
users: {},
pageSize: 15,
page: 0,
search: '',
dataEnd: false,
}
},
components: {
AdminNavbar,
AdminList,
},
beforeMount () {
this.getInitialUsers()
},
mounted () {
this.getNextUsers()
},
methods: {
formatDate (dateString) {
const d = new Date(dateString)
let hours = d.getHours();
let minutes = d.getMinutes();
const ampm = hours >= 12 ? 'pm' : 'am';
hours = hours % 12;
hours = hours ? hours : 12; // the hour '0' should be '12'
minutes = minutes < 10 ? '0'+minutes : minutes;
const strTime = hours + ':' + minutes + ' ' + ampm;
return d.getDate() + "/" + (d.getMonth()+1) + "/" + d.getFullYear() + " " + strTime;
},
async getInitialUsers () {
try {
const response = await this.axios.get(
`/admin/user?page=${this.page}&pageSize=${this.pageSize}&search=${this.search}`
)
if (response.status === 200) {
this.users = response.data
}
} catch (error) {
if (error.response.status === 404) {
this.users = {}
this.dataEnd = true
}
}
},
async getNextUsers () {
window.onscroll = async () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight;
if (bottomOfWindow) {
try {
this.page += 1
const response = await this.axios.get(
`/admin/user?page=${this.page}&pageSize=${this.pageSize}&search=${this.search}`
)
if (response.status === 200) {
this.users.push(...response.data)
}
} catch (error) {
if (error.response.status === 404) {
this.dataEnd = true
}
}
}
}
},
searchUsers () {
this.search = this.$refs.search.value
this.getInitialUsers()
}
}
}
</script>

+ 16
- 0
Frontend/vue/src/main.js View File

@ -2,6 +2,11 @@ import { createApp } from 'vue'
import axios from './utils/http'
import VueAxios from 'vue-axios'
import VueCookies from "vue3-cookies";
import { defineRule } from 'vee-validate';
import AllRules from '@vee-validate/rules';
import Toaster from "@meforma/vue-toaster";
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css'
import App from './App.vue'
import router from './router'
@ -9,6 +14,10 @@ import admin from './store/admin/index.js'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.js'
// Import the CSS or use your own!
import "vue-toastification/dist/index.css";
import './assets/css/admin.css'
const app = createApp(App)
router.app = app
@ -17,5 +26,12 @@ app.use(router)
app.use(VueAxios, axios)
app.use(VueCookies)
app.use(admin)
app.use(Toaster, { position: 'top-right' })
Object.keys(AllRules).forEach(rule => {
defineRule(rule, AllRules[rule]);
});
app.component('date-picker', Datepicker);
app.mount('#app')

+ 11
- 2
Frontend/vue/src/router/index.js View File

@ -3,6 +3,7 @@ import HelloWorld from "@/components/HelloWorld.vue";
import AdminLogin from "@/components/admin/AdminLogin.vue";
import AdminSignup from "@/components/admin/AdminSignup.vue";
import AdminUsersList from "@/components/admin/users/AdminUsersList.vue";
import AdminUsersForm from "@/components/admin/users/AdminUsersForm.vue";
import admin from '@/store/admin/index.js'
@ -28,7 +29,15 @@ const routes = [
component: AdminUsersList,
meta: {
requiresAuth: true,
}
},
},
{
path: '/admin/users/:id',
name: 'AdminUsersForm',
component: AdminUsersForm,
meta: {
requiresAuth: true,
},
},
];
@ -40,7 +49,7 @@ const router = createRouter({
router.beforeEach((to, from, next) => {
const user = admin.getters.getUser;
if ((to.name == 'AdminLogin' || to.name == 'AdminSignup') && user !== null) {
if ((to.name == 'AdminLogin' || to.name == 'AdminSignup') && user !== null && !to.params.unauthorized) {
next({ name: 'AdminUsersList' });
return;
}


+ 4
- 2
Frontend/vue/src/utils/http/index.js View File

@ -1,6 +1,8 @@
import axios from 'axios'
import router from '@/router'
import admin from '@/store/admin/index.js'
const instance = axios.create({
baseURL: "http://localhost:8080/api/v1/",
headers: {
@ -10,13 +12,13 @@ const instance = axios.create({
instance.interceptors.response.use(
function (response) {
console.log(response)
return response;
},
function (error) {
if (error.response.status === 401) {
router.push({ name: 'AdminLogin' })
admin.dispatch('setUser', null)
router.push({ name: 'AdminLogin', params: { unauthorized: true } })
return
}


+ 11
- 0
Models/Users.go View File

@ -2,8 +2,19 @@ package Models
import (
"time"
"gorm.io/gorm"
)
// Prevent updating the email if it has not changed
// This stops a unique constraint error
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
if !tx.Statement.Changed("Email") {
tx.Statement.Omit("Email")
}
return nil
}
type User struct {
Base
Email string `gorm:"not null;unique" json:"email"`


+ 21
- 0
Util/Strings.go View File

@ -0,0 +1,21 @@
package Util
import (
"math/rand"
)
var (
letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
)
func RandomString(n int) string {
var (
b []rune
i int
)
b = make([]rune, n)
for i = range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}

+ 18
- 1
main.go View File

@ -1,21 +1,38 @@
package main
import (
"flag"
"net/http"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database/Seeder"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Frontend"
"github.com/gorilla/mux"
)
var (
seed bool
)
func init() {
Database.Init()
flag.BoolVar(&seed, "seed", false, "Seed database for development")
flag.Parse()
}
func main() {
var (
router *mux.Router
)
Database.Init()
if seed {
Seeder.Seed()
return
}
router = mux.NewRouter()


Loading…
Cancel
Save