This commit is contained in:
j3d1 2023-05-10 23:28:55 +02:00
parent c02b1588f2
commit a83083d0eb
22 changed files with 1835 additions and 0 deletions

28
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

29
frontend/README.md Normal file
View file

@ -0,0 +1,29 @@
# frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

15
frontend/index.html Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="shortcut icon" href="/src/assets/icons/toolshed-48x48.png" type="image/png">
<title>Toolshed</title>
<link href="/src/assets/base.css" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

23
frontend/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "frontend",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"bootstrap-icons-vue": "^1.10.3",
"dns-query": "^0.11.2",
"js-nacl": "^1.4.0",
"vue": "^3.2.47",
"vue-router": "^4.1.6",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"sass": "^1.62.1",
"vite": "^4.1.4"
}
}

28
frontend/src/App.vue Normal file
View file

@ -0,0 +1,28 @@
<script setup>
</script>
<template>
<router-view></router-view>
<!-- TODO UI für Freunde liste, add, remove -->
</template>
<script>
import {mapMutations} from 'vuex';
import store from '@/store';
export default {
name: 'App',
methods: {
...mapMutations(['init'])
},
beforeCreate () {
store.commit('init')
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,294 @@
<template>
<div class="wrapper">
<nav id="sidebar" class="sidebar">
<div class="sidebar-content js-simplebar">
<router-link to="/" class="sidebar-brand">
<span class="align-middle">Toolshed</span>
</router-link>
<ul class="sidebar-nav">
<li class="sidebar-header">
Tools & Components
</li>
<!--
<li class="sidebar-item {% if 'icons' in segment %} active {% endif %}">
<a class="sidebar-link" href="{% url 'inventory' %}">
{% bs_icon 'archive' extra_classes="bi-valign-middle" %}
<span class="align-middle">Inventory</span>
</a>
</li>
-->
<li class="sidebar-item">
<router-link to="/inventory" class="sidebar-link">
<b-icon-archive class="bi-valign-middle"></b-icon-archive>
<span class="align-middle">Inventory</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/friends" class="sidebar-link">
<b-icon-people class="bi-valign-middle"></b-icon-people>
<span class="align-middle">Friends</span>
</router-link>
</li>
</ul>
</div>
</nav>
<div class="main">
<nav class="navbar navbar-expand navbar-light navbar-bg">
<a class="sidebar-toggle d-flex">
<i class="hamburger align-self-center"></i>
</a>
<form class="d-none d-sm-inline-block">
<div class="input-group input-group-navbar">
<input type="text" class="form-control" placeholder="Search…" aria-label="Search">
<button class="btn" type="button">
<b-icon-search class="bi-valign-middle"></b-icon-search>
</button>
</div>
</form>
<div class="navbar-collapse collapse">
<ul class="navbar-nav navbar-align">
<li class="nav-item dropdown">
<a class="nav-icon dropdown-toggle" href="#" id="alertsDropdown" data-toggle="dropdown">
<div class="position-relative">
<b-icon-bell class="bi-valign-middle"></b-icon-bell>
<span class="indicator">4</span>
</div>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right py-0"
aria-labelledby="alertsDropdown">
<div class="dropdown-menu-header">
4 New Notifications
</div>
<div class="list-group">
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<b-icon-exclamation-circle
class="bi-valign-middle text-danger"></b-icon-exclamation-circle>
</div>
<div class="col-10">
<div class="text-dark">Update completed</div>
<div class="text-muted small mt-1">Restart server 12 to complete the
update.
</div>
<div class="text-muted small mt-1">30m ago</div>
</div>
</div>
</a>
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<b-icon-bell class="bi-valign-middle text-warning"></b-icon-bell>
</div>
<div class="col-10">
<div class="text-dark">Lorem ipsum</div>
<div class="text-muted small mt-1">Aliquam ex eros, imperdiet vulputate
hendrerit
et.
</div>
<div class="text-muted small mt-1">2h ago</div>
</div>
</div>
</a>
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<b-icon-house class="bi-valign-middle text-primary"></b-icon-house>
</div>
<div class="col-10">
<div class="text-dark">Login from 192.186.1.8</div>
<div class="text-muted small mt-1">5h ago</div>
</div>
</div>
</a>
<!--{% for notification in top_notifications %}
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
{% bs_icon 'person-add' extra_classes="bi-valign-middle text-success" %}
</div>
<div class="col-10">
<div class="text-dark">New connection</div>
<div class="text-muted small mt-1">Christina accepted your request.</div>
<div class="text-muted small mt-1">14h ago</div>
</div>
</div>
</a>
{% endfor %}-->
</div>
<div class="dropdown-menu-footer">
<a href="#" class="text-muted">Show all notifications</a>
</div>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-icon dropdown-toggle" href="#" id="messagesDropdown" data-toggle="dropdown">
<div class="position-relative">
<b-icon-chat-left class="bi-valign-middle"></b-icon-chat-left>
</div>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right py-0"
aria-labelledby="messagesDropdown">
<div class="dropdown-menu-header">
<div class="position-relative">
4 New Messages
</div>
</div>
<div class="list-group">
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<!--<img src="/static/assets/img/avatars/avatar-5.png"
class="avatar img-fluid rounded-circle" alt="Vanessa Tucker">-->
</div>
<div class="col-10 pl-2">
<div class="text-dark">Vanessa Tucker</div>
<div class="text-muted small mt-1">Nam pretium turpis et arcu. Duis arcu
tortor.
</div>
<div class="text-muted small mt-1">15m ago</div>
</div>
</div>
</a>
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<!--<img src="/static/assets/img/avatars/avatar-2.png"
class="avatar img-fluid rounded-circle" alt="William Harris">-->
</div>
<div class="col-10 pl-2">
<div class="text-dark">William Harris</div>
<div class="text-muted small mt-1">Curabitur ligula sapien euismod
vitae.
</div>
<div class="text-muted small mt-1">2h ago</div>
</div>
</div>
</a>
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<!--<img src="/static/assets/img/avatars/avatar-4.png"
class="avatar img-fluid rounded-circle" alt="Christina Mason">-->
</div>
<div class="col-10 pl-2">
<div class="text-dark">Christina Mason</div>
<div class="text-muted small mt-1">Pellentesque auctor neque nec urna.
</div>
<div class="text-muted small mt-1">4h ago</div>
</div>
</div>
</a>
<!-- {% for messege in top_messages %}-->
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<!--<img src="/static/assets/img/avatars/avatar-3.png"
class="avatar img-fluid rounded-circle" alt="Sharon Lessman">-->
</div>
<div class="col-10 pl-2">
<div class="text-dark">Sharon Lessman</div>
<div class="text-muted small mt-1">Aenean tellus metus, bibendum sed,
posuere ac,
mattis non.
</div>
<div class="text-muted small mt-1">5h ago</div>
</div>
</div>
</a>
<!--{% endfor %}-->
</div>
<div class="dropdown-menu-footer">
<a href="#" class="text-muted">Show all messages</a>
</div>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-icon dropdown-toggle d-inline-block d-sm-none" href="#"
data-toggle="dropdown">
<i class="align-middle" data-feather="settings"></i>
<b-icon-chat-left class="bi-valign-middle"></b-icon-chat-left>
</a>
<a class="nav-link dropdown-toggle d-none d-sm-inline-block" href="#"
data-toggle="dropdown">
<!--<img src="/static/assets/img/avatars/avatar.png" class="avatar img-fluid rounded mr-1"
alt="Charles Hall"/>-->
<span class="text-dark">
<!--{{ request.user.username }}-->
</span>
</a>
<div class="dropdown-menu dropdown-menu-right">
<router-link to="/profile" class="dropdown-item">
<b-icon-person class="bi-valign-middle mr-1"></b-icon-person>
Profile
</router-link>
<router-link to="/settings" class="dropdown-item">
<b-icon-sliders class="bi-valign-middle mr-1"></b-icon-sliders>
Settings &
Privacy
</router-link>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" @click="logout"> Log out</a>
</div>
</li>
</ul>
</div>
</nav>
<slot></slot>
<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="https://github.com/gr4yj3d1/toolshed">Dev Docs</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>
</div>
</div>
</template>
<script>
import {mapGetters, mapMutations} from 'vuex';
import * as BIcons from "bootstrap-icons-vue";
export default {
name: 'BaseLayout',
components: {
...BIcons
},
computed: {
username() {
return this.$route.params.username
}
},
methods: {
...mapMutations(['logout'])
},
async mounted() {
}
}
</script>
<style scoped>
</style>

30
frontend/src/dns.js Normal file
View file

@ -0,0 +1,30 @@
import {query} from 'dns-query';
class FallBackResolver {
constructor() {
this._servers = ['1.1.1.1', '8.8.8.8'];
this._cache = JSON.parse(localStorage.getItem('dns-cache')) || {};
}
async query(domain, type) {
const key = domain + ':' + type;
if (key in this._cache && this._cache[key].time > Date.now() - 1000 * 60 * 60) {
const age_seconds = Math.ceil(Date.now() / 1000 - this._cache[key].time / 1000);
console.log('cache hit', key, this._cache[key].ttl - age_seconds);
return [this._cache[key].data];
}
const result = await query(
{question: {type: type, name: domain}},
{
endpoints: this._servers,
}
)
const first = result.answers[0];
this._cache[key] = {time: Date.now(), ...first}; // TODO hadle multiple answers
localStorage.setItem('dns-cache', JSON.stringify(this._cache));
console.log('cache miss', key, first.ttl);
return [first.data];
}
}
export default FallBackResolver;

19
frontend/src/main.js Normal file
View file

@ -0,0 +1,19 @@
import {createApp} from 'vue'
//import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'
import { BootstrapIconsPlugin } from 'bootstrap-icons-vue';
import App from './App.vue'
import './assets/css/toolshed.css'
import './assets/js/app.js'
import router from './router'
import store from './store';
import _nacl from 'js-nacl';
const app = createApp(App).use(router).use(store).use(BootstrapIconsPlugin);
_nacl.instantiate((nacl) => {
window.nacl = nacl
app.mount('#app')
});

38
frontend/src/neigbors.js Normal file
View file

@ -0,0 +1,38 @@
class NeighborsCache {
constructor() {
//this._max_age = 1000 * 60 * 60; // 1 hour
this._max_age = 1000 * 60 * 5; // 5 minutes
this._cache = JSON.parse(localStorage.getItem('neighbor-cache')) || {};
}
reachable(domain) {
console.log('reachable neighbor ' + domain)
if (domain in this._cache) {
delete this._cache[domain];
localStorage.setItem('neighbor-cache', JSON.stringify(this._cache));
}
}
unreachable(domain) {
console.log('unreachable neighbor ' + domain)
this._cache[domain] = {time: Date.now()};
localStorage.setItem('neighbor-cache', JSON.stringify(this._cache));
}
queryUnreachable(domain) {
//return false if unreachable
if (domain in this._cache) {
if (this._cache[domain].time > Date.now() - this._max_age) {
console.log('skip unreachable neighbor ' + domain + ' ' + Math.ceil(
Date.now()/1000 - this._cache[domain].time/1000) + 's/' + Math.ceil(this._max_age/1000) + 's')
return true
} else {
delete this._cache[domain];
localStorage.setItem('neighbor-cache', JSON.stringify(this._cache));
}
}
return false;
}
}
export default NeighborsCache;

49
frontend/src/router.js Normal file
View file

@ -0,0 +1,49 @@
import {createRouter, createWebHistory} from 'vue-router'
import Index from '@/views/Index.vue';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';
import store from '@/store';
import Profile from '@/views/Profile.vue';
import Settings from '@/views/Settings.vue';
import Inventory from '@/views/Inventory.vue';
import Friends from "@/views/Friends.vue";
import InventoryNew from "@/views/InventoryNew.vue";
import InventoryEdit from "@/views/InventoryEdit.vue";
import InventoryDetail from "@/views/InventoryDetail.vue";
const routes = [
{path: '/', component: Index, meta: {requiresAuth: true}},
{path: '/profile', component: Profile, meta: {requiresAuth: true}},
{path: '/settings', component: Settings, meta: {requiresAuth: true}},
{path: '/inventory', component: Inventory, meta: {requiresAuth: true}},
{path: '/inventory/:id', component: InventoryDetail, meta: {requiresAuth: true}},
{path: '/inventory/:id/edit', component: InventoryEdit, meta: {requiresAuth: true}},
{path: '/inventory/new', component: InventoryNew, meta: {requiresAuth: true}},
{path: '/friends', component: Friends, 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 && !store.getters.isLoggedIn) {
// this route requires auth, check if logged in
// if not, redirect to login page.
return {
path: '/login',
// save the location we were at to come back later
query: {redirect: to.fullPath},
}
}
})
export default router

186
frontend/src/store.js Normal file
View file

@ -0,0 +1,186 @@
import {createStore} from 'vuex';
import router from '@/router';
import FallBackResolver from "@/dns";
import NeighborsCache from "@/neigbors";
export default createStore({
state: {
user: null,
token: null,
keypair: null,
remember: false,
friends: [],
item_map: {},
resolver: new FallBackResolver(),
unreachable_neighbors: new NeighborsCache(),
/*wk: new Wellknown({
update: true, // Will load latest definitions from updateURL.
updateURL: new URL(), // URL to load the latest definitions. (default: project URL)
persist: false, // True to persist the loaded definitions (nodejs: in filesystem, browser: localStorage)
localStoragePrefix: 'dnsquery_', // Prefix for files persisted.
maxAge: 300000, // Max age of persisted data to be used in ms.
timeout: 5000 // Timeout when loading updates.
})*/
},
mutations: {
setUser(state, user) {
state.user = user;
if (state.remember)
localStorage.setItem('user', user);
},
setToken(state, token) {
state.token = token;
if (state.remember)
localStorage.setItem('token', token);
},
setKey(state, keypair) {
state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair))
if (state.remember)
localStorage.setItem('keypair', nacl.to_hex(state.keypair.signSk).slice(0, 64))
},
setRemember(state, remember) {
state.remember = remember;
if (!remember) {
localStorage.removeItem('user');
localStorage.removeItem('token');
localStorage.removeItem('keypair');
}
localStorage.setItem('remember', remember);
},
setInventoryItems(state, {url, items}) {
state.item_map[url] = items;
},
logout(state) {
state.user = null;
state.token = null;
state.keypair = null;
localStorage.removeItem('user');
localStorage.removeItem('token');
localStorage.removeItem('keypair');
router.push('/login');
},
init(state) {
const remember = localStorage.getItem('remember');
const user = localStorage.getItem('user');
const token = localStorage.getItem('token');
const keypair = localStorage.getItem('keypair');
if (user && token) {
this.commit('setUser', user);
this.commit('setToken', token);
if (keypair) {
this.commit('setKey', keypair)
} else {
}
router.push('/');
}
}
},
actions: {
async login({commit, dispatch, state}, {username, password, remember}) {
//this.setRemember(remember)
this.commit('setRemember', remember);
/*const response = await fetch('http://10.23.42.128:8000/auth/token/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username, password: password
})
})*/
const data = await dispatch('apiLocalPost', {
target: '/token/', data: {
username: username, password: password
}
})
if (data.token) {
commit('setToken', data.token);
commit('setUser', username + '@example.com');
const j = await dispatch('apiLocalGet', {target: '/keys/'})
const k = j.key
this.commit('setKey', k)
await router.push({path: '/'});
return true;
} else {
return false;
}
},
async getFriends(state) {
return ['jedi@j3d1.de', 'foobar@example.com', 'foobaz@example.eleon'];
},
async getFriendServer({state}, {username}) {
const domain = username.split('@')[1]
if (domain === 'example.eleon')
return ['10.23.42.186:8000'];
if (domain === 'localhost')
return ['127.0.0.1:8000'];
if (domain === 'example.com')
return ['10.23.42.128:8000'];
const request = '_toolshed-server._tcp.' + domain + '.'
return await state.resolver.query(request, 'SRV').then(
(result) => result.map(
(answer) => answer.target + ':' + answer.port))
},
async apiFederatedGet({state}, url) {
if (state.unreachable_neighbors.queryUnreachable(url)) {
throw new Error('unreachable neighbor')
}
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url), state.keypair.signSk)
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
return await fetch(url, {
method: 'GET',
headers: {
'Authorization': auth
}
}).catch( err => state.unreachable_neighbors.unreachable(url)
).then(response => response.json())
},
async apiFederatedPost({state}, {url, data}) {
if (state.unreachable_neighbors.queryUnreachable(url)) {
throw new Error('unreachable neighbor')
}
const json = JSON.stringify(data)
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + json), state.keypair.signSk)
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
return await fetch(url, {
method: 'POST',
headers: {
'Authorization': auth,
'Content-Type': 'application/json'
},
body: json
}).catch( err => state.unreachable_neighbors.unreachable(url)
).then(response => response.json())
},
async apiLocalGet({state}, {target}) {
const auth = state.token ? {'Authorization': 'Token ' + state.token} : {}
return await fetch('http://10.23.42.128:8000/auth' + target, {
method: 'GET',
headers: auth
}).then(response => response.json())
},
async apiLocalPost({state}, {target, data}) {
const auth = state.token ? {'Authorization': 'Token ' + state.token} : {}
return await fetch('http://10.23.42.128:8000/auth' + target, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...auth
},
body: JSON.stringify(data)
}).then(response => response.json())
}
},
getters: {
isLoggedIn(state) {
return state.user !== null && state.token !== null;
},
inventory_items(state) {
return Object.entries(state.item_map).reduce((acc, [url, items]) => {
return acc.concat(items)
}, [])
}
}
})

