Compare commits
3 commits
bbe0c91d7e
...
90932a49c8
Author | SHA1 | Date | |
---|---|---|---|
90932a49c8 | |||
cbfc4e9317 | |||
75f809051c |
8 changed files with 304 additions and 9 deletions
8
middleware/firstRun.ts
Normal file
8
middleware/firstRun.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineNuxtRouteMiddleware, navigateTo, useFetch } from "nuxt/app";
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
const firstRun = await useFetch("/api/firstRun");
|
||||||
|
|
||||||
|
if (firstRun.data.value)
|
||||||
|
return navigateTo({ path: "/firstRun" });
|
||||||
|
});
|
62
pages/firstRun.vue
Normal file
62
pages/firstRun.vue
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
/* global $fetch */
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { NuxtError, navigateTo, useFetch } from 'nuxt/app';
|
||||||
|
import { definePageMeta } from '~/.nuxt/imports';
|
||||||
|
|
||||||
|
import EntryEditor, { fieldDefinition } from '~/components/entryEditor.vue';
|
||||||
|
import Alerts, { AlertData } from '~/components/alerts.vue';
|
||||||
|
|
||||||
|
const editorFields: Array<fieldDefinition> = [
|
||||||
|
{ key: "username", type: "text", label: "Username", optional: false },
|
||||||
|
{ key: "password", type: "text", label: "Password", optional: false },
|
||||||
|
{ key: "email", type: "text", label: "email", optional: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const formValue = ref<any>({});
|
||||||
|
const alerts = ref<Array<AlertData>>([]);
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
try {
|
||||||
|
await $fetch("/api/firstRun", {
|
||||||
|
body: formValue.value,
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
await navigateTo("/login");
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
alerts.value.push({ text: (e as NuxtError).data.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await useFetch("/api/firstRun")).data.value)
|
||||||
|
await navigateTo("/login");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Alerts :alerts="alerts" />
|
||||||
|
<VCard max-width="450px" class="mx-auto mt-16" variant="outlined">
|
||||||
|
<template #title>
|
||||||
|
Initial setup
|
||||||
|
</template>
|
||||||
|
<template #text>
|
||||||
|
<p>
|
||||||
|
It looks like you've run the server with an empty or uninitialized database or with database without any users.<br>
|
||||||
|
Below you can initialize the database register your first user and.
|
||||||
|
</p><br>
|
||||||
|
<EntryEditor
|
||||||
|
:fields="editorFields"
|
||||||
|
@update-sub-model-value="(k, v) => { formValue[k] = v }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<VBtn color="primary" @click="submit">
|
||||||
|
Initialize
|
||||||
|
</VBtn>
|
||||||
|
</template>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
|
@ -19,6 +19,7 @@ const redirectTo = ref(route.redirectedFrom);
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: false,
|
layout: false,
|
||||||
|
middleware: ["first-run"],
|
||||||
});
|
});
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
|
|
167
schemaModel.sql
Normal file
167
schemaModel.sql
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
-- Server version 8.0.32
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` bigint unsigned NOT NULL DEFAULT (((unix_timestamp() * 1000 * pow(2,22)) + floor((rand() * pow(2,12))))),
|
||||||
|
`username` varchar(30) NOT NULL,
|
||||||
|
`email` varchar(128) NOT NULL,
|
||||||
|
`password` binary(64) NOT NULL,
|
||||||
|
`display_name` varchar(30) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idusers_UNIQUE` (`id`),
|
||||||
|
UNIQUE KEY `username_UNIQUE` (`username`),
|
||||||
|
UNIQUE KEY `email_UNIQUE` (`email`)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `clients`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `clients` (
|
||||||
|
`id` bigint unsigned NOT NULL DEFAULT (((unix_timestamp() * 1000 * pow(2,22)) + floor((rand() * pow(2,12))))),
|
||||||
|
`name` varchar(128) DEFAULT NULL,
|
||||||
|
`address` varchar(128) DEFAULT NULL,
|
||||||
|
`phone` varchar(16) DEFAULT NULL,
|
||||||
|
`email` varchar(128) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `orders`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `orders` (
|
||||||
|
`id` bigint unsigned NOT NULL DEFAULT (((unix_timestamp() * 1000 * pow(2,22)) + floor((rand() * pow(2,12))))),
|
||||||
|
`client` bigint unsigned NOT NULL,
|
||||||
|
`user` bigint unsigned NOT NULL,
|
||||||
|
`is_draft` tinyint NOT NULL DEFAULT '1',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `user_idx` (`user`),
|
||||||
|
KEY `client_idx` (`client`),
|
||||||
|
CONSTRAINT `client` FOREIGN KEY (`client`) REFERENCES `clients` (`id`),
|
||||||
|
CONSTRAINT `user` FOREIGN KEY (`user`) REFERENCES `users` (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `imported_products`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `imported_products` (
|
||||||
|
`id` bigint unsigned NOT NULL DEFAULT (((unix_timestamp() * 1000 * pow(2,22)) + floor((rand() * pow(2,12))))),
|
||||||
|
`order` bigint unsigned NOT NULL,
|
||||||
|
`name` varchar(128) DEFAULT NULL,
|
||||||
|
`link` varchar(1024) NOT NULL,
|
||||||
|
`price_imported` decimal(10,2) NOT NULL DEFAULT '0.00',
|
||||||
|
`price` decimal(10,2) NOT NULL DEFAULT '0.00',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `order_idx` (`order`),
|
||||||
|
CONSTRAINT `order2` FOREIGN KEY (`order`) REFERENCES `orders` (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `offer`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `offer` (
|
||||||
|
`id` bigint unsigned NOT NULL DEFAULT (((unix_timestamp() * 1000 * pow(2,22)) + floor((rand() * pow(2,12))))),
|
||||||
|
`name` varchar(45) NOT NULL,
|
||||||
|
`description` text,
|
||||||
|
`recommended_price` decimal(10,2) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `order_templates`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `order_templates` (
|
||||||
|
`id` bigint unsigned NOT NULL DEFAULT (((unix_timestamp() * 1000 * pow(2,22)) + floor((rand() * pow(2,12))))),
|
||||||
|
`name` varchar(45) NOT NULL,
|
||||||
|
`description` text,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `sessions`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `sessions` (
|
||||||
|
`id` bigint unsigned NOT NULL DEFAULT (((unix_timestamp() * 1000 * pow(2,22)) + floor((rand() * pow(2,12))))),
|
||||||
|
`user` bigint unsigned NOT NULL,
|
||||||
|
`expiry_date` timestamp NULL DEFAULT ((now() + interval 30 day)),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `user_idx` (`user`),
|
||||||
|
CONSTRAINT `user_session` FOREIGN KEY (`user`) REFERENCES `users` (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `work`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `work` (
|
||||||
|
`id` bigint unsigned NOT NULL DEFAULT (((unix_timestamp() * 1000 * pow(2,22)) + floor((rand() * pow(2,12))))),
|
||||||
|
`order` bigint unsigned NOT NULL,
|
||||||
|
`offer` bigint unsigned NOT NULL,
|
||||||
|
`price` decimal(10,2) NOT NULL,
|
||||||
|
`notes` text,
|
||||||
|
`is_fulfilled` tinyint NOT NULL DEFAULT '0',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `order_idx` (`order`),
|
||||||
|
KEY `offer_idx` (`offer`),
|
||||||
|
CONSTRAINT `offer` FOREIGN KEY (`offer`) REFERENCES `offer` (`id`),
|
||||||
|
CONSTRAINT `order` FOREIGN KEY (`order`) REFERENCES `orders` (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `work_templates`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `work_templates` (
|
||||||
|
`id` bigint unsigned NOT NULL DEFAULT (((unix_timestamp() * 1000 * pow(2,22)) + floor((rand() * pow(2,12))))),
|
||||||
|
`order_template` bigint unsigned NOT NULL,
|
||||||
|
`offer` bigint unsigned NOT NULL,
|
||||||
|
`price` decimal(10,2) NOT NULL DEFAULT '0.00',
|
||||||
|
`notes` text,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `order_template_idx` (`order_template`),
|
||||||
|
KEY `offer_idx` (`offer`),
|
||||||
|
CONSTRAINT `offer2` FOREIGN KEY (`offer`) REFERENCES `offer` (`id`),
|
||||||
|
CONSTRAINT `order_template` FOREIGN KEY (`order_template`) REFERENCES `order_templates` (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Final view structure for view `orderSummaries`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW `orderSummaries` AS
|
||||||
|
SELECT
|
||||||
|
`id`,
|
||||||
|
`client`,
|
||||||
|
`user`,
|
||||||
|
`is_draft`,
|
||||||
|
(COALESCE(`imported_products`.`price`, 0) + COALESCE(`work`.`price`, 0)) AS `value`,
|
||||||
|
COALESCE(`imported_products`.`count`, 0) as `imported_products_count`,
|
||||||
|
COALESCE(`work`.`count`, 0) as `work_count`
|
||||||
|
FROM
|
||||||
|
`orders`
|
||||||
|
LEFT JOIN
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
`order`,
|
||||||
|
SUM(`price`) as `price`,
|
||||||
|
COUNT(*) AS `count`
|
||||||
|
FROM `imported_products`
|
||||||
|
GROUP BY `order`
|
||||||
|
) as `imported_products` ON `orders`.`id` = `imported_products`.`order`
|
||||||
|
LEFT JOIN
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
`order`,
|
||||||
|
SUM(`price`) AS `price`,
|
||||||
|
COUNT(*) AS `count`
|
||||||
|
FROM `work`
|
||||||
|
GROUP BY `work`.`order`
|
||||||
|
) AS `work` ON `work`.`order` = `orders`.`id`;
|
15
server/api/firstRun.get.ts
Normal file
15
server/api/firstRun.get.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/* global defineEventHandler */
|
||||||
|
import { data, database } from "../utils/database";
|
||||||
|
|
||||||
|
export async function isFirstRun() {
|
||||||
|
const [tables] = await database.query({ sql: "SHOW TABLES", rowsAsArray: true }, []) as data<[string]>;
|
||||||
|
if (tables.length === 0) return true;
|
||||||
|
if (!tables.find(a => a[0] === "users")) return true;
|
||||||
|
const [[users]] = await database.query("SELECT COUNT(*) as `count` FROM `users`") as data<{count: number}>;
|
||||||
|
if (users.count === 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler((e) => {
|
||||||
|
return isFirstRun();
|
||||||
|
});
|
33
server/api/firstRun.post.ts
Normal file
33
server/api/firstRun.post.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/* global defineEventHandler, setResponseStatus, readBody, createError */
|
||||||
|
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
|
||||||
|
import { database as db } from "../utils/database";
|
||||||
|
import { isFirstRun } from "./firstRun.get";
|
||||||
|
import { getPasswordHash } from "./login.post";
|
||||||
|
import Snowflake from "~/utils/snowflake";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
if (!isFirstRun()) {
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(e);
|
||||||
|
if (typeof body !== "object") throw createError({ message: "Invalid body", statusCode: 400 });
|
||||||
|
const username = body.username;
|
||||||
|
if (typeof username !== "string") throw createError({ message: "username is not string", statusCode: 400 });
|
||||||
|
const password = body.password;
|
||||||
|
if (typeof password !== "string") throw createError({ message: "password is not string", statusCode: 400 });
|
||||||
|
const email = body.email;
|
||||||
|
if (typeof email !== "string") throw createError({ message: "email is not string", statusCode: 400 });
|
||||||
|
|
||||||
|
const sql = await fs.readFile("./schemaModel.sql", "utf-8");
|
||||||
|
|
||||||
|
const database = await db.new({ multipleStatements: true });
|
||||||
|
await database.query(sql);
|
||||||
|
await database.execute(
|
||||||
|
"INSERT INTO `users` (`id`, `username`, `password`, `email`) VALUES (?, ?, ?, ?)",
|
||||||
|
[new Snowflake().toString(), username, getPasswordHash(password), email]);
|
||||||
|
return "";
|
||||||
|
});
|
|
@ -6,6 +6,12 @@ import { isString } from "../utils/isString";
|
||||||
import { cookieSettings } from "../utils/rootUtils";
|
import { cookieSettings } from "../utils/rootUtils";
|
||||||
import Snowflake from "~/utils/snowflake";
|
import Snowflake from "~/utils/snowflake";
|
||||||
|
|
||||||
|
export function getPasswordHash(password: string) {
|
||||||
|
return crypto.createHmac("sha512", "42")
|
||||||
|
.update(password)
|
||||||
|
.digest();
|
||||||
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
if (getCookie(e, "token"))
|
if (getCookie(e, "token"))
|
||||||
throw createError({ statusCode: 501, message: "Case not implemented: logging in while cookie is set" });
|
throw createError({ statusCode: 501, message: "Case not implemented: logging in while cookie is set" });
|
||||||
|
@ -18,12 +24,10 @@ export default defineEventHandler(async (e) => {
|
||||||
if (!isString(login)) throw createError({ statusCode: 400, message: "Login is not string." });
|
if (!isString(login)) throw createError({ statusCode: 400, message: "Login is not string." });
|
||||||
if (!isString(password)) throw createError({ statusCode: 400, message: "Password is not string." });
|
if (!isString(password)) throw createError({ statusCode: 400, message: "Password is not string." });
|
||||||
|
|
||||||
const hashedPassword = crypto.createHmac("sha512", "42")
|
const hashedPassword = getPasswordHash(password);
|
||||||
.update(password)
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
const [account] = await database.query(
|
const [account] = await database.query(
|
||||||
"SELECT CONVERT(`id`, CHAR(32)) AS `id` from `users` WHERE `username` = ? AND LOWER(HEX(`password`)) = ? LIMIT 1",
|
"SELECT CONVERT(`id`, CHAR(32)) AS `id` from `users` WHERE `username` = ? AND `password` = ? LIMIT 1",
|
||||||
[login, hashedPassword],
|
[login, hashedPassword],
|
||||||
)as unknown as data<{id: string}>;
|
)as unknown as data<{id: string}>;
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ const endpointsWithoutAuth: string[] = [
|
||||||
"/hi",
|
"/hi",
|
||||||
"/login",
|
"/login",
|
||||||
"/logout",
|
"/logout",
|
||||||
|
"/firstRun",
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
|
@ -31,10 +32,14 @@ export default defineEventHandler(async (e) => {
|
||||||
*/
|
*/
|
||||||
export async function isAuthorised(token: string | undefined): Promise<boolean> {
|
export async function isAuthorised(token: string | undefined): Promise<boolean> {
|
||||||
if (!token) return false;
|
if (!token) return false;
|
||||||
|
try {
|
||||||
const [[session]] = await database.query(
|
const [[session]] = await database.query(
|
||||||
"SELECT EXISTS(SELECT `id` FROM `sessions` WHERE `id` = ? AND `expiry_date` >= NOW()) as `logged_in`",
|
"SELECT EXISTS(SELECT `id` FROM `sessions` WHERE `id` = ? AND `expiry_date` >= NOW()) as `logged_in`",
|
||||||
[token],
|
[token],
|
||||||
) as unknown as data<{logged_in: number}>;
|
) as unknown as data<{logged_in: number}>;
|
||||||
|
|
||||||
return session.logged_in === 1;
|
return session.logged_in === 1;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue