Compare commits

..

3 commits

Author SHA1 Message Date
90932a49c8 Add database initialization
now, when the project is ran without configured database,
it will prompt for the first user to configure
the database and add the first user
2023-06-14 13:00:19 +02:00
cbfc4e9317 fix auth check if database is uninitialised 2023-06-14 12:37:57 +02:00
75f809051c refactor out password hashing in login.post.ts
this will be used to create the first user account in an empty database
2023-06-14 11:48:31 +02:00
8 changed files with 304 additions and 9 deletions

8
middleware/firstRun.ts Normal file
View 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
View 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>

View file

@ -19,6 +19,7 @@ const redirectTo = ref(route.redirectedFrom);
definePageMeta({
layout: false,
middleware: ["first-run"],
});
async function submit() {

167
schemaModel.sql Normal file
View 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`;

View 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();
});

View 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 "";
});

View file

@ -6,6 +6,12 @@ import { isString } from "../utils/isString";
import { cookieSettings } from "../utils/rootUtils";
import Snowflake from "~/utils/snowflake";
export function getPasswordHash(password: string) {
return crypto.createHmac("sha512", "42")
.update(password)
.digest();
}
export default defineEventHandler(async (e) => {
if (getCookie(e, "token"))
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(password)) throw createError({ statusCode: 400, message: "Password is not string." });
const hashedPassword = crypto.createHmac("sha512", "42")
.update(password)
.digest("hex");
const hashedPassword = getPasswordHash(password);
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],
)as unknown as data<{id: string}>;

View file

@ -8,6 +8,7 @@ const endpointsWithoutAuth: string[] = [
"/hi",
"/login",
"/logout",
"/firstRun",
];
export default defineEventHandler(async (e) => {
@ -31,10 +32,14 @@ export default defineEventHandler(async (e) => {
*/
export async function isAuthorised(token: string | undefined): Promise<boolean> {
if (!token) return false;
try {
const [[session]] = await database.query(
"SELECT EXISTS(SELECT `id` FROM `sessions` WHERE `id` = ? AND `expiry_date` >= NOW()) as `logged_in`",
[token],
) as unknown as data<{logged_in: number}>;
return session.logged_in === 1;
} catch {
return false;
}
}