Rewrote backend in c++

This commit is contained in:
2022-08-28 17:37:09 +02:00
parent d199ecae87
commit 2e8877837a
98 changed files with 14078 additions and 1433 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@@ -1,109 +1,109 @@
<script setup lang="ts">
import { inject, onBeforeMount, ref } from 'vue';
import { inject, onBeforeMount, ref } from "vue";
import {
Responses,
check_token,
TokenInjectType,
Admin,
isErrorResponse
} from '@/api';
import { onBeforeRouteUpdate } from 'vue-router';
import router from '@/router';
Responses,
check_token,
TokenInjectType,
Admin,
isErrorResponse,
} from "@/api";
import { onBeforeRouteUpdate } from "vue-router";
import router from "@/router";
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const users = ref<Responses.Admin.GetUsersEntry[]>([]);
onBeforeRouteUpdate(async () => {
await updatePanel();
await updatePanel();
});
onBeforeMount(async () => {
await updatePanel();
await updatePanel();
});
async function updatePanel() {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.get_users(token);
if (isErrorResponse(res)) return router.replace({ path: '/' });
users.value = res.users;
const res = await Admin.get_users(token);
if (isErrorResponse(res)) return router.replace({ path: "/" });
users.value = res.users;
}
async function setRole(user: number, roleStr: string) {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.set_role(user, parseInt(roleStr, 10), token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
const res = await Admin.set_role(user, parseInt(roleStr, 10), token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
}
async function disableTfa(user: number) {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.disable_tfa(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
const res = await Admin.disable_tfa(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
}
async function logoutUser(user: number) {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.logout(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
const res = await Admin.logout(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
}
async function deleteUser(user: number) {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.delete_user(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
const res = await Admin.delete_user(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
}
</script>
<template>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Role</th>
<th>Tfa Status</th>
<th>Actions</th>
</tr>
<tr v-for="user in users" :key="user.id">
<td>{{ user.name }}</td>
<td>{{ user.gitlab ? 'Gitlab' : 'Password' }}</td>
<td>
<select @change="setRole(user.id, $event.target.value)">
<option value="0" :selected="user.role === 0 ? true : null">
Disabled
</option>
<option value="1" :selected="user.role === 1 ? true : null">
User
</option>
<option value="2" :selected="user.role === 2 ? true : null">
Admin
</option>
</select>
</td>
<td v-if="user.gitlab"></td>
<td v-else>
{{ user.tfaEnabled ? 'Enabled' : 'Disabled' }}
</td>
<td>
<button v-if="user.tfaEnabled" @click="disableTfa(user.id)">
Disable Tfa
</button>
<button @click="logoutUser(user.id)">Logout all</button>
<button @click="deleteUser(user.id)">Delete</button>
</td>
</tr>
</table>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Role</th>
<th>Tfa Status</th>
<th>Actions</th>
</tr>
<tr v-for="user in users" :key="user.id">
<td>{{ user.name }}</td>
<td>{{ user.gitlab ? "Gitlab" : "Password" }}</td>
<td>
<select @change="setRole(user.id, $event.target.value)">
<option value="0" :selected="user.role === 0 ? true : null">
Disabled
</option>
<option value="1" :selected="user.role === 1 ? true : null">
User
</option>
<option value="2" :selected="user.role === 2 ? true : null">
Admin
</option>
</select>
</td>
<td v-if="user.gitlab"></td>
<td v-else>
{{ user.tfaEnabled ? "Enabled" : "Disabled" }}
</td>
<td>
<button v-if="user.tfaEnabled" @click="disableTfa(user.id)">
Disable Tfa
</button>
<button @click="logoutUser(user.id)">Logout all</button>
<button @click="deleteUser(user.id)">Delete</button>
</td>
</tr>
</table>
</template>
<style scoped></style>

View File

@@ -1,70 +1,70 @@
<script setup lang="ts">
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
import { inject, onBeforeMount, ref } from 'vue';
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
import { inject, onBeforeMount, ref } from "vue";
import {
check_token,
FS,
Responses,
isErrorResponse,
TokenInjectType
} from '@/api';
import DirViewer from '@/components/FSView/DirViewer.vue';
import FileViewer from '@/components/FSView/FileViewer.vue';
check_token,
FS,
Responses,
isErrorResponse,
TokenInjectType,
} from "@/api";
import DirViewer from "@/components/FSView/DirViewer.vue";
import FileViewer from "@/components/FSView/FileViewer.vue";
const router = useRouter();
const route = useRoute();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const path = ref('');
const path = ref("");
const node = ref<Responses.FS.GetNodeResponse | null>(null);
async function fetch_node(node_id: number) {
const token = await check_token(jwt);
if (!token) return;
let [p, n] = [
await FS.get_path(token, node_id),
await FS.get_node(token, node_id)
];
if (isErrorResponse(p)) return gotoRoot();
if (isErrorResponse(n)) return gotoRoot();
[path.value, node.value] = [p.path, n];
const token = await check_token(jwt);
if (!token) return;
let [p, n] = [
await FS.get_path(token, node_id),
await FS.get_node(token, node_id),
];
if (isErrorResponse(p)) return gotoRoot();
if (isErrorResponse(n)) return gotoRoot();
[path.value, node.value] = [p.path, n];
}
onBeforeRouteUpdate(async (to) => {
await fetch_node(Number(to.params.node_id));
await fetch_node(Number(to.params.node_id));
});
async function reloadNode() {
await fetch_node(Number(route.params.node_id));
await fetch_node(Number(route.params.node_id));
}
onBeforeMount(async () => {
await reloadNode();
await reloadNode();
});
async function gotoRoot() {
const token = await check_token(jwt);
if (!token) return;
const rootRes = await FS.get_root(token);
if (isErrorResponse(rootRes)) return jwt.logout();
const root = rootRes.rootId;
await router.replace({
name: 'fs',
params: { node_id: root }
});
const token = await check_token(jwt);
if (!token) return;
const rootRes = await FS.get_root(token);
if (isErrorResponse(rootRes)) return jwt.logout();
const root = rootRes.rootId;
await router.replace({
name: "fs",
params: { node_id: root },
});
}
</script>
<template>
<div v-if="node">
<div>Path: {{ path }}</div>
<DirViewer
v-if="!node.isFile"
:node="node"
@reloadNode="reloadNode"
@gotoRoot="gotoRoot"
/>
<FileViewer v-else :node="node" />
</div>
<div v-if="node">
<div>Path: {{ path }}</div>
<DirViewer
v-if="!node.isFile"
:node="node"
@reloadNode="reloadNode"
@gotoRoot="gotoRoot"
/>
<FileViewer v-else :node="node" />
</div>
</template>
<style scoped></style>

View File

@@ -1,28 +1,28 @@
<template><p></p></template>
<script setup lang="ts">
import { onBeforeRouteUpdate, useRouter } from 'vue-router';
import { inject, onBeforeMount } from 'vue';
import { FS, check_token, isErrorResponse, TokenInjectType } from '@/api';
import { onBeforeRouteUpdate, useRouter } from "vue-router";
import { inject, onBeforeMount } from "vue";
import { FS, check_token, isErrorResponse, TokenInjectType } from "@/api";
const router = useRouter();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function start_redirect() {
const token = await check_token(jwt);
if (!token) return;
const root = await FS.get_root(token);
if (isErrorResponse(root)) return jwt.logout();
await router.replace({
name: 'fs',
params: { node_id: root.rootId }
});
const token = await check_token(jwt);
if (!token) return;
const root = await FS.get_root(token);
if (isErrorResponse(root)) return jwt.logout();
await router.replace({
name: "fs",
params: { node_id: root.rootId },
});
}
onBeforeRouteUpdate(async () => {
await start_redirect();
await start_redirect();
});
onBeforeMount(async () => {
await start_redirect();
await start_redirect();
});
</script>

View File

@@ -1,61 +1,61 @@
<script setup lang="ts">
import { ref, inject } from 'vue';
import { Auth, FS, isErrorResponse, TokenInjectType } from '@/api';
import { useRouter } from 'vue-router';
import { ref, inject } from "vue";
import { Auth, FS, isErrorResponse, TokenInjectType } from "@/api";
import { useRouter } from "vue-router";
const router = useRouter();
const username = ref('');
const password = ref('');
const otp = ref('');
const username = ref("");
const password = ref("");
const otp = ref("");
const error = ref('');
const error = ref("");
const requestOtp = ref(false);
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function login() {
error.value = '';
if (username.value === '' || password.value === '') {
error.value = 'Email and/or Password missing';
return;
}
const res = await (requestOtp.value
? Auth.auth_login(username.value, password.value, otp.value)
: Auth.auth_login(username.value, password.value));
if (isErrorResponse(res)) error.value = 'Login failed: ' + res.message;
else if ('jwt' in res) {
const root = await FS.get_root(res.jwt);
if (isErrorResponse(root)) {
error.value = 'Get root failed: ' + root.message;
return;
}
jwt.setToken(res.jwt);
await router.push({
name: 'fs',
params: { node_id: root.rootId }
});
} else {
error.value = '';
requestOtp.value = true;
}
error.value = "";
if (username.value === "" || password.value === "") {
error.value = "Email and/or Password missing";
return;
}
const res = await (requestOtp.value
? Auth.auth_login(username.value, password.value, otp.value)
: Auth.auth_login(username.value, password.value));
if (isErrorResponse(res)) error.value = "Login failed: " + res.message;
else if ("jwt" in res) {
const root = await FS.get_root(res.jwt);
if (isErrorResponse(root)) {
error.value = "Get root failed: " + root.message;
return;
}
jwt.setToken(res.jwt);
await router.push({
name: "fs",
params: { node_id: root.rootId },
});
} else {
error.value = "";
requestOtp.value = true;
}
}
</script>
<template>
<div v-if="error !== ''" v-text="error"></div>
<template v-if="!requestOtp">
<input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" />
<a href="/api/auth/gitlab">Login with gitlab</a>
<router-link to="signup">Signup instead?</router-link>
</template>
<template v-else>
<div>Please input your 2 factor authentication code</div>
<input type="text" placeholder="Code" v-model="otp" />
</template>
<button @click="login()">Login</button>
<div v-if="error !== ''" v-text="error"></div>
<template v-if="!requestOtp">
<input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" />
<a href="/api/auth/gitlab">Login with gitlab</a>
<router-link to="signup">Signup instead?</router-link>
</template>
<template v-else>
<div>Please input your 2 factor authentication code</div>
<input type="text" placeholder="Code" v-model="otp" />
</template>
<button @click="login()">Login</button>
</template>
<style scoped></style>

View File

@@ -1,124 +1,112 @@
<script setup lang="ts">
import { ref, inject, onBeforeMount } from 'vue';
import { ref, inject, onBeforeMount } from "vue";
import {
Auth,
User,
check_token,
isErrorResponse,
TokenInjectType,
Responses
} from '@/api';
import { onBeforeRouteUpdate } from 'vue-router';
Auth,
User,
check_token,
isErrorResponse,
TokenInjectType,
Responses,
} from "@/api";
import { onBeforeRouteUpdate } from "vue-router";
const error = ref('');
const oldPw = ref('');
const newPw = ref('');
const newPw2 = ref('');
const error = ref("");
const oldPw = ref("");
const newPw = ref("");
const newPw2 = ref("");
const user = ref<Responses.User.UserInfoResponse | null>(null);
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
onBeforeRouteUpdate(async () => {
await updateProfile();
await updateProfile();
});
onBeforeMount(async () => {
await updateProfile();
await updateProfile();
});
async function updateProfile() {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await User.get_user_info(token);
if (isErrorResponse(res)) return jwt.logout();
user.value = res;
const res = await User.get_user_info(token);
if (isErrorResponse(res)) return jwt.logout();
user.value = res;
}
async function deleteUser() {
const token = await check_token(jwt);
if (!token) return;
await User.delete_user(token);
jwt.logout();
const token = await check_token(jwt);
if (!token) return;
await User.delete_user(token);
jwt.logout();
}
async function logoutAll() {
const token = await check_token(jwt);
if (!token) return;
await Auth.logout_all(token);
jwt.logout();
const token = await check_token(jwt);
if (!token) return;
await Auth.logout_all(token);
jwt.logout();
}
async function changePw() {
if (oldPw.value === '' || newPw.value === '' || newPw2.value === '') {
error.value = 'Password missing';
return;
}
if (newPw.value !== newPw2.value) {
error.value = "Passwords don't match";
return;
}
const token = await check_token(jwt);
if (!token) return;
const res = await Auth.change_password(oldPw.value, newPw.value, token);
if (isErrorResponse(res))
error.value = 'Password change failed: ' + res.message;
else jwt.logout();
if (oldPw.value === "" || newPw.value === "" || newPw2.value === "") {
error.value = "Password missing";
return;
}
if (newPw.value !== newPw2.value) {
error.value = "Passwords don't match";
return;
}
const token = await check_token(jwt);
if (!token) return;
const res = await Auth.change_password(oldPw.value, newPw.value, token);
if (isErrorResponse(res))
error.value = "Password change failed: " + res.message;
else jwt.logout();
}
async function tfaDisable() {
const token = await check_token(jwt);
if (!token) return;
await Auth.tfa_disable(token);
jwt.logout();
const token = await check_token(jwt);
if (!token) return;
await Auth.tfa_disable(token);
jwt.logout();
}
</script>
<template>
<template v-if="user">
<div v-if="error !== ''" v-text="error"></div>
<div>User: {{ user.name }}</div>
<div>Signed in with {{ user.gitlab ? 'gitlab' : 'password' }}</div>
<template v-if="!user.gitlab">
<div>
<input
type="password"
placeholder="Old password"
v-model="oldPw"
/>
<input
type="password"
placeholder="New password"
v-model="newPw"
/>
<input
type="password"
placeholder="Repeat new password"
v-model="newPw2"
/>
<button @click="changePw">Change</button>
</div>
<div>
<div>
2 Factor authentication:
{{ user.tfaEnabled ? 'Enabled' : 'Disabled' }}
</div>
<div>
<a href="#" v-if="user.tfaEnabled" @click="tfaDisable">
Disable
</a>
<router-link to="/profile/2fa-enable" v-else>
Enable
</router-link>
</div>
</div>
</template>
<div>
<a href="#" @click="logoutAll">Logout everywhere</a>
<a href="#" @click="deleteUser">Delete Account</a>
</div>
</template>
<template v-else>
<div>Loading...</div>
</template>
<template v-if="user">
<div v-if="error !== ''" v-text="error"></div>
<div>User: {{ user.name }}</div>
<div>Signed in with {{ user.gitlab ? "gitlab" : "password" }}</div>
<template v-if="!user.gitlab">
<div>
<input type="password" placeholder="Old password" v-model="oldPw" />
<input type="password" placeholder="New password" v-model="newPw" />
<input
type="password"
placeholder="Repeat new password"
v-model="newPw2"
/>
<button @click="changePw">Change</button>
</div>
<div>
<div>
2 Factor authentication:
{{ user.tfaEnabled ? "Enabled" : "Disabled" }}
</div>
<div>
<a href="#" v-if="user.tfaEnabled" @click="tfaDisable"> Disable </a>
<router-link to="/profile/2fa-enable" v-else> Enable </router-link>
</div>
</div>
</template>
<div>
<a href="#" @click="logoutAll">Logout everywhere</a>
<a href="#" @click="deleteUser">Delete Account</a>
</div>
</template>
<template v-else>
<div>Loading...</div>
</template>
</template>
<style scoped></style>

View File

@@ -1,19 +1,19 @@
<script setup lang="ts">
import { inject } from 'vue';
import { TokenInjectType } from '@/api';
import { useRoute, useRouter } from 'vue-router';
import { inject } from "vue";
import { TokenInjectType } from "@/api";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
const route = useRoute();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
if ('token' in route.query) jwt.setToken(route.query['token'] as string);
router.replace({ path: '/' });
if ("token" in route.query) jwt.setToken(route.query["token"] as string);
router.replace({ path: "/" });
</script>
<template>
<router-link to="/">Click here to go home</router-link>
<router-link to="/">Click here to go home</router-link>
</template>
<style scoped></style>

View File

@@ -1,35 +1,35 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Auth, isErrorResponse } from '@/api';
import { ref } from "vue";
import { Auth, isErrorResponse } from "@/api";
const username = ref('');
const password = ref('');
const password2 = ref('');
const error = ref('');
const username = ref("");
const password = ref("");
const password2 = ref("");
const error = ref("");
async function signup() {
if (username.value === '' || password.value === '') {
error.value = 'Email and/or Password missing';
return;
}
if (password.value !== password2.value) {
error.value = "Passwords don't match";
return;
}
const res = await Auth.auth_signup(username.value, password.value);
error.value = isErrorResponse(res)
? 'Signup failed: ' + res.message
: 'Signup successful, please wait till an admin unlocks your account.';
if (username.value === "" || password.value === "") {
error.value = "Email and/or Password missing";
return;
}
if (password.value !== password2.value) {
error.value = "Passwords don't match";
return;
}
const res = await Auth.auth_signup(username.value, password.value);
error.value = isErrorResponse(res)
? "Signup failed: " + res.message
: "Signup successful, please wait till an admin unlocks your account.";
}
</script>
<template>
<div v-if="error !== ''" v-text="error"></div>
<input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" />
<input type="password" placeholder="Repeat password" v-model="password2" />
<button @click="signup()">Signup</button>
<router-link to="login">Login instead?</router-link>
<div v-if="error !== ''" v-text="error"></div>
<input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" />
<input type="password" placeholder="Repeat password" v-model="password2" />
<button @click="signup()">Signup</button>
<router-link to="login">Login instead?</router-link>
</template>
<style scoped></style>

View File

@@ -1,89 +1,89 @@
<script setup lang="ts">
import { ref, inject } from 'vue';
import { Auth, check_token, isErrorResponse, TokenInjectType } from '@/api';
import { ref, inject } from "vue";
import { Auth, check_token, isErrorResponse, TokenInjectType } from "@/api";
enum state {
SELECT,
MAIL,
TOTP
SELECT,
MAIL,
TOTP,
}
const currentState = ref<state>(state.SELECT);
const error = ref('');
const qrImage = ref('');
const secret = ref('');
const code = ref('');
const error = ref("");
const qrImage = ref("");
const secret = ref("");
const code = ref("");
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function selectMail() {
const token = await check_token(jwt);
if (!token) return;
error.value = 'Working...';
const res = await Auth.tfa_setup(true, token);
if (isErrorResponse(res))
error.value = 'Failed to select 2fa type: ' + res.message;
else {
error.value = '';
currentState.value = state.MAIL;
}
const token = await check_token(jwt);
if (!token) return;
error.value = "Working...";
const res = await Auth.tfa_setup(true, token);
if (isErrorResponse(res))
error.value = "Failed to select 2fa type: " + res.message;
else {
error.value = "";
currentState.value = state.MAIL;
}
}
async function selectTotp() {
const token = await check_token(jwt);
if (!token) return;
error.value = 'Working...';
const res = await Auth.tfa_setup(false, token);
if (isErrorResponse(res))
error.value = 'Failed to select 2fa type: ' + res.message;
else {
qrImage.value = res.qrCode;
secret.value = res.secret;
error.value = '';
currentState.value = state.TOTP;
}
const token = await check_token(jwt);
if (!token) return;
error.value = "Working...";
const res = await Auth.tfa_setup(false, token);
if (isErrorResponse(res))
error.value = "Failed to select 2fa type: " + res.message;
else {
qrImage.value = res.qrCode;
secret.value = res.secret;
error.value = "";
currentState.value = state.TOTP;
}
}
async function submit() {
const token = await check_token(jwt);
if (!token) return;
error.value = 'Working...';
const res = await Auth.tfa_complete(
currentState.value === state.MAIL,
code.value,
token
);
if (isErrorResponse(res))
error.value = 'Failed to submit code: ' + res.message;
else jwt.logout();
const token = await check_token(jwt);
if (!token) return;
error.value = "Working...";
const res = await Auth.tfa_complete(
currentState.value === state.MAIL,
code.value,
token
);
if (isErrorResponse(res))
error.value = "Failed to submit code: " + res.message;
else jwt.logout();
}
</script>
<template>
<div v-if="error !== ''" v-text="error"></div>
<template v-if="currentState === state.SELECT">
<div>Select 2 Factor authentication type:</div>
<div>
<button @click="selectMail">Mail</button>
<button @click="selectTotp">Google Authenticator</button>
</div>
</template>
<template v-else-if="currentState === state.MAIL">
<div>Please enter the code you got by mail</div>
<input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button>
</template>
<template v-else>
<img :src="qrImage" alt="QrCode" />
<details>
<summary>Show manual input code</summary>
{{ secret }}
</details>
<div>Please enter the current code</div>
<input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button>
</template>
<div v-if="error !== ''" v-text="error"></div>
<template v-if="currentState === state.SELECT">
<div>Select 2 Factor authentication type:</div>
<div>
<button @click="selectMail">Mail</button>
<button @click="selectTotp">Google Authenticator</button>
</div>
</template>
<template v-else-if="currentState === state.MAIL">
<div>Please enter the code you got by mail</div>
<input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button>
</template>
<template v-else>
<img :src="qrImage" alt="QrCode" />
<details>
<summary>Show manual input code</summary>
{{ secret }}
</details>
<div>Please enter the current code</div>
<input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button>
</template>
</template>
<style scoped></style>