44 Commits

Author SHA1 Message Date
db1d29f510 Updated boostrap to mitigate missing icons 2024-07-02 20:14:16 +02:00
f3e306b084 AFLOW-46-ui-duplication
Added AFLOW-46

Reviewed-on: #1
2024-06-19 18:34:40 +02:00
c23b1b306c updated and properly implemented auth middleware AFLOW-32
Co-authored-by: Spacelord <git@spacelord.de>
2023-11-01 20:04:19 +01:00
2371089f88 updated config handler to autogenerate secrets and default user structure
Co-authored-by: Spacelord <Spacelord09@users.noreply.github.com>
2023-11-01 20:03:28 +01:00
6fa2797903 improved login screen (fixed layout, added error msg) 2023-08-27 19:04:55 +02:00
af896a6688 Added local authentication (AFLOW-32) 2023-08-26 20:59:46 +02:00
347979bb10 Remove login demo page route 2023-08-26 20:57:16 +02:00
ddfdfc3092 Remove old demo login page 2023-08-26 20:56:48 +02:00
56cbebb36b Add form to login page 2023-08-26 20:56:26 +02:00
e307ff97ac Add passport.js dependencies 2023-08-26 20:56:03 +02:00
f52897fd4d added todo to search 2023-07-11 16:25:58 +02:00
b0b47e04f8 introduction of contactInfo route 2023-07-11 16:24:57 +02:00
660c9c092e make sure all tooltips are hidden before showing new ones to prevent stuck tooltips 2023-07-10 18:18:38 +02:00
6b092b34b3 fix AFLOW-28 2023-07-10 18:11:24 +02:00
421085a8d5 implemented AFLOW-27 2023-07-10 18:00:24 +02:00
ea80b4bf2b - fixed issues with jumpy actions for item view
- added AFLOW-23 support for categories and rewrote interface
2023-07-10 17:50:31 +02:00
94186a3a18 removed redundant getAll attribute 2023-07-10 17:36:59 +02:00
db6df2fdc6 another fix for AFLOW-24 2023-07-10 16:06:54 +02:00
9b2db6eed7 possible fix for AFLOW-24 2023-07-10 16:05:08 +02:00
cdbd4c3c10 Merge branch 'master' of https://git.project-name-here.de/Project-Name-Here/assetflow 2023-07-10 15:51:54 +02:00
bc9d395e77 improved /version route to include git info 2023-07-10 15:51:52 +02:00
16da321177 „README.md“ ändern 2023-07-10 15:47:03 +02:00
6f7f65fa36 fix error with unsortable relations 2023-07-10 15:36:19 +02:00
1605987952 update all storagelocation / unit routes to support new tables (AFLOW-23) 2023-07-10 15:34:04 +02:00
a79a1eab81 improve /version api route to only call git once on startup 2023-07-10 15:09:30 +02:00
45a4935190 added tool for load-testing many items 2023-07-09 23:37:48 +02:00
ff07698f16 fix bug where delete would reload the page even if new data loading is enabled 2023-07-09 23:35:59 +02:00
5aeec6fb28 Fixed AFLOW-26 and AFLOW-14 in items API route 2023-07-09 23:03:15 +02:00
3be376b214 Fixed AFLOW-26 and AFLOW-14 in categories API route 2023-07-09 22:29:52 +02:00
3f55b22ede Fixed AFLOW-26 and AFLOW-14 in storageUnit API route 2023-07-09 21:59:35 +02:00
534cc3055f Fixed AFLOW-26 and AFLOW-14 in storageLocation API route 2023-07-09 21:28:31 +02:00
abb7e7bab3 introduction of proper text length limiting 2023-07-09 20:17:05 +02:00
09e74f9eb6 Fix AFLOW-22 2023-07-09 18:20:27 +02:00
720a969484 respond with proper error page when the /s/:id sku fails 2023-07-09 18:14:29 +02:00
5524f14e1a fixed error with duplicate stringify operation in api routes
Co-authored-by: Spacelord <Spacelord09@users.noreply.github.com>
2023-07-09 18:07:28 +02:00
58a2d2ad19 comment function 2023-07-08 16:18:28 +02:00
c50aa8990c - fixed bug with item route
- fixed visual bug with ids in item view
2023-07-08 00:45:22 +02:00
8d954052f2 - added loader to sort operations 2023-07-08 00:25:33 +02:00
0e4bc7669a - improved behavior when updating and searching 2023-07-08 00:22:09 +02:00
0233453084 - introduced table view with client side loading
- improved /items/ with support for pagination
- improved helper functions for tooltips and popovers
- removed console log residue from handleSidebarTriangles
- introduction of version route
2023-07-08 00:09:54 +02:00
c026b5f1a8 fix greeting to finally respect the time 2023-07-07 22:15:50 +02:00
5d99baea8e added proper version information
Co-authored-by: Spacelord <Spacelord09@users.noreply.github.com>
2023-07-07 22:09:17 +02:00
587dac99c5 - fixed visual error where empty entries where shown as null 2023-07-07 15:43:02 +02:00
45bec04007 - made sidenavbar logo lead to dashboard
- moved easter egg somewhere else
2023-07-07 15:35:01 +02:00
48 changed files with 1487 additions and 381 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ node_modules
config.json
dist
docs
demoData.json

View File

@ -13,3 +13,4 @@ LocID_Regal16_Fach7
StorageLocation_LocID_Regal16_Fach7
Please also reference our wiki at https://project-name-here.atlassian.net/wiki/spaces/AFLOW/overview

View File

@ -10,7 +10,9 @@
"/@popperjs/core/dist/umd/popper.min.js.map",
"/bootstrap/dist/js/bootstrap.bundle.min.js.map",
"/bootstrap-icons/font/fonts/bootstrap-icons.woff",
"/tsparticles-confetti/tsparticles.confetti.bundle.min.js"
"/tsparticles-confetti/tsparticles.confetti.bundle.min.js",
"/bootstrap-table/dist/bootstrap-table.min.js",
"/bootstrap-table/dist/bootstrap-table.min.css"
],
"debugMode": false
}

148
package-lock.json generated
View File

@ -16,12 +16,16 @@
"body-parser": "^1.20.2",
"bootstrap": "^5.3.0-alpha3",
"bootstrap-icons": "^1.10.5",
"bootstrap-table": "^1.22.1",
"csv": "^6.2.11",
"eta": "^2.0.1",
"express": "^4.18.2",
"express-fileupload": "^1.4.0",
"express-session": "^1.17.3",
"jquery": "^3.6.4",
"lodash": "^4.17.21",
"passport": "^0.6.0",
"passport-local": "^1.0.0",
"signale": "^1.4.0",
"tsparticles-confetti": "^2.9.3"
},
@ -29,7 +33,10 @@
"@loancrate/prisma-schema-parser": "^2.0.0",
"@types/express": "^4.17.17",
"@types/express-fileupload": "^1.4.1",
"@types/express-session": "^1.17.7",
"@types/lodash": "^4.14.194",
"@types/passport": "^1.0.12",
"@types/passport-local": "^1.0.35",
"@types/signale": "^1.4.4",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
@ -831,6 +838,15 @@
"@types/send": "*"
}
},
"node_modules/@types/express-session": {
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz",
"integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.14.194",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz",
@ -867,6 +883,36 @@
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
"dev": true
},
"node_modules/@types/passport": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz",
"integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/passport-local": {
"version": "1.0.35",
"resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.35.tgz",
"integrity": "sha512-K4eLTJ8R0yYW8TvCqkjB0pTKoqfUSdl5PfZdidTjV2ETV3604fQxtY6BHKjQWAx50WUS0lqzBvKv3LoI1ZBPeA==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-strategy": "*"
}
},
"node_modules/@types/passport-strategy": {
"version": "0.2.35",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
"integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*"
}
},
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@ -1299,6 +1345,14 @@
}
]
},
"node_modules/bootstrap-table": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.22.1.tgz",
"integrity": "sha512-Nw8p+BmaiMDSfoer/p49YeI3vJQAWhudxhyKMuqnJBb3NRvCRewMk7JDgiN9SQO3YeSejOirKtcdWpM0dtddWg==",
"peerDependencies": {
"jquery": "3"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -2328,6 +2382,32 @@
"node": ">=12.0.0"
}
},
"node_modules/express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"dependencies": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@ -4202,6 +4282,14 @@
"node": ">= 0.8"
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -4413,6 +4501,42 @@
"node": ">= 0.8"
}
},
"node_modules/passport": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
"integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==",
"dependencies": {
"passport-strategy": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -4460,6 +4584,11 @@
"node": ">=8"
}
},
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@ -5107,6 +5236,14 @@
"node": ">=8"
}
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -6360,6 +6497,17 @@
"node": ">=12.20"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

View File

@ -23,13 +23,17 @@
"@sentry/tracing": "^7.52.1",
"body-parser": "^1.20.2",
"bootstrap": "^5.3.0-alpha3",
"bootstrap-icons": "^1.10.5",
"bootstrap-icons": "^1.11.0",
"bootstrap-table": "^1.22.1",
"csv": "^6.2.11",
"eta": "^2.0.1",
"express": "^4.18.2",
"express-fileupload": "^1.4.0",
"express-session": "^1.17.3",
"jquery": "^3.6.4",
"lodash": "^4.17.21",
"passport": "^0.6.0",
"passport-local": "^1.0.0",
"signale": "^1.4.0",
"tsparticles-confetti": "^2.9.3"
},
@ -37,7 +41,10 @@
"@loancrate/prisma-schema-parser": "^2.0.0",
"@types/express": "^4.17.17",
"@types/express-fileupload": "^1.4.1",
"@types/express-session": "^1.17.7",
"@types/lodash": "^4.14.194",
"@types/passport": "^1.0.12",
"@types/passport-local": "^1.0.35",
"@types/signale": "^1.4.4",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",

View File

