Browse Source

Merge pull request 'feature/add-vue-frontend' (#3) from feature/add-vue-frontend into develop

Reviewed-on: #3
develop
Tovi Jaeschke-Rogers 2 years ago
parent
commit
cfeb008724
41 changed files with 22382 additions and 51 deletions
  1. +1
    -0
      .gitignore
  2. +13
    -0
      Api/Auth/Login.go
  3. +36
    -0
      Api/Auth/Me.go
  4. +2
    -2
      Api/Auth/Session.go
  5. +8
    -8
      Api/JsonSerialization/DeserializeUserJson.go
  6. +20
    -21
      Api/Routes.go
  7. +29
    -3
      Api/Users.go
  8. +1
    -1
      Api/Users_test.go
  9. +8
    -0
      Database/Seeder/Seed.go
  10. +97
    -0
      Database/Seeder/UserSeeder.go
  11. +23
    -11
      Database/Users.go
  12. +13
    -0
      Frontend/GetFrontendAssets_dev.go
  13. +26
    -0
      Frontend/GetFrontendAssets_prod.go
  14. +51
    -0
      Frontend/Routes.go
  15. +23
    -0
      Frontend/vue/.gitignore
  16. +24
    -0
      Frontend/vue/README.md
  17. +5
    -0
      Frontend/vue/babel.config.js
  18. +19
    -0
      Frontend/vue/jsconfig.json
  19. +20536
    -0
      Frontend/vue/package-lock.json
  20. +56
    -0
      Frontend/vue/package.json
  21. BIN
      Frontend/vue/public/favicon.ico
  22. +17
    -0
      Frontend/vue/public/index.html
  23. +12
    -0
      Frontend/vue/src/App.vue
  24. +116
    -0
      Frontend/vue/src/assets/css/admin.css
  25. BIN
      Frontend/vue/src/assets/logo.png
  26. +58
    -0
      Frontend/vue/src/components/HelloWorld.vue
  27. +105
    -0
      Frontend/vue/src/components/admin/AdminLogin.vue
  28. +89
    -0
      Frontend/vue/src/components/admin/AdminNavbar.vue
  29. +166
    -0
      Frontend/vue/src/components/admin/AdminSignup.vue
  30. +165
    -0
      Frontend/vue/src/components/admin/users/AdminUsersCreate.vue
  31. +252
    -0
      Frontend/vue/src/components/admin/users/AdminUsersForm.vue
  32. +168
    -0
      Frontend/vue/src/components/admin/users/AdminUsersList.vue
  33. +37
    -0
      Frontend/vue/src/main.js
  34. +79
    -0
      Frontend/vue/src/router/index.js
  35. +28
    -0
      Frontend/vue/src/store/admin/index.js
  36. +29
    -0
      Frontend/vue/src/utils/http/index.js
  37. +4
    -0
      Frontend/vue/vue.config.js
  38. +11
    -0
      Models/Users.go
  39. +10
    -0
      Util/EmailValidation.go
  40. +21
    -0
      Util/Strings.go
  41. +24
    -5
      main.go

+ 1
- 0
.gitignore View File

@ -1 +1,2 @@
/Frontend/public/images/*
/Frontend/vue/node_modules

+ 13
- 0
Api/Auth/Login.go View File

@ -2,11 +2,13 @@ package Auth
import (
"encoding/json"
"log"
"net/http"
"time"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
"github.com/gofrs/uuid"
)
@ -22,6 +24,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
userData Models.User
sessionToken uuid.UUID
expiresAt time.Time
returnJson []byte
err error
)
@ -62,5 +65,15 @@ func Login(w http.ResponseWriter, r *http.Request) {
Expires: expiresAt,
})
userData.Password = ""
returnJson, err = json.MarshalIndent(userData, "", " ")
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}

+ 36
- 0
Api/Auth/Me.go View File

@ -0,0 +1,36 @@
package Auth
import (
"encoding/json"
"log"
"net/http"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
)
func Me(w http.ResponseWriter, r *http.Request) {
var (
userData Models.User
returnJson []byte
err error
)
userData, err = CheckCookieCurrentUser(w, r)
if err != nil {
Util.JsonReturn(w, 401, "NO ERROR")
return
}
returnJson, err = json.MarshalIndent(userData, "", " ")
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}

+ 2
- 2
Api/Auth/Session.go View File

@ -5,8 +5,8 @@ import (
"net/http"
"time"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
)
var (
@ -66,7 +66,7 @@ func CheckCookieCurrentUser(w http.ResponseWriter, r *http.Request) (Models.User
return userData, err
}
userData, err = Util.GetUserById(w, r)
userData, err = Database.GetUserById(userSession.UserID)
if err != nil {
return userData, err
}


+ 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
}

+ 20
- 21
Api/Routes.go View File

@ -8,40 +8,39 @@ import (
"github.com/gorilla/mux"
)
func InitApiEndpoints() *mux.Router {
func InitApiEndpoints(router *mux.Router) {
var (
router *mux.Router
api *mux.Router
)
log.Println("Initializing API routes...")
router = mux.NewRouter()
api = router.PathPrefix("/api/v1/").Subrouter()
// Define routes for posts api
router.HandleFunc("/post", getPosts).Methods("GET")
router.HandleFunc("/post", createPost).Methods("POST")
router.HandleFunc("/post/{postID}", getPost).Methods("GET")
router.HandleFunc("/post/{postID}", updatePost).Methods("PUT")
router.HandleFunc("/post/{postID}", deletePost).Methods("DELETE")
api.HandleFunc("/post", getPosts).Methods("GET")
api.HandleFunc("/post", createPost).Methods("POST")
api.HandleFunc("/post/{postID}", getPost).Methods("GET")
api.HandleFunc("/post/{postID}", updatePost).Methods("PUT")
api.HandleFunc("/post/{postID}", deletePost).Methods("DELETE")
router.HandleFunc("/frontPagePosts", getFrontPagePosts).Methods("GET")
api.HandleFunc("/frontPagePosts", getFrontPagePosts).Methods("GET")
router.HandleFunc("/post/{postID}/image", createPostImage).Methods("POST")
router.HandleFunc("/post/{postID}/image/{imageID}", deletePostImage).Methods("DELETE")
api.HandleFunc("/post/{postID}/image", createPostImage).Methods("POST")
api.HandleFunc("/post/{postID}/image/{imageID}", deletePostImage).Methods("DELETE")
// Define routes for users api
router.HandleFunc("/admin/user", getUsers).Methods("GET")
router.HandleFunc("/admin/user", createUser).Methods("POST")
router.HandleFunc("/admin/user/{userID}", getUser).Methods("GET")
router.HandleFunc("/admin/user/{userID}", updatePost).Methods("PUT")
router.HandleFunc("/admin/user/{userID}", deletePost).Methods("DELETE")
router.HandleFunc("/admin/user/{userID}/update-password", Auth.UpdatePassword).Methods("PUT")
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}", updateUser).Methods("PUT")
api.HandleFunc("/admin/user/{userID}", deletePost).Methods("DELETE")
api.HandleFunc("/admin/user/{userID}/update-password", Auth.UpdatePassword).Methods("PUT")
// Define routes for authentication
router.HandleFunc("/admin/login", Auth.Login).Methods("POST")
router.HandleFunc("/admin/logout", Auth.Logout).Methods("GET")
api.HandleFunc("/admin/login", Auth.Login).Methods("POST")
api.HandleFunc("/admin/logout", Auth.Logout).Methods("GET")
api.HandleFunc("/admin/me", Auth.Me).Methods("GET")
//router.PathPrefix("/").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir("./uploads"))))
return router
}

+ 29
- 3
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")
@ -96,6 +105,7 @@ func createUser(w http.ResponseWriter, r *http.Request) {
var (
userData Models.User
requestBody []byte
returnJson []byte
err error
)
@ -116,8 +126,17 @@ func createUser(w http.ResponseWriter, r *http.Request) {
return
}
if userData.FirstName == "" ||
userData.LastName == "" ||
userData.Email == "" ||
userData.Password == "" ||
userData.ConfirmPassword == "" {
Util.JsonReturn(w, http.StatusUnprocessableEntity, "Invalid data")
return
}
err = Database.CheckUniqueEmail(userData.Email)
if err != nil {
if err != nil || !Util.IsEmailValid(userData.Email) {
Util.JsonReturn(w, 405, "invalid_email")
return
}
@ -139,8 +158,15 @@ func createUser(w http.ResponseWriter, r *http.Request) {
return
}
returnJson, err = json.MarshalIndent(userData, "", " ")
if err != nil {
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}
func updateUser(w http.ResponseWriter, r *http.Request) {


+ 1
- 1
Api/Users_test.go View File

@ -190,7 +190,7 @@ func Test_getUsers(t *testing.T) {
createTestUser(true)
}
req, err := http.NewRequest("GET", ts.URL+"/user?page=1&pageSize=10", nil)
req, err := http.NewRequest("GET", ts.URL+"/user?page=0&pageSize=10", nil)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())


+ 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)
}
}
}

+ 23
- 11
Database/Users.go View File

@ -2,6 +2,7 @@ package Database
import (
"errors"
"fmt"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
@ -37,17 +38,15 @@ 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
)
if page == 0 {
page = 1
}
switch {
case pageSize > 100:
pageSize = 100
@ -55,8 +54,22 @@ func GetUsers(page, pageSize int) ([]Models.User, error) {
pageSize = 10
}
err = DB.Offset(page).
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
@ -108,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).


+ 13
- 0
Frontend/GetFrontendAssets_dev.go View File

@ -0,0 +1,13 @@
//go:build !prod
// +build !prod
package Frontend
import (
"io/fs"
"os"
)
func GetFrontendAssets() fs.FS {
return os.DirFS("Frontend/vue/dist")
}

+ 26
- 0
Frontend/GetFrontendAssets_prod.go View File

@ -0,0 +1,26 @@
//go:build prod
// +build prod
package Frontend
import (
"embed"
"io/fs"
"log"
)
//go:embed Frontend/vue/dist
var frontend embed.FS
func GetFrontendAssets() fs.FS {
var (
stripped fs.FS
err error
)
stripped, err = fs.Sub(frontend, "Frontend/vue/dist")
if err != nil {
log.Fatalln(err)
}
return stripped
}

+ 51
- 0
Frontend/Routes.go View File

@ -0,0 +1,51 @@
package Frontend
import (
"io/fs"
"net/http"
"github.com/gorilla/mux"
)
const (
indexPath = "Frontend/vue/dist/index.html"
)
var (
routes []string = []string{
"/admin/login",
"/admin/signup",
"/admin/users",
"/admin/users/new",
"/admin/users/{id}",
}
)
func indexHandler(entrypoint string) func(w http.ResponseWriter, r *http.Request) {
fn := func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, entrypoint)
}
return http.HandlerFunc(fn)
}
func InitFrontendRoutes(router *mux.Router) {
var (
frontendFS http.Handler
stripped fs.FS
route string
)
stripped = GetFrontendAssets()
frontendFS = http.FileServer(http.FS(stripped))
for _, route = range routes {
router.
PathPrefix(route).
HandlerFunc(indexHandler(indexPath))
}
router.PathPrefix("/").Handler(frontendFS)
}

+ 23
- 0
Frontend/vue/.gitignore View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

+ 24
- 0
Frontend/vue/README.md View File

@ -0,0 +1,24 @@
# vue
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

+ 5
- 0
Frontend/vue/babel.config.js View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

+ 19
- 0
Frontend/vue/jsconfig.json View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

+ 20536
- 0
Frontend/vue/package-lock.json
File diff suppressed because it is too large
View File


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

@ -0,0 +1,56 @@
{
"name": "vue",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"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"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.5.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

BIN
Frontend/vue/public/favicon.ico View File

Before After

+ 17
- 0
Frontend/vue/public/index.html View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

+ 12
- 0
Frontend/vue/src/App.vue View File

@ -0,0 +1,12 @@
<template>
<router-view />
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
</style>

+ 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;
}

BIN
Frontend/vue/src/assets/logo.png View File

Before After
Width: 200  |  Height: 200  |  Size: 6.7 KiB

+ 58
- 0
Frontend/vue/src/components/HelloWorld.vue View File

@ -0,0 +1,58 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

+ 105
- 0
Frontend/vue/src/components/admin/AdminLogin.vue View File

@ -0,0 +1,105 @@
<template>
<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="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="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>
</section>
</template>
<script>
import { Form, Field, ErrorMessage } from 'vee-validate'
export default {
name: 'AdminLogin',
data() {
return {
email: '',
password: '',
}
},
components: {
Form,
Field,
ErrorMessage,
},
methods: {
async login () {
try {
const response = await this.axios.post(
'/admin/login',
{
email: this.email,
password: this.password,
}
)
if (response.status === 200) {
this.$store.dispatch('setUser', response.data)
this.$router.push({ name: 'AdminUsersList' })
}
} catch (error) {
this.$toast.error('An error occured')
}
}
}
}
</script>

+ 89
- 0
Frontend/vue/src/components/admin/AdminNavbar.vue View File

@ -0,0 +1,89 @@
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid px-5">
<!-- TODO: Replace with logo -->
<router-link
:to="{ name: 'AdminUsersList' }"
class="nav-item"
>
Sudden Impact
</router-link>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-md-auto gap-2">
<li class="nav-item rounded">
<router-link
:to="{ name: 'AdminUsersList' }"
class="nav-link"
aria-current="page"
>
<i class="bi bi-house-fill me-2"></i>
Users
</router-link>
</li>
<li class="nav-item dropdown rounded">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"><i class="bi bi-person-fill me-2"></i>Profile</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li>
<router-link
:to="{ name: 'AdminUsersForm', params: { id: $store.getters.getUser.id } }"
class="dropdown-item"
>
Account
</router-link>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<div v-on:click="logout" class="dropdown-item">Logout</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default {
methods: {
async logout () {
try {
const response = await this.axios.get('/admin/logout');
if (response.status === 200) {
this.$store.dispatch('setUser', null);
this.$router.push({ name: 'AdminLogin' })
}
} catch (error) {
console.log(error)
}
}
}
}
</script>
<style>
body {
font-family: Montserrat, sans-serif;
}
.navbar-nav .nav-item:hover {
background-color: rgba(180, 190, 203, 0.4);
}
.navbar-dark .navbar-nav .nav-link.router-link-active {
color: #fff;
}
</style>

+ 166
- 0
Frontend/vue/src/components/admin/AdminSignup.vue View File

@ -0,0 +1,166 @@
<template>
<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>
</div>
</section>
</template>
<script>
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,
}
)
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>

+ 165
- 0
Frontend/vue/src/components/admin/users/AdminUsersCreate.vue View File

@ -0,0 +1,165 @@
<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'"
>
User Details
</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">Create User</h3>
<Form @submit="createUser" 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-12 col-md-6 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="user.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="user.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 :disabled="!meta.touched || !meta.valid" class="btn btn-primary btn-md" type="submit">
Create
</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,
password: null,
confirm_password: null,
}
}
},
components: {
AdminNavbar,
Form,
Field,
ErrorMessage,
},
methods: {
async createUser () {
try {
let response = await this.axios.post(
'/admin/user',
this.user,
)
if (response.status === 200) {
this.$router.push({ name: 'AdminUsersForm', params: { id: response.data.id } })
this.$toast.success('Successfully created user details.');
}
} catch (error) {
this.$toast.error('An error occured');
}
},
}
}
</script>

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

@ -0,0 +1,252 @@
<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"/>
<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>

+ 168
- 0
Frontend/vue/src/components/admin/users/AdminUsersList.vue View File

@ -0,0 +1,168 @@
<template>
<div id="admin-page-container">
<admin-navbar/>
<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">
<router-link :to="{ name: 'AdminUsersCreate' }">
<button
type="button"
class="btn btn-rounded btn-dark"
>
<!-- TODO: Change this to + sign on small screens -->
Add User
</button>
</router-link>
</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'
export default {
data() {
return {
users: {},
pageSize: 15,
page: 0,
search: '',
dataEnd: false,
}
},
components: {
AdminNavbar,
},
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>

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

@ -0,0 +1,37 @@
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'
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
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')

+ 79
- 0
Frontend/vue/src/router/index.js View File

@ -0,0 +1,79 @@
import { createWebHistory, createRouter } from "vue-router";
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 AdminUsersCreate from "@/components/admin/users/AdminUsersCreate.vue";
import AdminUsersForm from "@/components/admin/users/AdminUsersForm.vue";
import admin from '@/store/admin/index.js'
const routes = [
{
path: "/",
name: "Home",
component: HelloWorld,
},
{
path: "/admin/login",
name: "AdminLogin",
component: AdminLogin,
},
{
path: "/admin/signup",
name: "AdminSignup",
component: AdminSignup,
},
{
path: "/admin/users",
name: "AdminUsersList",
component: AdminUsersList,
meta: {
requiresAuth: true,
},
},
{
path: '/admin/users/new',
name: 'AdminUsersCreate',
component: AdminUsersCreate,
meta: {
requiresAuth: true,
},
},
{
path: '/admin/users/:id',
name: 'AdminUsersForm',
component: AdminUsersForm,
meta: {
requiresAuth: true,
},
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
router.beforeEach((to, from, next) => {
const user = admin.getters.getUser;
if ((to.name == 'AdminLogin' || to.name == 'AdminSignup') && user !== null && !to.params.unauthorized) {
next({ name: 'AdminUsersList' });
return;
}
if (!to.meta.requiresAuth) {
next();
return;
}
if (user === null) {
next({ name: 'AdminLogin' });
return;
}
next();
});
export default router;

+ 28
- 0
Frontend/vue/src/store/admin/index.js View File

@ -0,0 +1,28 @@
import { createStore } from 'vuex';
import createPersistedState from "vuex-persistedstate";
export default createStore({
plugins: [createPersistedState()],
state: {
user: {},
},
mutations: {
UPDATE_USER(state, user ){
state.user = user
}
},
actions: {
setUser(context, user) {
context.commit('UPDATE_USER', user)
}
},
getters: {
getUser (state) {
return state.user;
}
}
})

+ 29
- 0
Frontend/vue/src/utils/http/index.js View File

@ -0,0 +1,29 @@
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: {
"Content-Type": "application/json",
},
});
instance.interceptors.response.use(
function (response) {
return response;
},
function (error) {
if (error.response.status === 401) {
admin.dispatch('setUser', null)
router.push({ name: 'AdminLogin', params: { unauthorized: true } })
return
}
return Promise.reject(error);
}
);
export default instance

+ 4
- 0
Frontend/vue/vue.config.js View File

@ -0,0 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})

+ 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"`


+ 10
- 0
Util/EmailValidation.go View File

@ -0,0 +1,10 @@
package Util
import (
"net/mail"
)
func IsEmailValid(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}

+ 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)
}

+ 24
- 5
main.go View File

@ -1,24 +1,43 @@
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()
router = Api.InitApiEndpoints()
Api.InitApiEndpoints(router)
Frontend.InitFrontendRoutes(router)
// TODO: Run this within goroutine when running vue application
// Start and listen to requests
http.ListenAndServe(":8081", router)
http.ListenAndServe(":8080", router)
}

Loading…
Cancel
Save