Reviewed-on: #3develop
| @ -1 +1,2 @@ | |||||
| /Frontend/public/images/* | /Frontend/public/images/* | ||||
| /Frontend/vue/node_modules | |||||
| @ -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) | |||||
| } | |||||
| @ -0,0 +1,8 @@ | |||||
| package Seeder | |||||
| import "log" | |||||
| func Seed() { | |||||
| log.Println("Seeding users...") | |||||
| SeedUsers() | |||||
| } | |||||
| @ -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) | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -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") | |||||
| } | |||||
| @ -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 | |||||
| } | |||||
| @ -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) | |||||
| } | |||||
| @ -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? | |||||
| @ -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/). | |||||
| @ -0,0 +1,5 @@ | |||||
| module.exports = { | |||||
| presets: [ | |||||
| '@vue/cli-plugin-babel/preset' | |||||
| ] | |||||
| } | |||||
| @ -0,0 +1,19 @@ | |||||
| { | |||||
| "compilerOptions": { | |||||
| "target": "es5", | |||||
| "module": "esnext", | |||||
| "baseUrl": "./", | |||||
| "moduleResolution": "node", | |||||
| "paths": { | |||||
| "@/*": [ | |||||
| "src/*" | |||||
| ] | |||||
| }, | |||||
| "lib": [ | |||||
| "esnext", | |||||
| "dom", | |||||
| "dom.iterable", | |||||
| "scripthost" | |||||
| ] | |||||
| } | |||||
| } | |||||
| @ -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" | |||||
| ] | |||||
| } | |||||
| @ -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> | |||||
| @ -0,0 +1,12 @@ | |||||
| <template> | |||||
| <router-view /> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| name: 'App', | |||||
| } | |||||
| </script> | |||||
| <style> | |||||
| </style> | |||||
| @ -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,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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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') | |||||
| @ -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; | |||||
| @ -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; | |||||
| } | |||||
| } | |||||
| }) | |||||
| @ -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 | |||||
| @ -0,0 +1,4 @@ | |||||
| const { defineConfig } = require('@vue/cli-service') | |||||
| module.exports = defineConfig({ | |||||
| transpileDependencies: true | |||||
| }) | |||||
| @ -0,0 +1,10 @@ | |||||
| package Util | |||||
| import ( | |||||
| "net/mail" | |||||
| ) | |||||
| func IsEmailValid(email string) bool { | |||||
| _, err := mail.ParseAddress(email) | |||||
| return err == nil | |||||
| } | |||||
| @ -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) | |||||
| } | |||||
| @ -1,24 +1,43 @@ | |||||
| package main | package main | ||||
| import ( | import ( | ||||
| "flag" | |||||
| "net/http" | "net/http" | ||||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api" | "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api" | ||||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" | "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" | ||||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database/Seeder" | |||||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Frontend" | |||||
| "github.com/gorilla/mux" | "github.com/gorilla/mux" | ||||
| ) | ) | ||||
| var ( | |||||
| seed bool | |||||
| ) | |||||
| func init() { | |||||
| Database.Init() | |||||
| flag.BoolVar(&seed, "seed", false, "Seed database for development") | |||||
| flag.Parse() | |||||
| } | |||||
| func main() { | func main() { | ||||
| var ( | var ( | ||||
| router *mux.Router | 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) | |||||
| } | } | ||||