View file

@ -0,0 +1,88 @@
<template>
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">Friends</h1>
<div class="row">
<div class="col-12 col-xl-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">Foo</h5>
<h6 class="card-subtitle text-muted">Bar <code>baz</code>.</h6>
</div>
<table class="table table-striped">
<thead>
<tr>
<th style="width:40%;">Name</th>
<th class="d-none d-md-table-cell" style="width:25%">Server</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="friend in friends" :key="friend.name">
<td>{{ friend.name }}</td>
<td class="d-none d-md-table-cell">{{ friend.server.join(', ') }}</td>
<td class="table-action">
<a href="#">
<b-icon-pencil-square></b-icon-pencil-square>
</a>
<a href="#">
<b-icon-trash></b-icon-trash>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import {mapActions, mapGetters} from "vuex";
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: 'Inventory',
components: {
BaseLayout,
...BIcons
},
data() {
return {
friends: [],
}
},
computed: {
username() {
return this.$route.params.username
},
inventory_items() {
return this.local_items.concat(this.eleon_items)
}
},
methods: {
...mapActions(['getFriends', "getFriendServer"]),
},
mounted() {
this.getFriends().then((friends) => {
friends.map((friend) => {
this.getFriendServer({username: friend}).then((server) => {
this.friends.push({
name: friend,
server: server
})
})
})
})
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,41 @@
<template>
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">Blank Page</h1>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Empty card</h5>
</div>
<div class="card-body">
<div class="logo">
<img src="/src/assets/icons/toolshed-48x48.png" alt="Toolshed logo">
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import {mapGetters, mapMutations} from 'vuex';
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: 'Index',
components: {
...BIcons,
BaseLayout
},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,93 @@
<template>
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">Inventory Own & Friends"</h1>
<div class="row">
<div class="col-12 col-xl-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">Foo</h5>
<h6 class="card-subtitle text-muted">Bar <code>baz</code>.</h6>
</div>
<table class="table table-striped">
<thead>
<tr>
<th style="width:40%;">Name</th>
<th style="width:25%">Owner</th>
<th class="d-none d-md-table-cell" style="width:25%">Amount</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="item in inventory_items" :key="item.id">
<td>
<router-link :to="`/inventory/${item.id}`">{{ item.name }}</router-link>
</td>
<td>{{ item.owner }}</td>
<td class="d-none d-md-table-cell">{{ item.owned_amount }}</td>
<td class="table-action">
<router-link :to="`/inventory/${item.id}/edit`">
<b-icon-pencil-square></b-icon-pencil-square>
</router-link>
<a :href="`/inventory/${item.id}/delete`" @click.prevent="deleteItem(item.id)">
<b-icon-trash></b-icon-trash>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<button class="btn" @click="getInventoryItems">Refresh</button>
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: "Inventory",
components: {
BaseLayout,
...BIcons
},
computed: {
...mapGetters(["inventory_items"]),
username() {
return this.$route.params.username
}
},
methods: {
...mapActions(["apiFederatedGet", "getFriends", "getFriendServer"]),
...mapMutations(["setInventoryItems"]),
async getInventoryItems() {
try {
const servers = await this.getFriends().then(friends => friends.map(friend => this.getFriendServer({username: friend})))
const urls = servers.map(server => server.then(s => `http://${s}/api/inventory_items/`))
urls.map(url => url.then(u => this.apiFederatedGet(u).then(items => {
this.setInventoryItems({url: u, items})
}).catch(e => {
}))) // TODO: handle error
} catch (e) {
console.error(e)
}
},
},
async mounted() {
await this.getInventoryItems()
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,65 @@
<script setup>
</script>
<template>
<BaseLayout :username="username">
<main class="container">
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">Inventory</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Owner</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="item in inventory_items" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.owner }}</td>
<td>
<a href="#">
<b-icon-pencil-square></b-icon-pencil-square>
</a>
<a href="#">
<b-icon-trash></b-icon-trash>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<button class="btn" @click="getInventoryItems">Refresh</button>
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: "InventoryDetail",
components: {
BaseLayout,
...BIcons
},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,65 @@
<script setup>
</script>
<template>
<BaseLayout :username="username">
<main class="container">
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">Inventory</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Owner</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="item in inventory_items" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.owner }}</td>
<td>
<a href="#">
<b-icon-pencil-square></b-icon-pencil-square>
</a>
<a href="#">
<b-icon-trash></b-icon-trash>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<button class="btn" @click="getInventoryItems">Refresh</button>
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: "InventoryEdit",
components: {
BaseLayout,
...BIcons
},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,65 @@
<script setup>
</script>
<template>
<BaseLayout :username="username">
<main class="container">
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">Inventory</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Owner</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="item in inventory_items" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.owner }}</td>
<td>
<a href="#">
<b-icon-pencil-square></b-icon-pencil-square>
</a>
<a href="#">
<b-icon-trash></b-icon-trash>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<button class="btn" @click="getInventoryItems">Refresh</button>
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: "InventoryNew",
components: {
BaseLayout,
...BIcons
},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,97 @@
<template>
<main class="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">
Dont have an account?
<router-link to="/register">Sign up</router-link>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</template>
<script>
import {mapActions, mapMutations} from 'vuex';
export default {
name: 'Login',
data() {
return {
msg: 'Welcome to Your Vue.js App',
username: '',
password: '',
remember: false
}
},
methods: {
...mapActions(['login']),
...mapMutations(['setRemember']),
async do_login(e) {
e.preventDefault();
if (!await this.login({username: this.username, password: this.password, remember: this.remember})) {
this.msg = 'Invalid username or password';
}
},
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,257 @@
<template>
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">Profile</h1>
<div class="row">
<div class="col-md-4 col-xl-3">
<div class="card mb-3">
<div class="card-header">
<h5 class="card-title mb-0">Profile Details</h5>
</div>
<div class="card-body text-center">
<!--<img src="/static/assets/img/avatars/avatar.png"
alt="Christina Mason" class="img-fluid rounded-circle mb-2" width="128"
height="128"/>-->
<h5 class="card-title mb-0">
{{ user.username }}
</h5>
<div class="text-muted mb-2">
{{ user.email }}
</div>
<div>
<a class="btn btn-primary btn-sm" href="#">Follow</a>
<a class="btn btn-primary btn-sm" href="#"><span
data-feather="message-square"></span> Message</a>
</div>
</div>
<!--{% if user.bio %}-->
<hr class="my-0"/>
<div class="card-body">
<h5 class="h6 card-title">Bio</h5>
<div class="text-muted mb-2">
{{ user.bio }}
</div>
</div>
<!--{% endif %}-->
<hr class="my-0"/>
<div class="card-body">
<h5 class="h6 card-title">Skills</h5>
<a href="#" class="badge bg-primary mr-1 my-1">HTML</a>
<a href="#" class="badge bg-primary mr-1 my-1">JavaScript</a>
<a href="#" class="badge bg-primary mr-1 my-1">Sass</a>
<a href="#" class="badge bg-primary mr-1 my-1">Angular</a>
<a href="#" class="badge bg-primary mr-1 my-1">Vue</a>
<a href="#" class="badge bg-primary mr-1 my-1">React</a>
<a href="#" class="badge bg-primary mr-1 my-1">Redux</a>
<a href="#" class="badge bg-primary mr-1 my-1">UI</a>
<a href="#" class="badge bg-primary mr-1 my-1">UX</a>
</div>
<hr class="my-0"/>
<div class="card-body">
<h5 class="h6 card-title">About</h5>
<ul class="list-unstyled mb-0">
<!--{% if user.location %}-->
<li class="mb-1"><span data-feather="home" class="feather-sm mr-1"></span> Lives
in <a href="#">{{ user.location }}</a></li>
<!--{% endif %}-->
<li class="mb-1"><span data-feather="briefcase" class="feather-sm mr-1"></span>
Works at <a href="#">GitHub</a></li>
<li class="mb-1"><span data-feather="map-pin" class="feather-sm mr-1"></span>
From <a href="#">Boston</a></li>
</ul>
</div>
<hr class="my-0"/>
<div class="card-body">
<h5 class="h6 card-title">Elsewhere</h5>
<ul class="list-unstyled mb-0">
<li class="mb-1"><span class="fas fa-globe fa-fw mr-1"></span> <a href="#">staciehall.co</a>
</li>
<li class="mb-1"><span class="fab fa-twitter fa-fw mr-1"></span> <a
href="#">Twitter</a>
</li>
<li class="mb-1"><span class="fab fa-facebook fa-fw mr-1"></span> <a href="#">Facebook</a>
</li>
<li class="mb-1"><span class="fab fa-instagram fa-fw mr-1"></span> <a href="#">Instagram</a>
</li>
<li class="mb-1"><span class="fab fa-linkedin fa-fw mr-1"></span> <a href="#">LinkedIn</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-8 col-xl-9">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Activities</h5>
</div>
<div class="card-body h-100">
<div class="d-flex align-items-start">
<!--<img src="/static/assets/img/avatars/avatar-5.png" width="36" height="36"
class="rounded-circle mr-2" alt="Vanessa Tucker">-->
<div class="flex-grow-1">
<small class="float-right text-navy">5m ago</small>
<strong>Vanessa Tucker</strong> started following <strong>Christina
Mason</strong><br/>
<small class="text-muted">Today 7:51 pm</small><br/>
</div>
</div>
<hr/>
<div class="d-flex align-items-start">
<!--<img src="/static/assets/img/avatars/avatar.png" width="36" height="36"
class="rounded-circle mr-2" alt="Charles Hall">-->
<div class="flex-grow-1">
<small class="float-right text-navy">30m ago</small>
<strong>Charles Hall</strong> posted something on <strong>Christina
Mason</strong>'s timeline<br/>
<small class="text-muted">Today 7:21 pm</small>
<div class="border text-sm text-muted p-2 mt-1">
Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem
quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam
nunc, blandit vel, luctus
pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt
tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis
ante.
</div>
<a href="#" class="btn btn-sm btn-danger mt-1"><i class="feather-sm"
data-feather="heart"></i>
Like</a>
</div>
</div>
<hr/>
<div class="d-flex align-items-start">
<!--<img src="/static/assets/img/avatars/avatar-4.png" width="36" height="36"
class="rounded-circle mr-2" alt="Christina Mason">-->
<div class="flex-grow-1">
<small class="float-right text-navy">1h ago</small>
<strong>Christina Mason</strong> posted a new blog<br/>
<small class="text-muted">Today 6:35 pm</small>
</div>
</div>
<hr/>
<div class="d-flex align-items-start">
<!--<img src="/static/assets/img/avatars/avatar-2.png" width="36" height="36"
class="rounded-circle mr-2" alt="William Harris">-->
<div class="flex-grow-1">
<small class="float-right text-navy">3h ago</small>
<strong>William Harris</strong> posted two photos on <strong>Christina
Mason</strong>'s timeline<br/>
<small class="text-muted">Today 5:12 pm</small>
<div class="row g-0 mt-1">
<div class="col-6 col-md-4 col-lg-4 col-xl-3">
<!--<img src="/static/assets/img/photos/unsplash-1.jpg"
class="img-fluid pr-2" alt="Unsplash">-->
</div>
<div class="col-6 col-md-4 col-lg-4 col-xl-3">
<!--<img src="/static/assets/img/photos/unsplash-2.jpg"
class="img-fluid pr-2" alt="Unsplash">-->
</div>
</div>
<a href="#" class="btn btn-sm btn-danger mt-1"><i class="feather-sm"
data-feather="heart"></i>
Like</a>
</div>
</div>
<hr/>
<div class="d-flex align-items-start">
<!--<img src="/static/assets/img/avatars/avatar-2.png" width="36" height="36"
class="rounded-circle mr-2" alt="William Harris">-->
<div class="flex-grow-1">
<small class="float-right text-navy">1d ago</small>
<strong>William Harris</strong> started following <strong>Christina
Mason</strong><br/>
<small class="text-muted">Yesterday 3:12 pm</small>
<div class="d-flex align-items-start mt-1">
<a class="pr-3" href="#">
<!--<img src="/static/assets/img/avatars/avatar-4.png" width="36"
height="36" class="rounded-circle mr-2" alt="Christina Mason">-->
</a>
<div class="flex-grow-1">
<div class="border text-sm text-muted p-2 mt-1">
Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id,
lorem. Maecenas nec odio et ante tincidunt tempus.
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="d-flex align-items-start">
<!--<img src="/static/assets/img/avatars/avatar-4.png" width="36" height="36"
class="rounded-circle mr-2" alt="Christina Mason">-->
<div class="flex-grow-1">
<small class="float-right text-navy">1d ago</small>
<strong>Christina Mason</strong> posted a new blog<br/>
<small class="text-muted">Yesterday 2:43 pm</small>
</div>
</div>
<hr/>
<div class="d-flex align-items-start">
<!--<img src="/static/assets/img/avatars/avatar.png" width="36" height="36"
class="rounded-circle mr-2" alt="Charles Hall">-->
<div class="flex-grow-1">
<small class="float-right text-navy">1d ago</small>
<strong>Charles Hall</strong> started following <strong>Christina
Mason</strong><br/>
<small class="text-muted">Yesterdag 1:51 pm</small>
</div>
</div>
<hr/>
<a href="#" class="btn btn-primary btn-block">Load more</a>
</div>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: 'Profile',
data() {
return {
user: {
name: 'John Doe',
avatar: '/static/assets/img/avatars/avatar.png',
cover: '/static/assets/img/photos/unsplash-1.jpg',
occupation: 'Frontend Developer',
company: 'Facebook Inc.',
email: 'foo@bar.com',
phone: '+12 345 678 001',
address: 'Boulevard of Broken Dreams, 1234',
}
}
},
components: {BaseLayout},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,88 @@
<template>
<main class="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" action="">
<!--{% csrf_token %}-->
<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"/>
</div>
<span class="text-error">{{ form.username.errors }}</span>
<div class="mb-3">
<label class="form-label">Email</label>
<input class="form-control form-control-lg" type="email"
name="email" placeholder="Enter your email"/>
</div>
<span class="text-error">{{ form.email.errors }}</span>
<div class="mb-3">
<label class="form-label">Password</label>
<input class="form-control form-control-lg" type="password"
name="password1" placeholder="Enter your password"/>
</div>
<span class="text-error">{{ form.password1.errors }}</span>
<div class="mb-3">
<label class="form-label">Password Check</label>
<input class="form-control form-control-lg" type="password"
name="password2" placeholder="Enter your password again"/>
</div>
<span class="text-error">{{ form.password2.errors }}</span>
<div class="text-center mt-3">
<button type="submit" name="register" 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>
</main>
</template>
<script>
export default {
name: 'Register',
data() {
return {
msg: 'Register',
form: {
username: '',
email: '',
password1: '',
password2: '',}
}
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,209 @@
<template>
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">Settings</h1>
<div class="row">
<div class="col-md-3 col-xl-2">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Profile Settings</h5>
</div>
<div class="list-group list-group-flush" role="tablist">
<a class="list-group-item list-group-item-action active" data-toggle="list"
href="#account" role="tab">
Account
</a>
<a class="list-group-item list-group-item-action" data-toggle="list"
href="#password" role="tab">
Password
</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
role="tab">
Privacy and safety
</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
role="tab">
Email notifications
</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
role="tab">
Web notifications
</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
role="tab">
Widgets
</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
role="tab">
Your data
</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
role="tab">
Delete account
</a>
</div>
</div>
</div>
<div class="col-md-9 col-xl-10">
<div class="tab-content">
<div class="tab-pane fade show active" id="account" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Public info</h5>
</div>
<div class="card-body">
<form>
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label class="form-label"
for="inputUsername">Username</label>
<input type="text" class="form-control" id="inputUsername"
placeholder="Username">
</div>
<div class="mb-3">
<label class="form-label"
for="inputUsername">Biography</label>
<textarea rows="2" class="form-control" id="inputBio"
placeholder="Tell something about yourself"></textarea>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<img alt="Charles Hall"
src="/static/assets/img/avatars/avatar.png"
class="rounded-circle img-responsive mt-2" width="128"
height="128"/>
<div class="mt-2">
<span class="btn btn-primary"><i
class="fas fa-upload"></i> Upload</span>
</div>
<small>For best results, use an image at least 128px by
128px in .jpg format</small>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save changes</button>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Private info</h5>
</div>
<div class="card-body">
<form>
<div class="row">
<div class="mb-3 col-md-6">
<label class="form-label" for="inputFirstName">First
name</label>
<input type="text" class="form-control" id="inputFirstName"
placeholder="First name">
</div>
<div class="mb-3 col-md-6">
<label class="form-label" for="inputLastName">Last name</label>
<input type="text" class="form-control" id="inputLastName"
placeholder="Last name">
</div>
</div>
<div class="mb-3">
<label class="form-label" for="inputEmail4">Email</label>
<input type="email" class="form-control" id="inputEmail4"
placeholder="Email">
</div>
<div class="mb-3">
<label class="form-label" for="inputAddress">Address</label>
<input type="text" class="form-control" id="inputAddress"
placeholder="1234 Main St">
</div>
<div class="mb-3">
<label class="form-label" for="inputAddress2">Address 2</label>
<input type="text" class="form-control" id="inputAddress2"
placeholder="Apartment, studio, or floor">
</div>
<div class="row">
<div class="mb-3 col-md-6">
<label class="form-label" for="inputCity">City</label>
<input type="text" class="form-control" id="inputCity">
</div>
<div class="mb-3 col-md-4">
<label class="form-label" for="inputState">State</label>
<select id="inputState" class="form-control">
<option selected>Choose...</option>
<option>...</option>
</select>
</div>
<div class="mb-3 col-md-2">
<label class="form-label" for="inputZip">Zip</label>
<input type="text" class="form-control" id="inputZip">
</div>
</div>
<button type="submit" class="btn btn-primary">Save changes</button>
</form>
</div>
</div>
</div>
<div class="tab-pane fade" id="password" role="tabpanel">
<div class="card">
<div class="card-body">
<h5 class="card-title">Password</h5>
<form>
<div class="mb-3">
<label class="form-label" for="inputPasswordCurrent">Current
password</label>
<input type="password" class="form-control"
id="inputPasswordCurrent">
<small><a href="#">Forgot your password?</a></small>
</div>
<div class="mb-3">
<label class="form-label" for="inputPasswordNew">New
password</label>
<input type="password" class="form-control" id="inputPasswordNew">
</div>
<div class="mb-3">
<label class="form-label" for="inputPasswordNew2">Verify
password</label>
<input type="password" class="form-control" id="inputPasswordNew2">
</div>
<button type="submit" class="btn btn-primary">Save changes</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: 'Settings',
components: {BaseLayout},
}
</script>
<style scoped>
</style>

28
frontend/vite.config.js Normal file
View file

@ -0,0 +1,28 @@
import {fileURLToPath, URL} from 'node:url'
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
host: true,
cors: true,
headers: {
//allow all origins
//'Access-Control-Allow-Origin': 'http://10.23.42.128:8000, http://10.23.42.168:8000',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Auth-Token, Authorization, Accept,charset,boundary,Content-Length',
'Access-Control-Allow-Credentials': 'true'
}
}
})