@ -32,12 +32,13 @@ enum itemStatus {
lost
}
// comments and descriptions -> @db.VarChar(2048)
model Item {
id Int @id @unique @default(autoincrement())
SKU String? @unique
amount Int @default(1)
name String
comment String?
comment String? @db.VarChar(2048)
status itemStatus @default(normal) /// TODO: Would it be better to create a separate model for this as well instead of providing several static statuses to choose from(enum)?
contactInfo contactInfo? @relation(fields: [contactInfoId], references: [id])
@ -81,7 +82,7 @@ model StorageUnit {
model itemCategory {
id Int @id @default(autoincrement())
name String @unique
description String?
description String? @db.VarChar(2048)
Item Item[]
}

View File

@ -1,5 +1,6 @@
import fs from 'node:fs';
import _ from 'lodash';
import { randomUUID, randomBytes } from 'crypto';
export type configObject = Record<any, any>;
@ -14,6 +15,7 @@ export default class config {
#configPath: string;
//global = {[key: string] : string}
global: configObject;
replaceSecrets: boolean;
/**
* Creates an instance of config.
@ -22,9 +24,10 @@ export default class config {
* @param {string} configPath Path to config file.
* @param {object} configPreset Default config object with default values.
*/
constructor(configPath: string, configPreset: object) {
constructor(configPath: string, replaceSecrets: boolean, configPreset: object) {
this.#configPath = configPath;
this.global = configPreset;
this.replaceSecrets = replaceSecrets;
try {
// Read config
@ -52,6 +55,12 @@ export default class config {
*/
save_config() {
try {
// If enabled replace tokens defines as "gen" with random token
if (this.replaceSecrets) {
// Replace tokens with value "gen"
this.generate_secrets(this.global, 'gen')
}
fs.writeFileSync(this.#configPath, JSON.stringify(this.global, null, 8));
} catch (err) {
console.error(`Could not write config file at ${this.#configPath} due to: ${err}`);
@ -59,31 +68,73 @@ export default class config {
}
console.log(`Successfully written config file to ${this.#configPath}`);
}
/**
* Replaces each item matching the value of placeholder with a random UUID.
* Thanks to https://stackoverflow.com/questions/8085004/iterate-through-nested-javascript-objects
* @param {configObject} obj
*/
generate_secrets(obj: configObject, placeholder: string) {
const stack = [obj];
while (stack?.length > 0) {
const currentObj = stack.pop();
Object.keys(currentObj).forEach((key) => {
if (currentObj[key] === placeholder) {
console.log('Generating secret: ' + key);
currentObj[key] = randomBytes(48).toString('base64').replace(/\W/g, '');
}
if (typeof currentObj[key] === 'object' && currentObj[key] !== null) {
stack.push(currentObj[key]);
}
});
}
}
}
/*
**** Example ****
import configHandler from './assets/configHandler.js';
import ConfigHandlerNG from './assets/configHandlerNG.js';
// Create a new config instance.
export const config = new ConfigHandler(__path + '/config.json', {
db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE',
http_listen_address: '127.0.0.1',
http_port: 3000,
sentry_dsn: 'https://ID@sentry.example.com/PROJECTID',
debug: false
});
export const config = new ConfigHandler(__path + '/config.json', true, {
test1: 't1',
test2: 't2',
test3: 'gen',
test4: 't4',
test5: 'gen',
testObj: {
local: {
active: true,
users: {
user1: 'gen',
user2: 'gen',
user3: 'gen',
user4: 'gen',
}
},
oidc: {
active: false
}
}
});
console.log('Base Config:');
console.log(config.global);
console.log('Add some new key to config and call save_config.');
console.log('Add some new key to config and call save_config().');
config.global.NewKey = 'ThisIsANewKey!'
config.save_config()
console.log('This will add a new key with value gen, but gen gets replaced with a random UUID when save_config() is called.');
config.global.someSecret = 'gen'
config.save_config() // global.someSecret is getting replaced with some random UUID since it was set to 'gen'.
console.log('Complete Config:');
console.log(config.global);
*/

View File

@ -72,7 +72,7 @@ export function parseIntRelation(data: string, relation_name: string = 'id', doN
// This can be used by prisma to connect relations
// If the incoming data is null or empty, return a prisma disconnect object instead of a connect one
if (data === null || data === '') {
if (data === null || data === '' || data === "undefined") {
if (doNotDisconnect) {
return undefined;
}
@ -88,6 +88,26 @@ export function parseIntRelation(data: string, relation_name: string = 'id', doN
}`);
}
export function parseIntOrUndefined(data: string) {
/**
* Function to parse a string into a number or return undefined if it is not a number
*
* @export
* @param {string || any} data
* @returns {object}
*/
export function parseIntOrUndefined(data: any) {
return isNaN(parseInt(data)) ? undefined : parseInt(data);
}
/**
* A function to create a sortBy compatible object from a string
*
* @export
* @param {string} SortField
* @param {string} Order
* @returns {object}
*/
export function parseDynamicSortBy(SortField: string, Order: string){
return JSON.parse(`{ "${SortField}": "${Order}" }`);
}

View File

@ -4,9 +4,33 @@
<div class="background text-center">
<div class="row align-items-start">
<div class="col-9"></div>
<div class="col-3 sidePanel ps-4 text-black">
<div class="col-3 sidePanel ps-4 pe-4 text-black">
<h1>Log into AssetFlow</h1>
<div class="alert alert-danger" role="alert" id="passwordAlarm">
User does not exist or password is incorrect.
</div>
<form action="/auth/login" method="post">
<div class="mb-3">
<label for="userName" class="form-label">Username</label>
<input name="username" type="text" class="form-control" id="userName" aria-describedby="userNameHelp" />
<!-- <div id="userNameHelp" class="form-text">We'll never share your email with anyone else.</div> -->
</div>
<div class="mb-3">
<label for="userPassword" class="form-label">Password</label>
<input name="password" type="password" class="form-control" id="userPassword" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
<script>
// if url parameter ?failed is set, show the password alarm
if (window.location.search.includes("failed")) {
document.getElementById("passwordAlarm").style.display = "block";
} else {
document.getElementById("passwordAlarm").style.display = "none";
}
</script>
<%~ E.includeFile("../partials/foot.eta.html") %>
</div>

View File

@ -1,6 +1,19 @@
<%~ E.includeFile("partials/head.eta.html", {"title": "Dashboard"}) %> <%~ E.includeFile("partials/controls.eta.html", {"active": "Dashboard"}) %>
<h1>Good evening, ${user}</h1>
<h1 onclick="doTheConfetti()" class="user-select-none" id="greeting">Good evening, ${user}</h1>
<script>
// Handle greeting
var today = new Date();
var curHr = today.getHours();
if (curHr < 12) {
document.getElementById("greeting").innerHTML = "Good morning";
} else if (curHr < 18) {
document.getElementById("greeting").innerHTML = "Good afternoon";
} else {
document.getElementById("greeting").innerHTML = "Good evening";
}
</script>
<div class="container text-center">
<div class="row">
<div class="card col m-2">

View File

@ -4,7 +4,7 @@
<h1><%= it.name %></h1>
<div class="container">
<p><strong>Comment:</strong> <%= it.comment %></p>
<p><strong>Category:</strong> <%= it.category.name %></p>
<p><strong>Category:</strong> <% if (it.category == null) { %> <i>No category assigned</i> <% } else { %> <%= it.category.name %> <% } %></p>
<p><strong>Amount:</strong> <%= it.amount %></p>
<p><strong>SKU:</strong> <%= it.SKU %></p>
<p><strong>Status: </strong><% if(it.status == "normal") { %>

View File

@ -11,17 +11,17 @@
<div class="modal-body">
<div class="mb-3">
<label for="itemModifyModalName" class="form-label">Name</label>
<input type="text" class="form-control" id="itemModifyModalName" name="name" required />
<input type="text" class="form-control" id="itemModifyModalName" name="name" maxlength="128" required />
<div id="itemModifyModalNameText" class="form-text">This name should be unqiue.</div>
</div>
<div class="mb-3">
<label for="itemModifyModalComment" class="form-label">Comment</label>
<input type="text" class="form-control" id="itemModifyModalComment" name="comment" />
<input type="text" class="form-control" id="itemModifyModalComment" maxlength="2048" name="comment" />
<div id="itemModifyModalDescText" class="form-text">Optional</div>
</div>
<div class="mb-3">
<label for="itemModifyModalStorageLocation" class="form-label">Select a storage location</label>
<select class="form-select" id="itemModifyModalStorageLocation" name="storageLocation">
<select class="form-select" id="itemModifyModalStorageLocation" name="storageLocationId">
<option value=""><i>Do not assign a storage location</i></option>
<% it.storeLocs.forEach(function(locs){ %>
<option value="<%= locs.id %>"><%= locs.name %></option>
@ -36,12 +36,12 @@
</div>
<div class="mb-3">
<label for="itemModifyModalSKU" class="form-label">SKU</label>
<input type="text" class="form-control" id="itemModifyModalSKU" name="sku" />
<input type="text" class="form-control" id="itemModifyModalSKU" maxlength="64" name="sku" />
<div id="itemModifyModalSKUText" class="form-text">Optional</div>
</div>
<div class="mb-3">
<label for="itemModifyModalManuf" class="form-label">Manufacturer</label>
<input type="text" class="form-control" id="itemModifyModalManuf" name="manufacturer" />
<input type="text" class="form-control" id="itemModifyModalManuf" maxlength="190" name="manufacturer" />
<div id="itemModifyModalSKUText" class="form-text">Optional</div>
</div>
<div class="mb-3">
@ -93,13 +93,14 @@
<a href="/settings/category/new" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#itemModifyModal" onclick="primeCreateNew()">Create new item</a>
</div>
</div>
<table class="table align-middle">
<table class="table align-middle" id="itemList" data-sortable="true" data-search-highlight="true" data-pagination="true" data-page-size="25" data-remember-order="true">
<thead>
<tr>
<th scope="col">SKU</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
<th scope="col" data-field="SKU" class="sku" data-sortable="true">SKU</th>
<th scope="col" data-field="name" data-sortable="true">Name</th>
<th scope="col" data-field="comment" data-sortable="true" data-width="80">Comment</th>
<th scope="col" data-field="status" data-sortable="true">Status</th>
<th scope="col" data-field="actions" data-sortable="false" data-searchable="false" data-width="160">Actions</th>
</tr>
</thead>
<% if(it.items.length == 0) { %>
@ -109,49 +110,8 @@
</tr>
</tbody>
<% } %>
<tbody>
<% it.items.forEach(function(user){ %>
<tr>
<td scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= user.id %>">
<% if (user.SKU == null) { %>
<i>No SKU assigned</i>
<% } else { %> <%= user.SKU %> <% } %></td>
<td><%= user.name %></td>
<% if(user.status == "normal") { %>
<td><span class="badge text-bg-success"><%= user.status %></span></td>
<% } else if(user.status == "stolen") { %>
<td><span class="badge text-bg-danger"><%= user.status %></span></td>
<% } else if(user.status == "lost") { %>
<td><span class="badge text-bg-warning"><%= user.status %></span></td>
<% } else if(user.status == "borrowed") { %>
<td><span class="badge text-bg-info"><%= user.status %></span></td>
<% } %>
<td>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#itemModifyModal" onclick="primeEdit(); getDataForEdit('<%= user.id %>')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('<%= user.id %>','items','Item')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<% }) %>
</tbody>
</table>
<br />
<% if(it.maxPages > 1) { %>
<nav aria-label="Page selector">
<ul class="pagination justify-content-center">
<li class="page-item <%= it.currentPage-1 < 1 ? 'disabled' : ''%>"><a class="page-link" href="?page=<%= it.currentPage - 1 %>">Previous</a></li>
<% for (var i = 1; i <= it.maxPages; i++) { %>
<li class="page-item <%= it.currentPage == i ? 'active' : ''%>"><a class="page-link" href="?page=<%= i %>"><%= i %></a></li>
<% } %>
<li class="page-item <%= it.currentPage+1 > it.maxPages ? 'disabled' : ''%>"><a class="page-link" href="?page=<%= it.currentPage + 1 %>">Next</a></li>
</ul>
</nav>
<% } %>
</div>
<script src="/js/editItems.js"></script>
<script src="/js/itemPageHandler.js"></script>
<%~ E.includeFile("partials/controlsFoot.eta.html") %> <%~ E.includeFile("partials/foot.eta.html") %>

View File

@ -20,12 +20,12 @@
<div class="modal-body">
<div class="mb-3">
<label for="editCategoryModalName" class="form-label">Name</label>
<input type="text" class="form-control" id="editCategoryModalName" name="name" required />
<input type="text" class="form-control" id="editCategoryModalName" maxlength="128" name="name" required />
<div id="editCategoryModalNameText" class="form-text">This name should be unqiue.</div>
</div>
<div class="mb-3">
<label for="editCategoryModalDescription" class="form-label">Description</label>
<input type="text" class="form-control" id="editCategoryModalDescription" name="description" />
<input type="text" class="form-control" id="editCategoryModalDescription" maxlength="2048" name="description" />
<div id="editCategoryModalDescText" class="form-text">Optional</div>
</div>
<input type="text" id="editCategoryModalId" name="id" hidden />
@ -40,31 +40,15 @@
</div>
<!-- Table with all categories -->
<table class="table align-middle">
<table class="table align-middle" id="itemList" data-sortable="true" data-search-highlight="true" data-pagination="true" data-page-size="25" data-remember-order="true">
<thead>
<tr>
<!-- <th scope="col">#</th> -->
<th scope="col">Name</th>
<th scope="col">Description</th>
<th scope="col">Action</th>
<th scope="col" data-field="name" data-sortable="true" data-width="300">Name</th>
<th scope="col" data-field="description" data-sortable="true">Description</th>
<th scope="col" data-field="actions" data-sortable="false" data-searchable="false" data-width="160">Actions</th>
</tr>
</thead>
<tbody>
<% it.items.forEach(function(user){ %>
<tr id="listEntry-<%= user.id %>">
<td scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= user.id %>"><%= user.name %></td>
<td><%= user.description %></td>
<td>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#editCategoryModal" onclick="primeEdit(); getDataForEdit('<%= user.name %>')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('<%= user.name %>','categories','Category', 'name')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<script src="/js/editCategory.js"></script>

View File

@ -1,7 +1,7 @@
<%~ E.includeFile("../partials/head.eta.html", {"title": "Settings"}) %> <%~ E.includeFile("../partials/controls.eta.html", {"active": "SETT"}) %>
<h1>Manage your AssetFlow instance</h1>
<div class="alert alert-success" role="alert">A new version is available. <a href="#" class="alert-link">Click here to update</a></div>
<div class="alert alert-success" role="alert" id="updateNotifier">A new version is available. <a href="https://git.project-name-here.de/Project-Name-Here/assetflow/releases" class="alert-link">Click here to update</a></div>
<div class="container text-center">
<div class="row">
<a class="card col m-2" href="/manage/categories">
@ -30,5 +30,16 @@
</a>
</div>
</div>
<script>
$(document).ready(function () {
$.getJSON("/api/v1/version", function (data) {
if (data.updateAvailable) {
$("#updateNotifier").show();
// $("#updateNotifier").find(".alert-link").attr("href", data.url);
}else {
$("#updateNotifier").hide();
}
});
});
</script>
<%~ E.includeFile("../partials/controlsFoot.eta.html") %> <%~ E.includeFile("../partials/foot.eta.html") %>

View File

@ -13,7 +13,7 @@
<div class="modal-body">
<div class="mb-3">
<label for="storageLocationModalName" class="form-label">Name</label>
<input type="text" class="form-control" id="storageLocationModalName" name="name" required />
<input type="text" class="form-control" id="storageLocationModalName" name="name" maxlength="128" required />
<div id="storageLocationModalNameText" class="form-text">This name should be unqiue.</div>
</div>
<div class="mb-3">
@ -150,37 +150,16 @@
>
</div>
</div>
<table class="table align-middle">
<table class="table align-middle" id="itemList" data-sortable="true" data-search-highlight="true" data-pagination="true" data-page-size="25" data-remember-order="true">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Storage Unit</th>
<th scope="col">Actions</th>
<th scope="col" data-field="name" data-sortable="true">Name</th>
<th scope="col" data-field="storageUnit" data-sortable="false">Storage Unit</th>
<th scope="col" data-field="actions" data-sortable="false" data-searchable="false" data-width="160">Actions</th>
</tr>
</thead>
<tbody>
<% it.storLocs.forEach(function(locations){ %>
<tr id="listEntry-<%= locations.id %>">
<td scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= locations.id %>"><%= locations.name %></td>
<td>
<% if (locations.storageUnit == null) { %>
<i>No storage unit connected</i>
<% } else { %> <%= locations.storageUnit.name %> <% } %>
</td>
<td>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageLocationModal" onclick="primeEdit(); getDataForEditLoc('<%= locations.id %>')">
<i class="bi bi-pencil"></i>
</button>
<button
class="btn btn-danger"
onclick="preFillDeleteModalNxt('<%= locations.id %>','storageLocations','Storage Location')"
data-bs-toggle="modal"
data-bs-target="#staticBackdrop">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
@ -195,29 +174,15 @@
<a class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageUnitModal" onclick="primeCreateNew()"><i class="bi bi-building-add"></i> Create new unit</a>
</div>
</div>
<table class="table align-middle">
<table class="table align-middle" id="itemListUnit" data-sortable="true" data-search-highlight="true" data-pagination="true" data-page-size="25" data-remember-order="true">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Address</th>
<th scope="col">Actions</th>
<th scope="col" data-field="name" data-sortable="true">Name</th>
<th scope="col "data-field="address" data-sortable="false">Address</th>
<th scope="col" data-field="actions" data-searchable="false" data-width="160">Actions</th>
</tr>
</thead>
<tbody>
<% it.storUnits.forEach(function(units){ %>
<tr id="listEntry-<%= units.id %>">
<td scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= units.id %>"><%= units.name %></td>
<td><%= units.contactInfo.street %> <%= units.contactInfo.houseNumber %>, <%= units.contactInfo.city %> <%= units.contactInfo.country %></td>
<td>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageUnitModal" onclick="primeEdit(); getDataForEdit('<%= units.id %>')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('<%= units.id %>', 'storageUnits', 'Storage Unit')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>

View File

@ -1,6 +1,6 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary sticky-top navShadow" style="z-index: 999">
<div class="container-fluid">
<a class="navbar-brand user-select-none ms-2" onclick="doTheConfetti()" style="cursor: default">
<a class="navbar-brand user-select-none ms-2" style="cursor: default" href="/">
<img alt="AssetFlow Logo" draggable="false" class="me-2 headLogo" src="/logo/Design_icon.svg"/> AssetFlow</a>
<button
class="navbar-toggler position-absolute d-md-none collapsed"
@ -137,7 +137,7 @@
</div>
<!-- Align the mode picker at the bottom of the navbar -->
<ul class="nav flex-column mb-2 position-absolute bottom-0 align-items-center w-100">
<ul class="nav flex-column mb-5 position-absolute bottom-0 align-items-center w-100">
<div class="input-group mb-3 justify-content-center w-100">
<label class="btn btn-secondary" for="mode_light"><i class="bi bi-brightness-high"></i></label>
<input type="radio" class="btn-check" name="options" id="mode_light" autocomplete="off" />
@ -177,7 +177,23 @@
});
</script>
</ul>
<div onclick="toggleAutoReload();" class="text-secondary versionInfo nav flex-column position-absolute bottom-0 align-items-center w-100">AssetFlow Alpha V0.0.1 </div>
<div onclick="toggleAutoReload();" class="text-secondary versionInfo nav flex-column position-absolute bottom-0 align-items-center w-100" id="versionInfo">AssetFlow Alpha <i>No version info</i> </div>
<script>
// Request /api/v1/version
// If the response is 200, set the commit hash
$.ajax({
type: "GET",
url: "/api/v1/version",
dataType: 'json',
success: function (data) {
$('#versionInfo').text(`AssetFlow Alpha ${data.version} ${data.commit}`);
},
error: function (data) {
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Unable to load version information', "text-bg-danger", 3000, false)
}
});
</script>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4" style="min-height: 100%">

View File

@ -6,6 +6,17 @@
<script src="/js/handleSidebarTriangles.js"></script>
<script src="/js/formHandler.js"></script>
<script>
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
</script>
function activateTooltips(){
// Enable all bootstrap tooltips.
// https://getbootstrap.com/docs/5.3/components/tooltips/#enable-tooltips
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
}
function activatePopovers(){
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
}
activatePopovers();
activateTooltips();
</script>

View File

@ -1,8 +1,2 @@
<script>
// Enable all bootstrap tooltips.
// https://getbootstrap.com/docs/5.3/components/tooltips/#enable-tooltips
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
</script>
</body>
</html>

View File

@ -8,7 +8,6 @@
<title>AssetFlow - <%= it.title %></title>
<meta name="author" content="[Project-Name-Here]" />
<!--<link rel="icon" href="/favicon.ico" />-->
<link rel="icon" href="/logo/Design_icon.svg" type="image/svg+xml" />
<script src="/js/handleColorMode.js"></script>
@ -21,7 +20,8 @@
<script src="/static/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="/static/tsparticles-confetti/tsparticles.confetti.bundle.min.js"></script>
<link rel="stylesheet" href="/static/bootstrap-table/dist/bootstrap-table.min.css">
<script src="/static/bootstrap-table/dist/bootstrap-table.min.js"></script>
</head>
<body>
<!-- The body and html tag need to be left open! -->

View File

@ -5,6 +5,9 @@ import fileUpload from 'express-fileupload';
import { PrismaClient } from '@prisma/client';
import * as eta from 'eta';
import bodyParser from 'body-parser';
import session from 'express-session';
import passport from 'passport';
import _ from 'lodash';
// Sentry
import * as Sentry from '@sentry/node';
@ -28,18 +31,40 @@ export const log = {
core: coreLogger,
db: coreLogger.scope('DB'),
web: coreLogger.scope('WEB'),
auth: coreLogger.scope('AUTH'),
helper: coreLogger.scope('HELPER')
};
// Create a new config instance.
export const config = new ConfigHandler(__path + '/config.json', {
export const config = new ConfigHandler(__path + '/config.json', true, {
db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE',
http_listen_address: '127.0.0.1',
http_port: 3000,
sentry_dsn: 'https://ID@sentry.example.com/PROJECTID',
debug: false
debug: false,
auth: {
cookie_secret: 'gen',
cookie_secure: true,
local: {
active: true,
users: {}
},
oidc: {
active: false
}
}
});
// If no local User exists, create the default with a generated password
if (_.isEqual(config.global.auth.local.users, {})) {
config.global.auth.local.users = {
'flowAdmin': 'gen',
};
config.save_config();
}
// TODO: Add errorhandling with some sort of message.
export const prisma = new PrismaClient({
datasources: {
db: {
@ -67,6 +92,17 @@ Sentry.init({
environment: config.global.debug ? 'development' : 'production'
});
// TODO: Version check need to be rewritten.
app.locals.versionRevLong = require('child_process').execSync('git rev-parse HEAD').toString().trim();
app.locals.versionRev = require('child_process').execSync('git rev-parse --short HEAD').toString().trim();
app.locals.versionRevLatest = require('child_process').execSync('git ls-remote --refs -q').toString().trim().split('\t')[0];
if (app.locals.versionRevLong === app.locals.versionRevLatest) {
log.core.info(`Running Latest Version (${app.locals.versionRevLong})`);
} else {
log.core.info(`Running Version: ${app.locals.versionRevLong} (Latest: ${app.locals.versionRevLatest})`);
}
// RequestHandler creates a separate execution context using domains, so that every
// transaction/span/breadcrumb is attached to its own Hub instance
app.use(Sentry.Handlers.requestHandler());
@ -82,14 +118,23 @@ app.use(bodyParser.urlencoded({ extended: false }));
// Using bodyParser to parse JSON bodies into JS objects
app.use(bodyParser.json());
// Session store
// TODO: Move secret to config -> Autogenerate.
app.use(
session({
secret: config.global.auth.cookie_secret,
resave: false,
saveUninitialized: false,
cookie: { secure: config.global.auth.cookie_secure }
})
);
app.use(passport.authenticate('session'));
app.use(fileUpload());
app.use(express.static(__path + '/static'));
app.use(routes);
// The error handler must be before any other error middleware and after all controllers
app.use(Sentry.Handlers.errorHandler());
app.listen(config.global.http_port, config.global.http_listen_address, () => {
log.web.info(`Listening at http://${config.global.http_listen_address}:${config.global.http_port}`);
});

21
src/middleware/auth.mw.ts Normal file
View File

@ -0,0 +1,21 @@
export function checkAuthentication(req: any, res: any, next: Function) {
if (req.isAuthenticated()) {
//req.isAuthenticated() will return true if user is logged in
next();
} else {
res.redirect('/auth/login');
}
}
// const checkIsInRole = (...roles) => (req, res, next) => {
// if (!req.user) {
// return res.redirect('/login')
// }
// const hasRole = roles.find(role => req.user.role === role)
// if (!hasRole) {
// return res.redirect('/login')
// }
// return next()
// }

View File

@ -1,38 +1,104 @@
import { Request, Response } from 'express';
import { prisma, __path, log } from '../../../index.js';
import { parseIntOrUndefined, parseDynamicSortBy } from '../../../assets/helper.js';
// Get category.
function get(req: Request, res: Response) {
// Get category
async function get(req: Request, res: Response) {
// Check if required fields are present.
if (!req.query.name) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
return;
if (req.query.sort === undefined) {
req.query.sort = 'id';
}
if (req.query.order === undefined) {
req.query.order = 'asc';
}
if (req.query.search === undefined) {
req.query.search = '';
}
prisma.itemCategory
.findUnique({
if (req.query.name) {
prisma.itemCategory
.findUnique({
where: {
name: req.query.name.toString()
}
})
.then((item) => {
if (item) {
res.status(200).json(item);
} else {
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Category does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
} else {
// Get all items
const itemCountNotFiltered = await prisma.itemCategory.count({});
// Get all items (filtered)
const itemCountFiltered = await prisma.itemCategory.count({
where: {
name: req.query.name.toString()
}
})
.then((item) => {
if (item) {
res.status(200).json(JSON.stringify(item));
} else {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'Category does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
OR: [
{
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
{
description: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
}
]
},
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
});
prisma.itemCategory
.findMany({
take: parseIntOrUndefined(req.query.limit),
skip: parseIntOrUndefined(req.query.offset),
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()),
where: {
OR: [
{
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
{
description: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
}
]
},
})
.then((items) => {
if (items) {
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
} else {
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}
}
// Create category.
function post(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.name) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
@ -48,18 +114,21 @@ function post(req: Request, res: Response) {
}
})
.then((data) => {
res.status(201).json({ status: 'created', id: data.id });
res.status(201).json({ status: 'CREATED', message: 'Successfully created category', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ errorcode: 'EXISTING', error: 'Category already exists' });
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Category already exists' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
@ -68,7 +137,7 @@ function post(req: Request, res: Response) {
async function patch(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id || !req.body.name) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
@ -81,12 +150,12 @@ async function patch(req: Request, res: Response) {
});
if (result === null) {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'Category does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Category does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
prisma.itemCategory
@ -97,20 +166,27 @@ async function patch(req: Request, res: Response) {
data: {
name: req.body.name,
description: req.body.description
},
select: {
id: true
}
})
.then(() => {
res.status(201).json({ status: 'updated' });
.then((data) => {
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated category', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ errorcode: 'EXISTING', error: 'Category already exists' });
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Category already exists' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
@ -119,7 +195,7 @@ async function patch(req: Request, res: Response) {
async function del(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
@ -132,12 +208,12 @@ async function del(req: Request, res: Response) {
});
if (result === null) {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'Category does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Category does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
prisma.itemCategory
@ -147,11 +223,11 @@ async function del(req: Request, res: Response) {
}
})
.then(() => {
res.status(200).json({ errorcode: 'DELETED', error: 'Sucessfully deleted entry' });
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted category' });
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}

View File

@ -0,0 +1,244 @@
import { Request, Response } from 'express';
import { prisma, __path, log } from '../../../index.js';
import { parseIntOrUndefined, parseDynamicSortBy } from '../../../assets/helper.js';
// Get category
async function get(req: Request, res: Response) {
// Check if required fields are present.
if (req.query.sort === undefined) {
req.query.sort = 'id';
}
if (req.query.order === undefined) {
req.query.order = 'asc';
}
if (req.query.search === undefined) {
req.query.search = '';
}
if (req.query.id) {
prisma.contactInfo
.findUnique({
where: {
id: parseInt(req.query.id.toString())
}
})
.then((item) => {
if (item) {
res.status(200).json(item);
} else {
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'ContactInfo does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
} else {
// Get all items
const itemCountNotFiltered = await prisma.contactInfo.count({});
// Get all items (filtered)
const itemCountFiltered = await prisma.contactInfo.count({
where: {
OR: [
{
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
{
street: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
}
]
},
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
});
prisma.contactInfo
.findMany({
take: parseIntOrUndefined(req.query.limit),
skip: parseIntOrUndefined(req.query.offset),
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()),
where: {
OR: [
{
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
{
street: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
}
]
},
})
.then((items) => {
if (items) {
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
} else {
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'ContactInfo does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}
}
// Create category.
function post(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.street || !req.body.houseNumber || !req.body.zipCode || !req.body.city || !req.body.country) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
// Save data.
prisma.contactInfo
.create({
data: {
name: req.body.name,
lastName: req.body.lastName,
street: req.body.street,
houseNumber: req.body.houseNumber,
zipCode: req.body.zipCode,
city: req.body.city,
country: req.body.country,
},
select: {
id: true
}
})
.then((data) => {
res.status(201).json({ status: 'CREATED', message: 'Successfully created contactInfo', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'ContactInfo already exists' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
// Update category.
async function patch(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id || !req.body.name) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
// Check if the category id exists. If not return 410 Gone.
try {
const result = await prisma.contactInfo.findUnique({
where: {
id: parseInt(req.body.id)
}
});
if (result === null) {
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Category does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
prisma.contactInfo
.update({
where: {
id: parseInt(req.body.id)
},
data: {
name: req.body.name,
lastName: req.body.lastName,
street: req.body.street,
houseNumber: req.body.houseNumber,
zipCode: req.body.zipCode,
city: req.body.city,
country: req.body.country,
},
select: {
id: true
}
})
.then((data) => {
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated category', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Category already exists' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
// Delete category
async function del(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
// Does the id exist? If not return 410 Gone.
try {
const result = await prisma.contactInfo.findUnique({
where: {
id: parseInt(req.body.id)
}
});
if (result === null) {
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'ContactInfo does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
prisma.contactInfo
.delete({
where: {
id: parseInt(req.body.id)
}
})
.then(() => {
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted contactInfo' });
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}
export default { get, post, patch, del };

View File

@ -1,4 +1,5 @@
import express from 'express';
import passport from 'passport';
// Route imports
import testRoute from './test.js';
@ -6,6 +7,8 @@ import itemRoute from './items.js';
import categoryRoute from './categories.js';
import storageUnitRoute from './storageUnits.js';
import storageLocationRoute from './storageLocations.js';
import contactInfo from './contactInfo.js';
import versionRoute from './version.js'
import search_routes from './search/index.js';
@ -27,7 +30,9 @@ Router.route('/categories').get(categoryRoute.get).post(categoryRoute.post).patc
// TODO: Migrate routes to lowercase.
Router.route('/storageUnits').get(storageUnitRoute.get).post(storageUnitRoute.post).patch(storageUnitRoute.patch).delete(storageUnitRoute.del);
Router.route('/storageLocations').get(storageLocationRoute.get).post(storageLocationRoute.post).patch(storageLocationRoute.patch).delete(storageLocationRoute.del);
Router.route('/contactInfo').get(contactInfo.get).post(contactInfo.post).patch(contactInfo.patch).delete(contactInfo.del);
Router.route('/version').get(versionRoute.get);
Router.use('/search', search_routes);
Router.route('/test').get(testRoute.get);

View File

@ -1,19 +1,24 @@
import { Request, Response } from 'express';
import { prisma, __path, log } from '../../../index.js';
import { itemStatus } from '@prisma/client';
import { parseIntRelation, parseIntOrUndefined } from '../../../assets/helper.js';
import { parseIntRelation, parseIntOrUndefined, parseDynamicSortBy } from '../../../assets/helper.js';
// Get item.
function get(req: Request, res: Response) {
if (req.query.getAll === undefined) {
// Check if required fields are present
if (!req.query.id) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
return;
}
async function get(req: Request, res: Response) {
// Set sane defaults if undefined.
if (req.query.sort === undefined) {
req.query.sort = 'id';
}
if (req.query.order === undefined) {
req.query.order = 'asc';
}
if (req.query.search === undefined) {
req.query.search = '';
}
if (req.query.id) {
// Check if number is a valid integer
if (!Number.isInteger(parseInt(req.query.id.toString()))) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'The id field must be an integer' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
prisma.item
@ -38,18 +43,63 @@ function get(req: Request, res: Response) {
})
.then((items) => {
if (items) {
res.status(200).json(JSON.stringify(items));
res.status(200).json(items);
} else {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'Item does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
} else {
// Get all items
const itemCountNotFiltered = await prisma.item.count({});
// Get all items (filtered)
const itemCountFiltered = await prisma.item.count({
where: {
OR: [
{
SKU: {
// Probably use prisma's Full-text search if it's out of beta
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
{
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
}
]
},
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
});
// log.core.debug('Dynamic relation:', parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()));
prisma.item
.findMany({
take: parseIntOrUndefined(req.query.limit),
skip: parseIntOrUndefined(req.query.offset),
where: {
OR: [
{
SKU: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
{
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
}
]
},
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()),
// Get contactInfo, category, storageLocation( storageUnit<contactInfo> ) from relations.
include: {
contactInfo: true,
@ -67,14 +117,14 @@ function get(req: Request, res: Response) {
})
.then((items) => {
if (items) {
res.status(200).json(JSON.stringify(items));
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
} else {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'Item does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}
}
@ -83,13 +133,13 @@ function get(req: Request, res: Response) {
function post(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.name) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
// Check if status is valid.
if (req.body.status !== undefined && !Object.keys(itemStatus).includes(req.body.status)) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: `Status is not valid, valid values are: ${Object.keys(itemStatus).join(', ')}` });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: `Status is not valid, valid values are: ${Object.keys(itemStatus).join(', ')}` });
return;
}
@ -97,7 +147,7 @@ function post(req: Request, res: Response) {
.create({
data: {
SKU: req.body.sku,
amount: parseIntOrUndefined(req.body.ammount), // FIXME: This is silently failing if NaN..
amount: parseIntOrUndefined(req.body.amount), // FIXME: This is silently failing if NaN..
name: req.body.name,
comment: req.body.comment,
status: req.body.status, // Only enum(itemStatus) values are valid
@ -120,22 +170,26 @@ function post(req: Request, res: Response) {
}
})
.then((data) => {
res.status(201).json({ status: 'created', id: data.id });
res.status(201).json({ status: 'CREATED', message: 'Successfully created item', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ errorcode: 'EXISTING', error: 'Item already exists' });
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Item already exists' });
} else if (err.code == 'P2003') {
// P2003 -> "Foreign key constraint failed on the field: {field_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
// FIXME: Is this errormessage right?
res.status(404).json({ errorcode: 'NOT_EXISTING', error: 'Specified item does not exist' });
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
@ -144,19 +198,19 @@ function post(req: Request, res: Response) {
async function patch(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
// Check if number is a valid integer
if (!Number.isInteger(parseInt(req.body.id.toString()))) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'The id field must be an integer' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'id field must be an integer' });
return;
}
// Check if status is valid.
if (req.body.status !== undefined && !Object.keys(itemStatus).includes(req.body.status)) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: `Status is not valid, valid values are: ${Object.keys(itemStatus).join(', ')}` });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: `Status is not valid, valid values are: ${Object.keys(itemStatus).join(', ')}` });
return;
}
@ -167,7 +221,7 @@ async function patch(req: Request, res: Response) {
},
data: {
SKU: req.body.sku,
amount: parseIntOrUndefined(req.body.ammount), // FIXME: This is silently failing if NaN..
amount: parseIntOrUndefined(req.body.amount), // FIXME: This is silently failing if NaN..
name: req.body.name,
comment: req.body.comment,
status: req.body.status, // Only enum(itemStatus) values are valid
@ -187,24 +241,31 @@ async function patch(req: Request, res: Response) {
// }
//},
createdBy: req.body.createdBy
},
select: {
id: true
}
})
.then(() => {
res.status(201).json({ status: 'updated' });
.then((data) => {
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated item', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ errorcode: 'EXISTING', error: 'Item already exists', err: err });
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Item already exists' });
} else if (err.code == 'P2003') {
// P2003 -> "Foreign key constraint failed on the field: {field_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ errorcode: 'NOT_EXISTING', error: 'Specified item does not exist' });
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
@ -213,7 +274,7 @@ async function patch(req: Request, res: Response) {
async function del(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
@ -226,12 +287,12 @@ async function del(req: Request, res: Response) {
});
if (result === null) {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'Item does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
prisma.item
@ -241,11 +302,11 @@ async function del(req: Request, res: Response) {
}
})
.then(() => {
res.status(200).json({ errorcode: 'DELETED', error: 'Sucessfully deleted entry' });
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted item' });
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}

View File

@ -19,7 +19,7 @@ function get(req: Request, res: Response) {
}
})
.then((items) => {
res.status(200).json(JSON.stringify(items));
res.status(200).json(items);
})
.catch((err) => {
log.db.error(err);

View File

@ -1,14 +1,20 @@
import { Request, Response } from 'express';
import { prisma, __path, log } from '../../../index.js';
import { parseIntOrUndefined, parseDynamicSortBy, parseIntRelation } from '../../../assets/helper.js';
// Get storageLocation.
function get(req: Request, res: Response) {
if (req.query.getAll === undefined) {
// Check if required fields are present.
if (!req.query.id) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
return;
}
async function get(req: Request, res: Response) {
if (req.query.sort === undefined) {
req.query.sort = 'id';
}
if (req.query.order === undefined) {
req.query.order = 'asc';
}
if (req.query.search === undefined) {
req.query.search = '';
}
if (req.query.id) {
prisma.storageLocation
.findUnique({
where: {
@ -21,33 +27,56 @@ function get(req: Request, res: Response) {
})
.then((items) => {
if (items) {
res.status(200).json(JSON.stringify(items));
res.status(200).json(items);
} else {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'storageLocation does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
} else {
// Get all items
const itemCountNotFiltered = await prisma.storageLocation.count({});
// Get all items (filtered)
const itemCountFiltered = await prisma.storageLocation.count({
where: {
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
});
prisma.storageLocation
.findMany({
take: parseIntOrUndefined(req.query.limit),
skip: parseIntOrUndefined(req.query.offset),
where: {
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
// Get storageUnit from relation.
include: {
storageUnit: true
}
},
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()),
})
.then((items) => {
if (items) {
res.status(200).json(JSON.stringify(items));
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
} else {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'storageLocation does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}
}
@ -56,7 +85,7 @@ function get(req: Request, res: Response) {
function post(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.name) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
// Create storageLocation with existing storageUnit.
@ -71,22 +100,26 @@ function post(req: Request, res: Response) {
}
})
.then((data) => {
res.status(201).json({ status: 'created', id: data.id });
res.status(201).json({ status: 'CREATED', message: 'Successfully created storageLocation', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ errorcode: 'EXISTING', error: 'storageLocation already exists' });
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageLocation already exists' });
} else if (err.code == 'P2003') {
// P2003 -> "Foreign key constraint failed on the field: {field_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
// FIXME: Is this errormessage right?
res.status(404).json({ error: 'specified storageUnitId does not exist' });
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnitId does not exist' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
@ -95,7 +128,7 @@ function post(req: Request, res: Response) {
async function patch(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id || !req.body.name || !req.body.storageUnitId) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
@ -108,12 +141,12 @@ async function patch(req: Request, res: Response) {
});
if (result === null) {
res.status(404).json({ error: 'storageLocation does not exist.' });
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
prisma.storageLocation
@ -123,26 +156,33 @@ async function patch(req: Request, res: Response) {
},
data: {
name: req.body.name,
storageUnitId: parseInt(req.body.storageUnitId) || undefined
storageUnit: parseIntRelation(req.body.storageUnitId)
},
select: {
id: true
}
})
.then(() => {
res.status(201).json({ status: 'updated' });
.then((data) => {
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated storageLocation', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ errorcode: 'EXISTING', error: 'storageLocation already exists' });
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageLocation already exists' });
} else if (err.code == 'P2003') {
// P2003 -> "Foreign key constraint failed on the field: {field_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
// FIXME: Is this errormessage right?
res.status(404).json({ error: 'specified storageUnitId does not exist' });
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnitId does not exist' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
@ -151,7 +191,7 @@ async function patch(req: Request, res: Response) {
async function del(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
@ -164,12 +204,12 @@ async function del(req: Request, res: Response) {
});
if (result === null) {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'storageLocation does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
prisma.storageLocation
@ -179,11 +219,11 @@ async function del(req: Request, res: Response) {
}
})
.then(() => {
res.status(200).json({ errorcode: 'DELETED', error: 'Sucessfully deleted entry' });
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted storageLocation' });
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}

View File

@ -1,15 +1,20 @@
import { Request, Response } from 'express';
import { prisma, __path, log } from '../../../index.js';
import { contactType } from '@prisma/client';
import { parseDynamicSortBy, parseIntOrUndefined } from '../../../assets/helper.js';
// Get storageUnit.
function get(req: Request, res: Response) {
if (req.query.getAll === undefined) {
// Check if required fields are present.
if (!req.query.id) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
return;
}
async function get(req: Request, res: Response) {
if (req.query.sort === undefined) {
req.query.sort = 'id';
}
if (req.query.order === undefined) {
req.query.order = 'asc';
}
if (req.query.search === undefined) {
req.query.search = '';
}
if (req.query.id) {
prisma.storageUnit
.findUnique({
where: {
@ -23,34 +28,57 @@ function get(req: Request, res: Response) {
})
.then((items) => {
if (items) {
res.status(200).json(JSON.stringify(items));
res.status(200).json(items);
} else {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'storageUnit does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
} else {
// Get all items
const itemCountNotFiltered = await prisma.storageUnit.count({});
// Get all items (filtered)
const itemCountFiltered = await prisma.storageUnit.count({
where: {
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
});
prisma.storageUnit
.findMany({
take: parseIntOrUndefined(req.query.limit),
skip: parseIntOrUndefined(req.query.offset),
// Get contactInfo and StorageLocation from relation.
include: {
contactInfo: true,
StorageLocation: true
}
},
where: {
name: {
// @ts-ignore
contains: req.query.search.length > 0 ? req.query.search : ''
}
},
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
})
.then((items) => {
if (items) {
res.status(200).json(JSON.stringify(items));
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
} else {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'storageUnit does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit does not exist' });
}
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}
}
@ -61,7 +89,7 @@ function post(req: Request, res: Response) {
if (req.body.locationId === 'META_CREATENEW') {
// Check if required fields are present.
if (!req.body.street || !req.body.houseNumber || !req.body.zipCode || !req.body.city || !req.body.country || !req.body.name) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
@ -86,23 +114,27 @@ function post(req: Request, res: Response) {
}
})
.then((data) => {
res.status(201).json({ status: 'created', id: data.id });
res.status(201).json({ status: 'CREATED', message: 'Successfully created storageUnit', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ errorcode: 'EXISTING', error: 'storageUnit already exists' });
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageUnit already exists' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
} else {
// Check if required fields are present.
if (!req.body.name || !req.body.locationId) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
// Create storageUnit with existing location.
@ -121,17 +153,21 @@ function post(req: Request, res: Response) {
}
})
.then((data) => {
res.status(201).json({ status: 'created', id: data.id });
res.status(201).json({ status: 'CREATED', message: 'Successfully created storageUnit', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ errorcode: 'EXISTING', error: 'storageUnit already exists' });
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageUnit already exists' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
@ -141,7 +177,7 @@ function post(req: Request, res: Response) {
async function patch(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id || !req.body.name || !req.body.locationId) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
@ -154,12 +190,12 @@ async function patch(req: Request, res: Response) {
});
if (result === null) {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'storageUnit does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
// Check if the locationId(contactInfo) exists. If not return 410 Gone.
@ -171,12 +207,12 @@ async function patch(req: Request, res: Response) {
});
if (result === null) {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'locationId does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'locationId does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
prisma.storageUnit
@ -191,20 +227,27 @@ async function patch(req: Request, res: Response) {
id: parseInt(req.body.locationId) // TODO: Rename to contactInfoId
}
}
},
select: {
id: true
}
})
.then(() => {
res.status(201).json({ status: 'updated' });
.then((data) => {
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated storageUnit', id: data.id });
})
.catch((err) => {
// Check if an entry already exists.
if (err.code === 'P2002') {
// P2002 -> "Unique constraint failed on the {constraint}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(409).json({ errorcode: 'EXISTING', error: 'storageUnit already exists' });
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageUnit already exists' });
} else if (err.code == 'P2000') {
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
} else {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
});
}
@ -213,7 +256,7 @@ async function patch(req: Request, res: Response) {
async function del(req: Request, res: Response) {
// Check if required fields are present.
if (!req.body.id) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
@ -226,13 +269,13 @@ async function del(req: Request, res: Response) {
});
if (result === null) {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'storageUnit does not exist' });
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit does not exist' });
return;
}
} catch (err) {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
}
prisma.storageUnit
@ -242,11 +285,11 @@ async function del(req: Request, res: Response) {
}
})
.then(() => {
res.status(200).json({ errorcode: 'DELETED', error: 'Sucessfully deleted entry' });
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted storageUnit' });
})
.catch((err) => {
log.db.error(err);
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
});
}

View File

@ -0,0 +1,13 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
const revision = req.app.locals.versionRev;
let updateAvailable = false;
if(req.app.locals.versionRevLong !== req.app.locals.versionRevLatest) {
updateAvailable = true;
}
res.status(200).send({ version: '1.0.0', commit: revision, updateAvailable: updateAvailable });
};
export default { get };

90
src/routes/auth/index.ts Normal file
View File

@ -0,0 +1,90 @@
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import express, { Request, Response } from 'express';
import { prisma, __path, log, config, app } from '../../index.js';
// Middleware Imports
import { checkAuthentication } from '../../middleware/auth.mw.js'
/* Configure password authentication strategy.
*
* The `LocalStrategy` authenticates users by verifying a username and password.
* The strategy parses the username and password from the request and calls the
* `verify` function.
*
* The `verify` function queries the database for the user record and verifies
* the password by hashing the password supplied by the user and comparing it to
* the hashed password stored in the database. If the comparison succeeds, the
* user is authenticated; otherwise, not.
*/
passport.use(
new LocalStrategy(function verify(username, password, cb) {
//log.auth.debug('LocalStrategy:', username, password);
for (const [user, pass] of Object.entries(config.global.auth.local.users)) {
//log.auth.debug('Loop(REQ):', username, password);
//log.auth.debug('Loop(CFG):', user, pass);
if (user.toLowerCase() === username.toLowerCase() && pass === password) {
log.auth.debug('LocalStrategy: success');
return cb(null, { username: username }); // This is the user object.
}
}
log.auth.debug('LocalStrategy: failed');
return cb(null, false, { message: 'Incorrect username or password.' });
})
/*
1. If the user not found in DB,
done (null, false)
2. If the user found in DB, but password does not match,
done (null, false)
3. If user found in DB and password match,
done (null, {authenticated_user})
*/
);
/* Configure session management.
*
* When a login session is established, information about the user will be
* stored in the session. This information is supplied by the `serializeUser`
* function, which is yielding the user ID and username.
*
* As the user interacts with the app, subsequent requests will be authenticated
* by verifying the session. The same user information that was serialized at
* session establishment will be restored when the session is authenticated by
* the `deserializeUser` function.
*
*/
passport.serializeUser(function (user: any, cb) {
process.nextTick(function () {
// log.auth.debug('Called seriealizeUser');
// log.auth.debug('user:', user);
return cb(null, {
username: user.username
});
});
});
passport.deserializeUser(function (user, cb) {
process.nextTick(function () {
// log.auth.debug('Called deseriealizeUser');
return cb(null, user);
});
});
// Route imports
import testRoute from './test.js';
import loginRoute from './login.js';
//import logoutRoute from './login.js'
// Router base is '/auth'
const Router = express.Router({ strict: false });
Router.route('/login').get(loginRoute.get);
Router.route('/login').post(passport.authenticate('local', { successRedirect: '/', failureRedirect: '/auth/login?failed' }));
Router.route('/test').get(checkAuthentication, testRoute.get);
export default Router;

View File

@ -1,5 +1,7 @@
import passport from 'passport';
import express, { Request, Response } from 'express';
import { prisma, __path } from '../../../index.js';
import { prisma, __path, log } from '../../index.js';
function get(req: Request, res: Response) {
res.render(__path + '/src/frontend/auth/login.eta.html'); //, { items: items });

7
src/routes/auth/test.ts Normal file
View File

@ -0,0 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.status(200).send('Auth Test Successful!');
};
export default { get };

View File

@ -29,7 +29,9 @@ function get(req: Request, res: Response) {
res.send(html);
});
} else {
res.send('Item not found');
Eta.renderFile(__path + '/src/frontend/errors/404.eta.html', item).then((html) => {
res.status(404).send(html);
});
}
});
}

View File

@ -2,33 +2,13 @@ import { Request, Response } from 'express';
import { prisma, __path, log } from '../../index.js';
async function get(req: Request, res: Response) {
// If no page is provided redirect to first
if (req.query.page === undefined) {
res.redirect('?page=1');
return;
}
let page = parseInt(req.query.page.toString());
const itemCount = await prisma.item.count({}); // Count all items in the DB
const takeSize = 25; // Amount of times per page
const pageSize = Math.ceil(itemCount / takeSize); // Amount of pages, always round up
// If page is less then 1 or more then the max page size redirect to first or last page. If itemCount is 0 do not redirect.
if (page < 1) {
res.redirect('?page=1');
return;
} else if (page > pageSize && itemCount !== 0) {
res.redirect('?page=' + pageSize);
return;
}
prisma.item
.findMany({ skip: (page - 1) * takeSize, take: takeSize, orderBy: { SKU: "asc" } }) // Skip the amount of items per page times the page number minus 1; skip has to be (page-1)*takeSize because skip is 0 indexed
.findMany({}) // Skip the amount of items per page times the page number minus 1; skip has to be (page-1)*takeSize because skip is 0 indexed
.then((items) => {
prisma.storageLocation.findMany({}).then((locations) => {
prisma.itemCategory.findMany({}).then((categories) => {
prisma.contactInfo.findMany({}).then((contactInfo) => {
res.render(__path + '/src/frontend/items.eta.html', { items: items, currentPage: page, maxPages: pageSize, storeLocs: locations, categories: categories, contactInfo: contactInfo });
res.render(__path + '/src/frontend/items.eta.html', { items: items, storeLocs: locations, categories: categories, contactInfo: contactInfo });
})
});
});

View File

@ -7,6 +7,12 @@ function get(req: Request, res: Response) {
.findMany({})
.then((items) => {
// Count amount of total items
// Replace "null" with an empty string
items.forEach((item) => {
if (item.description == null || item.description == "null") {
item.description = '';
}
});
res.render(__path + '/src/frontend/manage/categoryManager.eta.html', { items: items });
})
.catch((err) => {

View File

@ -7,7 +7,6 @@ import jsonImportRoute from './import/jsonImport.js';
import categoryManager from './categoryManager.js';
import storageManager from './storageManager.js';
import startpageRoute from './startpage.js';
import demoLoginRoute from './demo.js'
// Router base is '/manage'
const Router = express.Router({ strict: false });
@ -17,7 +16,6 @@ Router.route('/categories').get(categoryManager.get);
Router.route('/storages').get(storageManager.get);
Router.route('/import/csv').get(csvImportRoute.get).post(csvImportRoute.post);
Router.route('/import/json').get(jsonImportRoute.get).post(jsonImportRoute.post);
Router.route('/demo/login').get(demoLoginRoute.get);
Router.route('/').get(startpageRoute.get);
export default Router;

View File

@ -1,16 +1,25 @@
import express, { Express } from 'express';
import { __path, prisma } from '../index.js';
import * as Sentry from '@sentry/node';
// Middleware Imports
import { checkAuthentication } from '../middleware/auth.mw.js'
// Route imports
import frontend_routes from './frontend/index.js';
import static_routes from './static/index.js';
import api_routes from './api/index.js';
import auth_routes from './auth/index.js';
const Router = express.Router({ strict: false });
Router.use('/static', static_routes);
Router.use('/api', api_routes);
Router.use('/', frontend_routes);
Router.use('/api', checkAuthentication, api_routes);
Router.use('/auth', auth_routes);
Router.use('/', checkAuthentication, frontend_routes);
// The error handler must be before any other error middleware and after all controllers
Router.use(Sentry.Handlers.errorHandler());
// Default route.
Router.all('*', function (req, res) {
@ -23,3 +32,4 @@ Router.all('*', function (req, res) {
});
export default Router;

View File

@ -6,6 +6,11 @@ body {
width: 5%;
}
/* Give the logo a dark shadow to make it pop out */
.headLogo {
filter: drop-shadow(0 0 0.85rem rgba(0, 0, 0, 0.35));
}
@-moz-document url-prefix() {
.headLogo {
width: 40%;

View File

@ -3,10 +3,11 @@ function randomInRange(min, max) {
}
function doTheConfetti() {
// Create confetti
confetti({
angle: 100,
angle: randomInRange(90, 110),
spread: randomInRange(70, 120),
particleCount: randomInRange(100, 200),
origin: { y: 0.6 }
origin: { y: 0.6, x: randomInRange(0.4, 0.8) },
});
}

View File

@ -1,10 +1,10 @@
const FLAG_supports_new_data_loader = true;
function getDataForEdit(name) {
$.ajax({
type: 'get',
url: `/api/v1/categories?name=${name}`,
success: function (data) {
const result = JSON.parse(data);
success: function (result) {
// Get elements inside the editCategoryModal
const modal_categoryName = document.getElementById('editCategoryModalName');
const modal_categoryDescription = document.getElementById('editCategoryModalDescription');
@ -39,3 +39,42 @@ function primeEdit() {
form.setAttribute('method', 'PATCH');
return true;
}
const itemList = $('#itemList');
// itemList.empty();
itemList.bootstrapTable({ url: '/api/v1/categories', search: true, showRefresh: true, responseHandler: dataResponseHandler, sidePagination: 'server', serverSort: true, silentSort: false });
setTimeout(() => {
activateTooltips();
}, 1000);
function loadPageData() {
itemList.bootstrapTable('refresh')
setTimeout(() => {
$(".tooltip").tooltip("hide");
activateTooltips();
}, 1000);
}
function dataResponseHandler(json) {
// console.log(json)
totalNotFiltered = json.totalNotFiltered;
total = json.total;
json = json.items;
json.forEach((item) => {
item.actions = `
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#editCategoryModal" onclick="primeEdit(); getDataForEdit('${item.name}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('${item.name}','categories','Category','name')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
<i class="bi bi-trash"></i>
</button>`
});
///// --------------------------------- /////
setTimeout(() => {
activateTooltips();
}, 200);
return {"rows": json, total: total, totalNotFiltered: totalNotFiltered, totalRows: total};
}
loadPageData()

View File

@ -8,6 +8,16 @@ function primeCreateNew() {
return true;
}
function triggerDuplicationDialog(sourceItemId) {
// Clear the form
$('.form-control').val('');
const form = document.getElementById('ItemModalForm');
document.getElementById('itemModifyModalLabel').innerText= "Duplicate an item";
form.setAttribute('method', 'POST');
getDataForEdit(sourceItemId);
return true;
}
function primeEdit() {
const form = document.getElementById('ItemModalForm');
document.getElementById('itemModifyModalLabel').innerText = 'Edit an item';
@ -19,8 +29,7 @@ function getDataForEdit(id) {
$.ajax({
type: 'get',
url: `/api/v1/items?id=${id}`,
success: function (data) {
const result = JSON.parse(data);
success: function (result) {
// Get elements inside the editCategoryModal
const modal_itemName = document.getElementById('itemModifyModalName');
@ -42,6 +51,7 @@ function getDataForEdit(id) {
// Select the correct option in the dropdown
const modal_itemCategoryOptions = modal_itemCategory.options;
modal_itemCategoryOptions[0].selected = true;
for (let i = 0; i < modal_itemCategoryOptions.length; i++) {
if (modal_itemCategoryOptions[i].value == result.categoryId) {
modal_itemCategoryOptions[i].selected = true;
@ -50,6 +60,7 @@ function getDataForEdit(id) {
// Select the correct option in the dropdown
const modal_itemStatusOptions = modal_itemStatus.options;
modal_itemStatusOptions[0].selected = true;
for (let i = 0; i < modal_itemStatusOptions.length; i++) {
if (modal_itemStatusOptions[i].value == result.statusId) {
modal_itemStatusOptions[i].selected = true;
@ -58,6 +69,7 @@ function getDataForEdit(id) {
// Select the correct option in the dropdown
const modal_itemStorageLocationOptions = modal_itemStorageLocation.options;
modal_itemStorageLocationOptions[0].selected = true;
for (let i = 0; i < modal_itemStorageLocationOptions.length; i++) {
if (modal_itemStorageLocationOptions[i].value == result.storageLocationId) {
modal_itemStorageLocationOptions[i].selected = true;

View File

@ -2,6 +2,8 @@
// This magic js codes enables anchor links to work with bootstrap tabs
// Taken from https://stackoverflow.com/a/9393768/11317151 (and edited, like a lot)
const FLAG_supports_new_data_loader = true;
// Also update on location change
window.addEventListener(
'hashchange',
@ -29,9 +31,9 @@ function primeCreateNew() {
const form = document.getElementById('storageUnitModalForm');
const form2 = document.getElementById('storageLocationModalForm');
document.getElementById('createNewLocationSelection').disabled = false;
document.getElementById('storageUnitModalLocationSelectText').innerText= "Select or create a new location.";
document.getElementById('storageUnitModalLabel').innerText = "Create new storage unit";
document.getElementById('storageLocationModalTitle').innerText = "Create new storage location";
document.getElementById('storageUnitModalLocationSelectText').innerText = 'Select or create a new location.';
document.getElementById('storageUnitModalLabel').innerText = 'Create new storage unit';
document.getElementById('storageLocationModalTitle').innerText = 'Create new storage location';
form.setAttribute('method', 'POST');
form2.setAttribute('method', 'POST');
return true;
@ -42,25 +44,25 @@ function primeEdit() {
const form2 = document.getElementById('storageLocationModalForm');
// Disable create new location
document.getElementById('createNewLocationSelection').disabled = true;
document.getElementById('storageUnitModalLocationSelectText').innerText= "While editing you can only select already existing locations. Use the settings to create new ones.";
document.getElementById('storageUnitModalLabel').innerText = "Edit a storage unit";
document.getElementById('storageLocationModalTitle').innerText = "Edit a storage location"
document.getElementById('storageUnitModalLocationSelect').selectedIndex = 1
handleSelector()
document.getElementById('storageUnitModalLocationSelectText').innerText = 'While editing you can only select already existing locations. Use the settings to create new ones.';
document.getElementById('storageUnitModalLabel').innerText = 'Edit a storage unit';
document.getElementById('storageLocationModalTitle').innerText = 'Edit a storage location';
document.getElementById('storageUnitModalLocationSelect').selectedIndex = 1;
handleSelector();
form.setAttribute('method', 'PATCH');
form2.setAttribute('method', 'PATCH');
return true;
}
function handleSelector(){
const selector = document.getElementById('storageUnitModalLocationSelect')
function handleSelector() {
const selector = document.getElementById('storageUnitModalLocationSelect');
const value = selector.options[selector.selectedIndex].value;
if(value == "META_CREATENEW") {
$('#storageUnitModalContactInfoCreator').removeClass('d-none')
$('.requireOnCreate').attr('required', true)
if (value == 'META_CREATENEW') {
$('#storageUnitModalContactInfoCreator').removeClass('d-none');
$('.requireOnCreate').attr('required', true);
} else {
$('#storageUnitModalContactInfoCreator').addClass('d-none')
$('.requireOnCreate').attr('required', false)
$('#storageUnitModalContactInfoCreator').addClass('d-none');
$('.requireOnCreate').attr('required', false);
}
}
@ -68,9 +70,7 @@ function getDataForEdit(id) {
$.ajax({
type: 'get',
url: `/api/v1/storageUnits?id=${id}`,
success: function (data) {
const result = JSON.parse(data);
success: function (result) {
// Get elements inside the editCategoryModal
const modal_unitName = document.getElementById('storageUnitModalName');
const modal_unitLocation = document.getElementById('storageUnitModalLocationSelect');
@ -81,15 +81,13 @@ function getDataForEdit(id) {
modal_unitId.value = result.id;
// Select the correct location from the select based on the value of the option
for(var i, j = 0; i = modal_unitLocation.options[j]; j++) {
if(i.value == result.contactInfoId) {
console.log("Found it");
for (var i, j = 0; (i = modal_unitLocation.options[j]); j++) {
if (i.value == result.contactInfoId) {
console.log('Found it');
modal_unitLocation.selectedIndex = j;
break;
}
}
},
error: function (data) {
console.log('!!!! ERROR !!!!', data);
@ -97,19 +95,16 @@ function getDataForEdit(id) {
$('.loader-overlay').removeClass('active');
// Close the modal
$('.modal').modal('hide');
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The storage unit does no longer exist.', "text-bg-danger")
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The storage unit does no longer exist.', 'text-bg-danger');
}
});
}
function getDataForEditLoc(id) {
$.ajax({
type: 'get',
url: `/api/v1/storageLocations?id=${id}`,
success: function (data) {
const result = JSON.parse(data);
success: function (result) {
// Get elements inside the editCategoryModal
const modal_locationName = document.getElementById('storageLocationModalName');
const modal_locationUnitSel = document.getElementById('storageLocationModalUnit');
@ -120,15 +115,13 @@ function getDataForEditLoc(id) {
modal_locationId.value = result.id;
// Select the correct location from the select based on the value of the option
for(var i, j = 0; i = modal_locationUnitSel.options[j]; j++) {
if(i.value == result.storageUnitId) {
console.log("Found it");
for (var i, j = 0; (i = modal_locationUnitSel.options[j]); j++) {
if (i.value == result.storageUnitId) {
console.log('Found it');
modal_locationUnitSel.selectedIndex = j;
break;
}
}
},
error: function (data) {
console.log('!!!! ERROR !!!!', data);
@ -136,9 +129,89 @@ function getDataForEditLoc(id) {
$('.loader-overlay').removeClass('active');
// Close the modal
$('.modal').modal('hide');
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The storage unit does no longer exist.', "text-bg-danger")
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The storage unit does no longer exist.', 'text-bg-danger');
}
});
}
handleSelector()
const itemList = $('#itemList');
const itemListUnit = $('#itemListUnit');
// itemList.empty();
itemListUnit.bootstrapTable({ url: '/api/v1/storageUnits', search: true, showRefresh: true, responseHandler: dataResponseHandlerUnit, sidePagination: 'server', serverSort: true, silentSort: false });
itemList.bootstrapTable({ url: '/api/v1/storageLocations', search: true, showRefresh: true, responseHandler: dataResponseHandler, sidePagination: 'server', serverSort: true, silentSort: false });
setTimeout(() => {
activateTooltips();
}, 1000);
function loadPageData() {
// itemList.empty();
itemList.bootstrapTable('refresh');
itemListUnit.bootstrapTable('refresh');
setTimeout(() => {
$(".tooltip").tooltip("hide");
activateTooltips();
}, 1000);
}
function dataResponseHandler(json) {
// console.log(json)
totalNotFiltered = json.totalNotFiltered;
total = json.total;
json = json.items;
json.forEach((item) => {
colorStatus = '';
item.actions = `
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageLocationModal" onclick="primeEdit(); getDataForEditLoc('${item.id}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('${item.id}','storageLocations','Storage Location')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
<i class="bi bi-trash"></i>
</button>`;
if (item.storageUnit == null) {
item.storageUnit = '<i>No storage unit assigned</i>';
} else {
item.storageUnit = item.storageUnit.name;
console.log(item.storageUnit);
}
// item.SKU = `<p data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: ${item.id}">${item.SKU}</p>`
});
///// --------------------------------- /////
setTimeout(() => {
activateTooltips();
}, 200);
return { rows: json, total: total, totalNotFiltered: totalNotFiltered, totalRows: total };
}
function dataResponseHandlerUnit(json) {
// console.log(json)
totalNotFiltered = json.totalNotFiltered;
total = json.total;
json = json.items;
json.forEach((item) => {
colorStatus = '';
item.actions = `
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageUnitModal" onclick="primeEdit(); getDataForEdit('${item.id}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('${item.id}','storageUnits','Storage Unit')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
<i class="bi bi-trash"></i>
</button>`;
if (item.contactInfo == null) {
item.address = '<i>No address assigned</i>';
} else {
item.address = `${item.contactInfo.street} ${item.contactInfo.houseNumber}, ${item.contactInfo.city} ${item.contactInfo.country}`;
}
// item.SKU = `<p data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: ${item.id}">${item.SKU}</p>`
});
///// --------------------------------- /////
setTimeout(() => {
activateTooltips();
}, 200);
return { rows: json, total: total, totalNotFiltered: totalNotFiltered, totalRows: total };
}
handleSelector();

View File

@ -1,4 +1,13 @@
var amountOfForms = $('.frontendForm').length;
function isNewDataLoaderAvailable() {
try {
return FLAG_supports_new_data_loader;
} catch (error) {
return false;
}
}
$('.frontendForm').each(function () {
// TODO Handle empty strings as null or undefined, not as ''
$(this).on('submit', function (e) {
@ -26,7 +35,12 @@ $('.frontendForm').each(function () {
// Clear all fields
form.find('input, textarea').val('');
// Create toast
createNewToast('<i class="bi bi-check2"></i> Changes saved successfully.', "text-bg-success")
if(isNewDataLoaderAvailable()) {
createNewToast('<i class="bi bi-check2"></i> Changes saved successfully.', "text-bg-success", undefined, false)
} else {
createNewToast('<i class="bi bi-check2"></i> Changes saved successfully.', "text-bg-success")
}
},
error: function (data) {
console.log('error');
@ -57,7 +71,12 @@ function deleteEntryNxt(id, route, name) {
data: { id: id },
success: function (data) {
$('#staticBackdrop').modal('hide');
createNewToast(`<i class="bi bi-check2"></i> ${name} deleted successfully.`, "text-bg-success")
if(isNewDataLoaderAvailable()) {
createNewToast(`<i class="bi bi-check2"></i> ${name} deleted successfully.`, "text-bg-success", undefined, false)
} else {
createNewToast(`<i class="bi bi-check2"></i> ${name} deleted successfully.`, "text-bg-success")
}
confetti({
spread: 360,
@ -94,8 +113,7 @@ function preFillDeleteModalNxt(id, route, name, requestIdent='id') {
$.ajax({
type: 'get',
url: `/api/v1/${route}?${requestIdent}=${id}`,
success: function (data) {
const result = JSON.parse(data);
success: function (result) {
// Get elements inside the editCategoryModal
const modal_categoryName = document.getElementById('deleteNamePlaceholder');

View File

@ -10,7 +10,6 @@ trinagles.each(function () {
$(this).addClass('rotate');
}
console.log('target', target);
target.on('show.bs.collapse', function () {
$(triTar).addClass('rotate');
$(triTar).removeClass('derotate');

View File

@ -0,0 +1,68 @@
const FLAG_supports_new_data_loader = true;
/**
* Should we ever implement items in items, have a look at this:
* https://examples.bootstrap-table.com/index.html?extensions/treegrid.html#extensions/treegrid.html
*/
// Inital thing
const itemList = $('#itemList');
itemList.bootstrapTable({ url: '/api/v1/items', search: true, showRefresh: true, responseHandler: dataResponseHandler, sidePagination: 'server', serverSort: true, silentSort: false });
setTimeout(() => {
activateTooltips();
}, 1000);
function loadPageData() {
itemList.bootstrapTable('refresh')
setTimeout(() => {
$(".tooltip").tooltip("hide");
activateTooltips();
}, 1000);
}
function dataResponseHandler(json) {
// console.log(json)
totalNotFiltered = json.totalNotFiltered;
total = json.total;
json = json.items;
json.forEach((item) => {
colorStatus = '';
if(item.SKU == null) item.SKU = '<i>No SKU assigned</i>';
switch (item.status) {
case 'normal':
colorStatus = 'success';
break;
case 'stolen':
colorStatus = 'danger';
break;
case 'lost':
colorStatus = 'warning';
break;
case 'borrowed':
colorStatus = 'info';
break;
default:
colorStatus = 'secondary';
}
item.status = `<span class="badge text-bg-${colorStatus}">${item.status}</span>`;
item.actions = `
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#itemModifyModal" onclick="primeEdit(); getDataForEdit('${item.id}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#itemModifyModal" onclick="triggerDuplicationDialog('${item.id}')">
<i class="bi bi-copy"></i>
</button>
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('${item.id}','items','Item')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
<i class="bi bi-trash"></i>
</button>`
item.SKU = `<p data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: ${item.id}">${item.SKU}</p>`
});
///// --------------------------------- /////
setTimeout(() => {
activateTooltips();
}, 200);
return {"rows": json, total: total, totalNotFiltered: totalNotFiltered, totalRows: total};
}
loadPageData()

View File

@ -60,15 +60,14 @@ function handleSearchChange(e) {
autocompleteBox.innerHTML = 'Start typing to search for commands <br> #SKU';
return;
}
const baseURI = window.location.origin;
const baseURI = window.location.origin; // move to new fancy route
const url = baseURI + '/api/v1/search/sku?sku=' + searchedSKU;
$.ajax({
type: 'get',
url: url,
success: function (data) {
let result = JSON.parse(data);
success: function (result) {
let htmlResult = ""
result.forEach(element => {
console.log(element);

View File

@ -26,6 +26,11 @@ function createNewToast(message, colorSelector, autoHideTime = 1500, autoReload
targetContainer.appendChild(newToast);
currentToasts.push(newToast);
$(newToast).toast('show');
try {
loadPageData();
} catch (error) {
console.debug("Page does not support new data loading.")
}
setTimeout(() => {
destroyToast(newToast.id);
if (autoReload && !forceSkipReload) {

35
tools/generate.js Normal file
View File

@ -0,0 +1,35 @@
/**
* This file can be used to load-test the application
*/
import { writeFileSync } from "fs";
// Temporary file to generate demo data for the inventory
const inventoryItems = [];
// Function to add an item to the inventory using a simple function
function addInventoryItem(name, amount, manufacturer, category, sku) {
const item = {
name: name,
amount: amount,
manufacturer: manufacturer,
category: category,
sku: sku
};
inventoryItems.push(item);
}
// Loop to generate 2000 items
for (let i = 1; i <= 1024; i++) {
const itemName = `Item ${i}`;
const itemAmount = Math.floor(Math.random() * 100) + 1;
const itemManufacturer = `Manufacturer ${i}`;
const itemCategory = `Category ${i}`;
const itemSKU = `SKU-${i}`;
addInventoryItem(itemName, itemAmount, itemManufacturer, itemCategory, itemSKU);
}
// Save the generated data to a file
writeFileSync('demoData.json', JSON.stringify(inventoryItems))