frontend: add /login and /register forms
This commit is contained in:
parent
bea56f101a
commit
c4c49931a4
13 changed files with 3635 additions and 0 deletions
2807
frontend/package-lock.json
generated
Normal file
2807
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
29
frontend/package.json
Normal file
29
frontend/package.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^4.6.2",
|
||||||
|
"bootstrap-icons-vue": "^1.10.3",
|
||||||
|
"dns-query": "^0.11.2",
|
||||||
|
"js-nacl": "^1.4.0",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"vue": "^3.2.47",
|
||||||
|
"vue-multiselect": "^2.1.7",
|
||||||
|
"vue-router": "^4.1.6",
|
||||||
|
"vuex": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"@vue/test-utils": "^2.3.2",
|
||||||
|
"jsdom": "^22.0.0",
|
||||||
|
"sass": "^1.72.0",
|
||||||
|
"vite": "^4.1.4",
|
||||||
|
"vitest": "^0.31.1"
|
||||||
|
}
|
||||||
|
}
|
18
frontend/src/App.vue
Normal file
18
frontend/src/App.vue
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<router-view></router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
110
frontend/src/components/BaseLayout.vue
Normal file
110
frontend/src/components/BaseLayout.vue
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="main">
|
||||||
|
<nav class="navbar navbar-expand navbar-light navbar-bg">
|
||||||
|
<a class="sidebar-toggle d-flex" @click="toggleSidebar">
|
||||||
|
<i class="hamburger align-self-center"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<slot></slot>
|
||||||
|
<Footer/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Footer from "@/components/Footer.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BaseLayout',
|
||||||
|
components: {
|
||||||
|
Footer
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
hideSearch: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleSidebar() {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
align-items: stretch;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: margin-left .35s ease-in-out, left .35s ease-in-out, margin-right .35s ease-in-out, right .35s ease-in-out;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-expand {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
border-bottom: 0;
|
||||||
|
padding: .875rem 1.375rem;
|
||||||
|
box-shadow: 0 0 2rem var(--bs-shadow)
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-bg {
|
||||||
|
background: var(--bs-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&, &:after, &:before {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 1px;
|
||||||
|
height: 3px;
|
||||||
|
width: 24px;
|
||||||
|
background: var(--bs-gray-700);
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
transition: background .1s ease-in-out, color .1s ease-in-out
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
top: -7.5px;
|
||||||
|
width: 24px;
|
||||||
|
position: absolute
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
bottom: -7.5px;
|
||||||
|
width: 16px;
|
||||||
|
position: absolute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle:hover {
|
||||||
|
.hamburger, .hamburger:after, .hamburger:before {
|
||||||
|
background: var(--bs-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
53
frontend/src/components/Footer.vue
Normal file
53
frontend/src/components/Footer.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row text-muted">
|
||||||
|
<div class="col-6 text-left">
|
||||||
|
<p class="mb-0">
|
||||||
|
<a target="_blank" href="https://www.gnu.org/licenses/gpl-3.0.de.html"
|
||||||
|
class="text-muted">
|
||||||
|
License: <strong>GPL-3.0</strong>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-right">
|
||||||
|
<ul class="list-inline">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a class="text-muted"
|
||||||
|
target="_blank" href="/docs/">API Docs</a>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a class="text-muted"
|
||||||
|
target="_blank" href="/wiki/">Wiki</a>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a class="text-muted"
|
||||||
|
target="_blank" href="https://github.com/gr4yj3d1/toolshed">Sources</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Footer"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 1rem .875rem;
|
||||||
|
direction: ltr;
|
||||||
|
background: var(--bs-background-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
16
frontend/src/main.js
Normal file
16
frontend/src/main.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {createApp} from 'vue'
|
||||||
|
import {BootstrapIconsPlugin} from 'bootstrap-icons-vue';
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
import './scss/toolshed.scss'
|
||||||
|
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
import _nacl from 'js-nacl';
|
||||||
|
|
||||||
|
const app = createApp(App).use(BootstrapIconsPlugin);
|
||||||
|
|
||||||
|
_nacl.instantiate((nacl) => {
|
||||||
|
window.nacl = nacl
|
||||||
|
app.use(router).mount('#app')
|
||||||
|
});
|
35
frontend/src/router.js
Normal file
35
frontend/src/router.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
|
import Index from '@/views/Index.vue';
|
||||||
|
import Login from '@/views/Login.vue';
|
||||||
|
import Register from '@/views/Register.vue';
|
||||||
|
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{path: '/', component: Index, meta: {requiresAuth: true}},
|
||||||
|
{path: '/login', component: Login, meta: {requiresAuth: false}},
|
||||||
|
{path: '/register', component: Register, meta: {requiresAuth: false}},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
// 4. Provide the history implementation to use. We are using the hash history for simplicity here.
|
||||||
|
history: createWebHistory(),
|
||||||
|
linkActiveClass: "active",
|
||||||
|
routes, // short for `routes: routes`
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to/*, from*/) => {
|
||||||
|
// instead of having to check every route record with
|
||||||
|
// to.matched.some(record => record.meta.requiresAuth)
|
||||||
|
if (to.meta.requiresAuth && false) {
|
||||||
|
// this route requires auth, check if logged in
|
||||||
|
// if not, redirect to login page.
|
||||||
|
console.log("Not logged in, redirecting to login page")
|
||||||
|
return {
|
||||||
|
path: '/login',
|
||||||
|
// save the location we were at to come back later
|
||||||
|
query: {redirect: to.fullPath},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
58
frontend/src/scss/_card.scss
Normal file
58
frontend/src/scss/_card.scss
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 0 .875rem map-get($theme-colors, shadow);
|
||||||
|
background-clip: initial;
|
||||||
|
border: 0 solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: map-get($theme-colors, background-1);
|
||||||
|
border-bottom: 0 solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: map-get($theme-colors, text-3);
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: -.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle, .card-text:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.card {
|
||||||
|
& > .dataTables_wrapper .table.dataTable,
|
||||||
|
& > .table,
|
||||||
|
& > .table-responsive-lg .table,
|
||||||
|
& > .table-responsive-md .table,
|
||||||
|
& > .table-responsive-sm .table,
|
||||||
|
& > .table-responsive-xl .table,
|
||||||
|
& > .table-responsive .table {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-left: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
& tr:first-child td,
|
||||||
|
& tr:first-child th {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& td:last-child,
|
||||||
|
& th:last-child {
|
||||||
|
border-right: 0;
|
||||||
|
padding-right: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& td:first-child,
|
||||||
|
& th:first-child {
|
||||||
|
border-left: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
frontend/src/scss/_forms.scss
Normal file
71
frontend/src/scss/_forms.scss
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
height: initial;
|
||||||
|
min-height: calc(1.8125rem + 2px);
|
||||||
|
padding: .25rem .7rem;
|
||||||
|
appearance: none;
|
||||||
|
background-color: initial;
|
||||||
|
border-radius: .2rem;
|
||||||
|
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control-lg {
|
||||||
|
height: initial;
|
||||||
|
min-height: calc(2.0875rem + 2px);
|
||||||
|
padding: .35rem 1rem;
|
||||||
|
font-size: .925rem;
|
||||||
|
border-radius: .3rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
padding: .25rem .7rem;
|
||||||
|
font-size: .875rem;
|
||||||
|
border-radius: .2rem;
|
||||||
|
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: .25rem 1.7rem .25rem .7rem;
|
||||||
|
color: map-get($theme-colors, text-3);
|
||||||
|
background-color: map-get($theme-colors, background-1);
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right .7rem center;
|
||||||
|
background-size: 16px 12px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: .2rem;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group-sm > .btn, .btn-sm {
|
||||||
|
padding: .15rem .5rem;
|
||||||
|
font-size: .75rem;
|
||||||
|
border-radius: .1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group > :not(:first-child):not(.dropdown-menu) {
|
||||||
|
margin-left: -1px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group > .dropdown-toggle:nth-last-child(n+3), .input-group > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu) {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: .25rem .7rem;
|
||||||
|
background-color: map-get($theme-colors, background-2);
|
||||||
|
border-right-width: 0;
|
||||||
|
}
|
138
frontend/src/scss/toolshed.scss
Normal file
138
frontend/src/scss/toolshed.scss
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
$variable-prefix: bs-;
|
||||||
|
|
||||||
|
$white: #ffffff;
|
||||||
|
|
||||||
|
$theme-colors: (
|
||||||
|
"light": #d7e1dc,
|
||||||
|
"dark": #1f2327,
|
||||||
|
"primary": #3a7ddd,
|
||||||
|
"secondary": #45393a,
|
||||||
|
"info": #027980,
|
||||||
|
"success": #019a56,
|
||||||
|
"warning": #ffc107,
|
||||||
|
"danger": #ee1200,
|
||||||
|
"background-1": $white,
|
||||||
|
"background-2": #e9ecef,
|
||||||
|
"text-1": #000,
|
||||||
|
"text-3": #495057,
|
||||||
|
"shadow": #2125291a,
|
||||||
|
);
|
||||||
|
|
||||||
|
$font-size-base: 0.875rem;
|
||||||
|
|
||||||
|
$h1-font-size: $font-size-base * 2;
|
||||||
|
$h2-font-size: $font-size-base * 1.75;
|
||||||
|
$h3-font-size: $font-size-base * 1.5;
|
||||||
|
$h4-font-size: $font-size-base * 1.25;
|
||||||
|
$h5-font-size: $font-size-base;
|
||||||
|
$h6-font-size: $font-size-base;
|
||||||
|
|
||||||
|
@import "bootstrap/scss/functions";
|
||||||
|
@import "bootstrap/scss/variables";
|
||||||
|
|
||||||
|
$body-color: $gray-700;
|
||||||
|
|
||||||
|
@import "bootstrap/scss/mixins";
|
||||||
|
:root {
|
||||||
|
@each $color, $value in $colors {
|
||||||
|
--#{$variable-prefix}#{$color}: #{$value};
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $color, $value in $theme-colors {
|
||||||
|
--#{$variable-prefix}#{$color}: #{$value};
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $color, $value in $grays {
|
||||||
|
--#{$variable-prefix}gray-#{$color}: #{$value};
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $bp, $value in $grid-breakpoints {
|
||||||
|
--#{$variable-prefix}breakpoint-#{$bp}: #{$value};
|
||||||
|
}
|
||||||
|
|
||||||
|
--#{$variable-prefix}font-family-sans-serif: #{inspect($font-family-sans-serif)};
|
||||||
|
--#{$variable-prefix}font-family-monospace: #{inspect($font-family-monospace)};
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "bootstrap/scss/reboot";
|
||||||
|
@import "bootstrap/scss/type";
|
||||||
|
@import "bootstrap/scss/images";
|
||||||
|
@import "bootstrap/scss/code";
|
||||||
|
@import "bootstrap/scss/grid";
|
||||||
|
@import "bootstrap/scss/tables";
|
||||||
|
@import "bootstrap/scss/forms";
|
||||||
|
@import "bootstrap/scss/buttons";
|
||||||
|
@import "bootstrap/scss/transitions";
|
||||||
|
@import "bootstrap/scss/dropdown";
|
||||||
|
@import "bootstrap/scss/button-group";
|
||||||
|
@import "bootstrap/scss/input-group";
|
||||||
|
@import "bootstrap/scss/custom-forms";
|
||||||
|
@import "bootstrap/scss/nav";
|
||||||
|
@import "bootstrap/scss/navbar";
|
||||||
|
@import "bootstrap/scss/card";
|
||||||
|
@import "bootstrap/scss/breadcrumb";
|
||||||
|
@import "bootstrap/scss/pagination";
|
||||||
|
@import "bootstrap/scss/badge";
|
||||||
|
@import "bootstrap/scss/jumbotron";
|
||||||
|
@import "bootstrap/scss/alert";
|
||||||
|
@import "bootstrap/scss/progress";
|
||||||
|
@import "bootstrap/scss/media";
|
||||||
|
@import "bootstrap/scss/list-group";
|
||||||
|
@import "bootstrap/scss/close";
|
||||||
|
@import "bootstrap/scss/toasts";
|
||||||
|
@import "bootstrap/scss/modal";
|
||||||
|
@import "bootstrap/scss/tooltip";
|
||||||
|
@import "bootstrap/scss/popover";
|
||||||
|
@import "bootstrap/scss/carousel";
|
||||||
|
@import "bootstrap/scss/spinners";
|
||||||
|
@import "bootstrap/scss/utilities";
|
||||||
|
@import "bootstrap/scss/print";
|
||||||
|
|
||||||
|
@import "card";
|
||||||
|
@import "forms";
|
||||||
|
|
||||||
|
#root, body, html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow-y: scroll;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
background-color: var(--bs-gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 1.5rem 1.5rem .75rem;
|
||||||
|
flex: 1;
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 100vw;
|
||||||
|
direction: ltr
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: map-get($grid-breakpoints, md)) {
|
||||||
|
.content {
|
||||||
|
width: auto;
|
||||||
|
max-width: auto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: map-get($grid-breakpoints, lg)) {
|
||||||
|
.content {
|
||||||
|
padding: 2.5rem 2.5rem 1rem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 400;
|
||||||
|
color: map-get($theme-colors, text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > :not(caption) > * > * {
|
||||||
|
padding: .75rem;
|
||||||
|
background-color: var(--bs-table-bg);
|
||||||
|
background-image: linear-gradient(var(--bs-table-accent-bg), var(--bs-table-accent-bg));
|
||||||
|
border-bottom-width: 1px !important;
|
||||||
|
}
|
31
frontend/src/views/Index.vue
Normal file
31
frontend/src/views/Index.vue
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<main class="content">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
<h1 class="h3 mb-3">Dashboard</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
components: {
|
||||||
|
...BIcons,
|
||||||
|
BaseLayout
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
106
frontend/src/views/Login.vue
Normal file
106
frontend/src/views/Login.vue
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<div class="main d-flex w-100">
|
||||||
|
<div class="container d-flex flex-column">
|
||||||
|
<div class="row vh-100">
|
||||||
|
<div class="col-sm-10 col-md-8 col-lg-6 mx-auto d-table h-100">
|
||||||
|
<div class="d-table-cell align-middle">
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<h1 class="h2">
|
||||||
|
Toolshed
|
||||||
|
</h1>
|
||||||
|
<p class="lead" v-if="msg">
|
||||||
|
{{ msg }}
|
||||||
|
</p>
|
||||||
|
<p class="lead" v-else>
|
||||||
|
Sign in to your account to continue
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="m-sm-4">
|
||||||
|
<form role="form" @submit.prevent="do_login">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input class="form-control form-control-lg" type="text"
|
||||||
|
name="username" placeholder="Enter your username"
|
||||||
|
v-model="username"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input class="form-control form-control-lg" type="password"
|
||||||
|
name="password" placeholder="Enter your password"
|
||||||
|
v-model="password"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" value="remember-me"
|
||||||
|
name="remember-me" checked v-model="remember"
|
||||||
|
@change="setRemember(remember)">
|
||||||
|
<span class="form-check-label">
|
||||||
|
Remember me next time
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<button type="submit" name="login" class="btn btn-lg btn-primary">Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<br/>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-0 text-muted">
|
||||||
|
Don’t have an account?
|
||||||
|
<router-link to="/register">Sign up</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Login',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
msg: 'Welcome to ' + location.hostname,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
remember: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setRemember(remember) {
|
||||||
|
},
|
||||||
|
login(data) {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async do_login(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (await this.login({username: this.username, password: this.password, remember: this.remember})) {
|
||||||
|
if (this.$route.query.redirect) {
|
||||||
|
await router.push({path: this.$route.query.redirect});
|
||||||
|
} else {
|
||||||
|
await router.push({path: '/'});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.msg = 'Invalid username or password';
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
163
frontend/src/views/Register.vue
Normal file
163
frontend/src/views/Register.vue
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
<template>
|
||||||
|
<div class="main d-flex w-100">
|
||||||
|
<div class="container d-flex flex-column">
|
||||||
|
<div class="row vh-100">
|
||||||
|
<div class="col-sm-10 col-md-8 col-lg-6 mx-auto d-table h-100">
|
||||||
|
<div class="d-table-cell align-middle">
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<h1 class="h2">
|
||||||
|
Toolshed
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="lead" v-if="msg">
|
||||||
|
{{ msg }}
|
||||||
|
</p>
|
||||||
|
<p class="lead" v-else>
|
||||||
|
Create an account to get started
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="m-sm-4">
|
||||||
|
<form role="form" method="post" @submit.prevent="do_register">
|
||||||
|
<div :class="errors.username||errors.domain?['mb-3','is-invalid']:['mb-3']">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control form-control-lg"
|
||||||
|
type="text" v-model="form.username" id="validationCustomUsername"
|
||||||
|
placeholder="Enter your username" required/>
|
||||||
|
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text form-control form-control-lg">@</span>
|
||||||
|
</div>
|
||||||
|
<select class="form-control form-control-lg"
|
||||||
|
id="exampleFormControlSelect1"
|
||||||
|
placeholder="Domain" v-model="form.domain" required>
|
||||||
|
<option v-for="domain in domains">{{ domain }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ errors.username }}{{ errors.domain }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="errors.email?['mb-3','is-invalid']:['mb-3']">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input class="form-control form-control-lg" type="email"
|
||||||
|
v-model="form.email" placeholder="Enter your email"/>
|
||||||
|
<div class="invalid-feedback">{{ errors.email }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="errors.password?['mb-3','is-invalid']:['mb-3']">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input class="form-control form-control-lg" type="password"
|
||||||
|
v-model="form.password" placeholder="Enter your password"/>
|
||||||
|
<div class="invalid-feedback">{{ errors.password }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="errors.password2?['mb-3','is-invalid']:['mb-3']">
|
||||||
|
<label class="form-label">Password Check</label>
|
||||||
|
<input class="form-control form-control-lg" type="password"
|
||||||
|
v-model="password2" placeholder="Enter your password again"/>
|
||||||
|
<div class="invalid-feedback">{{ errors.password2 }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<button type="submit" class="btn btn-lg btn-primary">
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<br/>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-0 text-muted">
|
||||||
|
Already have an account?
|
||||||
|
<router-link to="/login">Login</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Register',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
msg: 'Register new account',
|
||||||
|
password2: '',
|
||||||
|
form: {
|
||||||
|
username: '',
|
||||||
|
domain: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
username: null,
|
||||||
|
domain: null,
|
||||||
|
email: null,
|
||||||
|
password: null,
|
||||||
|
password2: null,
|
||||||
|
},
|
||||||
|
domains: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
do_register() {
|
||||||
|
console.log('do_register');
|
||||||
|
console.log(this.form);
|
||||||
|
if (this.form.password !== this.password2) {
|
||||||
|
this.errors.password2 = 'Passwords do not match';
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.errors.password2 = null;
|
||||||
|
}
|
||||||
|
fetch('/auth/register/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(this.form)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.errors) {
|
||||||
|
console.error('Error:', data.errors);
|
||||||
|
this.errors = data.errors;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Success:', data);
|
||||||
|
this.msg = 'Success';
|
||||||
|
this.$router.push('/login');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
this.msg = 'Error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
fetch('/api/domains/')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
this.domains = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.is-invalid input, .is-invalid select {
|
||||||
|
border: 1px solid var(--bs-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-invalid .invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue