Reviewed-on: #3develop
@ -1 +1,2 @@ | |||
/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 | |||
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) | |||
} |