17 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
26 changed files with 861 additions and 97 deletions

139
package-lock.json generated
View File

@ -21,8 +21,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"
},
@ -30,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",
@ -832,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",
@ -868,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",
@ -2337,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",
@ -4211,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",
@ -4422,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",
@ -4469,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",
@ -5116,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",
@ -6369,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,14 +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"
},
@ -38,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

@ -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

@ -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

@ -100,7 +100,7 @@
<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-searchable="false">Actions</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) { %>

View File

@ -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

@ -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,16 +92,16 @@ Sentry.init({
environment: config.global.debug ? 'development' : 'production'
});
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]
log.core.info(`Running revision ${app.locals.versionRevLong} (${app.locals.versionRevLatest} latest)`);
// 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
@ -93,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,31 +1,97 @@
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({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: '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(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' });
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.
@ -56,6 +122,10 @@ function post(req: Request, res: Response) {
// 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' });
@ -110,6 +180,10 @@ async function patch(req: Request, res: Response) {
// 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' });

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,7 @@ 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';
@ -28,6 +30,8 @@ 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);

View File

@ -183,6 +183,10 @@ function post(req: Request, res: Response) {
// https://www.prisma.io/docs/reference/api-reference/error-reference
// FIXME: Is this errormessage right?
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({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
@ -255,6 +259,10 @@ async function patch(req: Request, res: Response) {
// P2003 -> "Foreign key constraint failed on the field: {field_name}"
// https://www.prisma.io/docs/reference/api-reference/error-reference
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({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });

View File

@ -14,12 +14,7 @@ async function get(req: Request, res: Response) {
req.query.search = '';
}
if (req.query.getAll === undefined) {
// Check if required fields are present.
if (!req.query.id) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
if (req.query.id) {
prisma.storageLocation
.findUnique({
where: {
@ -118,6 +113,10 @@ function post(req: Request, res: Response) {
// https://www.prisma.io/docs/reference/api-reference/error-reference
// FIXME: Is this errormessage right?
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({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
@ -177,6 +176,10 @@ async function patch(req: Request, res: Response) {
// https://www.prisma.io/docs/reference/api-reference/error-reference
// FIXME: Is this errormessage right?
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({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });

View File

@ -14,12 +14,7 @@ async function get(req: Request, res: Response) {
if (req.query.search === undefined) {
req.query.search = '';
}
if (req.query.getAll === undefined) {
// Check if required fields are present.
if (!req.query.id) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
return;
}
if (req.query.id) {
prisma.storageUnit
.findUnique({
where: {
@ -127,6 +122,10 @@ function post(req: Request, res: Response) {
// 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: '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({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
@ -162,6 +161,10 @@ function post(req: Request, res: Response) {
// 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: '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({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
@ -238,6 +241,10 @@ async function patch(req: Request, res: Response) {
// 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: '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({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });

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

@ -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,6 +3,7 @@ function randomInRange(min, max) {
}
function doTheConfetti() {
// Create confetti
confetti({
angle: randomInRange(90, 110),
spread: randomInRange(70, 120),

View File

@ -1,3 +1,5 @@
const FLAG_supports_new_data_loader = true;
function getDataForEdit(name) {
$.ajax({
type: 'get',
@ -37,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';

View File

@ -138,8 +138,8 @@ const itemList = $('#itemList');
const itemListUnit = $('#itemListUnit');
// itemList.empty();
itemListUnit.bootstrapTable({ url: '/api/v1/storageUnits?getAll=true', search: true, showRefresh: true, responseHandler: dataResponseHandlerUnit, sidePagination: 'server', serverSort: true, silentSort: false });
itemList.bootstrapTable({ url: '/api/v1/storageLocations?getAll=true', search: true, showRefresh: true, responseHandler: dataResponseHandler, sidePagination: 'server', serverSort: true, silentSort: false });
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);
@ -150,6 +150,7 @@ function loadPageData() {
itemListUnit.bootstrapTable('refresh');
setTimeout(() => {
$(".tooltip").tooltip("hide");
activateTooltips();
}, 1000);
}

View File

@ -7,18 +7,15 @@ const FLAG_supports_new_data_loader = true;
// Inital thing
const itemList = $('#itemList');
// itemList.empty();
itemList.bootstrapTable({url: "/api/v1/items", search: true, showRefresh: true, responseHandler: dataResponseHandler, sidePagination: 'server', serverSort: true, silentSort: false})
setTimeout(() => {
activateTooltips();
}, 1000);
itemList.bootstrapTable({ url: '/api/v1/items', search: true, showRefresh: true, responseHandler: dataResponseHandler, sidePagination: 'server', serverSort: true, silentSort: false });
setTimeout(() => {
activateTooltips();
}, 1000);
function loadPageData() {
const itemList = $('#itemList');
// itemList.empty();
itemList.bootstrapTable('refresh')
setTimeout(() => {
$(".tooltip").tooltip("hide");
activateTooltips();
}, 1000);
}
@ -52,6 +49,9 @@ function dataResponseHandler(json) {
<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>`

View File

@ -60,7 +60,7 @@ 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;