initial commit

deepsource-autofix-76c6eb20
dangowans 2022-07-12 10:29:08 -04:00
parent a0d78854f1
commit ed5d7b8d4b
114 changed files with 19871 additions and 0 deletions

1
.atomignore 100644
View File

@ -0,0 +1 @@
*.db-journal

11
.codeclimate.yml 100644
View File

@ -0,0 +1,11 @@
version: "2"
checks:
file-lines:
config:
threshold: 1000
method-complexity:
config:
threshold: 15
method-lines:
config:
threshold: 300

3
.eslintignore 100644
View File

@ -0,0 +1,3 @@
**/*.d.ts
**/*.ejs
**/*.js

61
.eslintrc.json 100644
View File

@ -0,0 +1,61 @@
{
"root": true,
"env": {
"es6": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.json", "./public-typescript/tsconfig.json"],
"ecmaVersion": 2020,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"unicorn"
],
"extends": [
"eslint:recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:node/recommended",
"plugin:promise/recommended",
"plugin:unicorn/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"node/no-missing-import": "off",
"unicorn/consistent-function-scoping": "warn",
"unicorn/empty-brace-spaces": "off",
"unicorn/filename-case": [
"error", {
"case": "camelCase",
"ignore": [
"DB",
"URL"
]
}
],
"unicorn/prefer-node-protocol": "off",
"unicorn/prevent-abbreviations": [
"error", {
"replacements": {
"def": {
"definition": true
},
"ele": {
"element": true
},
"eles": {
"elements": true
},
"fns": {
"functions": true
},
"res": {
"result": false
}
}
}
]
}
}

15
.gitignore vendored 100644
View File

@ -0,0 +1,15 @@
.nyc_output/
coverage/
data/sessions/
node_modules/
app.min.js
bin/daemon/
data/*.db
data/*.db-journal
data/*.min.js
data/config.*
helpers/*.min.js
public/images-custom/*
routes/*.min.js
*.pem

5
.ncurc.json 100644
View File

@ -0,0 +1,5 @@
{
"reject": [
"@fortawesome/fontawesome-free"
]
}

29
.sass-lint.yml 100644
View File

@ -0,0 +1,29 @@
rules:
class-name-format:
- 0
empty-line-between-blocks:
- 0
final-newline:
- 0
force-attribute-nesting:
- 0
force-element-nesting:
- 0
hex-length:
- 0
indentation:
- 0
leading-zero:
- 0
no-color-literals:
- 1
no-css-comments:
- 0
no-ids:
- 0
no-trailing-whitespace:
- 0
property-sort-order:
- 1
-
order: 'recess'

81
CODE_OF_CONDUCT.md 100644
View File

@ -0,0 +1,81 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at
[mailto:corporateapps@cityssm.on.ca](corporateapps@cityssm.on.ca). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
For answers to common questions about this code of conduct, see
<https://www.contributor-covenant.org/faq>
[homepage]: https://www.contributor-covenant.org

33
CONTRIBUTING.md 100644
View File

@ -0,0 +1,33 @@
### Thank you for your interest in making the Lot Occupancy System better
Together, we can build high quality software that meets the needs of municipalities,
while remaining open and budget conscious.
**Thank you for taking the time to read the contributing guidelines.**
### All contributions are welcome
Being a very small team, contributions are greatly appreciated. How can you contribute?
- **Promote this project!**
- Documentation and tutorials
- Submit bug reports (or fix them!)
- Request new features (or build them!)
### Please Read the Code of Conduct
The [Code of Conduct](CODE_OF_CONDUCT.md) document describes how we should act when working together.
Be nice! :smile:
### How to report a bug or suggest an enhancement
**If you find a security vulnerability, do NOT open an issue. Email
[mailto:corporateapps@cityssm.on.ca](corporateapps@cityssm.on.ca) instead.**
For all other bug reports, feature requests, or enhancements,
open an issue and use the closest template.
### Thanks Again
We are a very small team with big aspirations and limited resources.
Thank you for helping with this project in any way you can!

2
app.d.ts vendored 100644
View File

@ -0,0 +1,2 @@
export declare const app: import("express-serve-static-core").Express;
export default app;

129
app.js 100644
View File

@ -0,0 +1,129 @@
import createError from "http-errors";
import express from "express";
import compression from "compression";
import path from "path";
import cookieParser from "cookie-parser";
import csurf from "csurf";
import rateLimit from "express-rate-limit";
import session from "express-session";
import FileStore from "session-file-store";
import routerLogin from "./routes/login.js";
import routerDashboard from "./routes/dashboard.js";
import routerLicences from "./routes/licences.js";
import routerReports from "./routes/reports.js";
import routerAdmin from "./routes/admin.js";
import * as configFunctions from "./helpers/functions.config.js";
import * as dateTimeFns from "@cityssm/expressjs-server-js/dateTimeFns.js";
import * as stringFns from "@cityssm/expressjs-server-js/stringFns.js";
import * as htmlFns from "@cityssm/expressjs-server-js/htmlFns.js";
import { version } from "./version.js";
import * as databaseInitializer from "./helpers/databaseInitializer.js";
import debug from "debug";
const debugApp = debug("general-licence-manager:app");
databaseInitializer.initLicencesDB();
const __dirname = ".";
export const app = express();
if (!configFunctions.getProperty("reverseProxy.disableEtag")) {
app.set("etag", false);
}
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
if (!configFunctions.getProperty("reverseProxy.disableCompression")) {
app.use(compression());
}
app.use((request, _response, next) => {
debugApp(`${request.method} ${request.url}`);
next();
});
app.use(express.json());
app.use(express.urlencoded({
extended: false
}));
app.use(cookieParser());
app.use(csurf({ cookie: true }));
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 1000
});
app.use(limiter);
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
if (urlPrefix !== "") {
debugApp("urlPrefix = " + urlPrefix);
}
app.use(urlPrefix, express.static(path.join("public")));
app.use(urlPrefix + "/lib/fa", express.static(path.join("node_modules", "@fortawesome", "fontawesome-free")));
app.use(urlPrefix + "/lib/cityssm-bulma-webapp-js", express.static(path.join("node_modules", "@cityssm", "bulma-webapp-js")));
app.use(urlPrefix + "/lib/cityssm-bulma-js", express.static(path.join("node_modules", "@cityssm", "bulma-js", "dist")));
const sessionCookieName = configFunctions.getProperty("session.cookieName");
const FileStoreSession = FileStore(session);
app.use(session({
store: new FileStoreSession({
path: "./data/sessions",
logFn: debug("general-licence-manager:session"),
retries: 10
}),
name: sessionCookieName,
secret: configFunctions.getProperty("session.secret"),
resave: true,
saveUninitialized: false,
rolling: true,
cookie: {
maxAge: configFunctions.getProperty("session.maxAgeMillis"),
sameSite: "strict"
}
}));
app.use((request, response, next) => {
if (request.cookies[sessionCookieName] && !request.session.user) {
response.clearCookie(sessionCookieName);
}
next();
});
const sessionChecker = (request, response, next) => {
if (request.session.user && request.cookies[sessionCookieName]) {
return next();
}
return response.redirect(`${urlPrefix}/login?redirect=${request.originalUrl}`);
};
app.use((request, response, next) => {
response.locals.buildNumber = version;
response.locals.user = request.session.user;
response.locals.csrfToken = request.csrfToken();
response.locals.configFunctions = configFunctions;
response.locals.dateTimeFns = dateTimeFns;
response.locals.stringFns = stringFns;
response.locals.htmlFns = htmlFns;
response.locals.urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
next();
});
app.get(urlPrefix + "/", sessionChecker, (_request, response) => {
response.redirect(urlPrefix + "/dashboard");
});
app.use(urlPrefix + "/dashboard", sessionChecker, routerDashboard);
app.use(urlPrefix + "/licences", sessionChecker, routerLicences);
app.use(urlPrefix + "/reports", sessionChecker, routerReports);
app.use(urlPrefix + "/admin", sessionChecker, routerAdmin);
app.all(urlPrefix + "/keepAlive", (_request, response) => {
response.json(true);
});
app.use(urlPrefix + "/login", routerLogin);
app.get(urlPrefix + "/logout", (request, response) => {
if (request.session.user && request.cookies[sessionCookieName]) {
request.session.destroy(null);
request.session = undefined;
response.clearCookie(sessionCookieName);
response.redirect(urlPrefix + "/");
}
else {
response.redirect(urlPrefix + "/login");
}
});
app.use((_request, _response, next) => {
next(createError(404));
});
app.use((error, request, response) => {
response.locals.message = error.message;
response.locals.error = request.app.get("env") === "development" ? error : {};
response.status(error.status || 500);
response.render("error");
});
export default app;

233
app.ts 100644
View File

@ -0,0 +1,233 @@
import createError from "http-errors";
import express from "express";
import compression from "compression";
import path from "path";
import cookieParser from "cookie-parser";
import csurf from "csurf";
import rateLimit from "express-rate-limit";
import session from "express-session";
import FileStore from "session-file-store";
import routerLogin from "./routes/login.js";
import routerDashboard from "./routes/dashboard.js";
import routerLicences from "./routes/licences.js";
import routerReports from "./routes/reports.js";
import routerAdmin from "./routes/admin.js";
import * as configFunctions from "./helpers/functions.config.js";
import * as dateTimeFns from "@cityssm/expressjs-server-js/dateTimeFns.js";
import * as stringFns from "@cityssm/expressjs-server-js/stringFns.js";
import * as htmlFns from "@cityssm/expressjs-server-js/htmlFns.js";
import { version } from "./version.js";
import * as databaseInitializer from "./helpers/databaseInitializer.js";
import debug from "debug";
const debugApp = debug("general-licence-manager:app");
/*
* INITALIZE THE DATABASE
*/
databaseInitializer.initLicencesDB();
/*
* INITIALIZE APP
*/
const __dirname = ".";
export const app = express();
if (!configFunctions.getProperty("reverseProxy.disableEtag")) {
app.set("etag", false);
}
// View engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
if (!configFunctions.getProperty("reverseProxy.disableCompression")) {
app.use(compression());
}
app.use((request, _response, next) => {
debugApp(`${request.method} ${request.url}`);
next();
});
app.use(express.json());
app.use(express.urlencoded({
extended: false
}));
app.use(cookieParser());
app.use(csurf({ cookie: true }));
/*
* Rate Limiter
*/
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 1000
});
app.use(limiter);
/*
* STATIC ROUTES
*/
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
if (urlPrefix !== "") {
debugApp("urlPrefix = " + urlPrefix);
}
app.use(urlPrefix, express.static(path.join("public")));
app.use(urlPrefix + "/lib/fa",
express.static(path.join("node_modules", "@fortawesome", "fontawesome-free")));
app.use(urlPrefix + "/lib/cityssm-bulma-webapp-js",
express.static(path.join("node_modules", "@cityssm", "bulma-webapp-js")));
app.use(urlPrefix + "/lib/cityssm-bulma-js",
express.static(path.join("node_modules", "@cityssm", "bulma-js", "dist")));
/*
* SESSION MANAGEMENT
*/
const sessionCookieName: string = configFunctions.getProperty("session.cookieName");
const FileStoreSession = FileStore(session);
// Initialize session
app.use(session({
store: new FileStoreSession({
path: "./data/sessions",
logFn: debug("general-licence-manager:session"),
retries: 10
}),
name: sessionCookieName,
secret: configFunctions.getProperty("session.secret"),
resave: true,
saveUninitialized: false,
rolling: true,
cookie: {
maxAge: configFunctions.getProperty("session.maxAgeMillis"),
sameSite: "strict"
}
}));
// Clear cookie if no corresponding session
app.use((request, response, next) => {
if (request.cookies[sessionCookieName] && !request.session.user) {
response.clearCookie(sessionCookieName);
}
next();
});
// Redirect logged in users
const sessionChecker = (request: express.Request, response: express.Response, next: express.NextFunction) => {
if (request.session.user && request.cookies[sessionCookieName]) {
return next();
}
return response.redirect(`${urlPrefix}/login?redirect=${request.originalUrl}`);
};
/*
* ROUTES
*/
// Make the user and config objects available to the templates
app.use((request, response, next) => {
response.locals.buildNumber = version;
response.locals.user = request.session.user;
response.locals.csrfToken = request.csrfToken();
response.locals.configFunctions = configFunctions;
response.locals.dateTimeFns = dateTimeFns;
response.locals.stringFns = stringFns;
response.locals.htmlFns = htmlFns;
response.locals.urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
next();
});
app.get(urlPrefix + "/", sessionChecker, (_request, response) => {
response.redirect(urlPrefix + "/dashboard");
});
app.use(urlPrefix + "/dashboard", sessionChecker, routerDashboard);
app.use(urlPrefix + "/licences", sessionChecker, routerLicences);
app.use(urlPrefix + "/reports", sessionChecker, routerReports);
app.use(urlPrefix + "/admin", sessionChecker, routerAdmin);
app.all(urlPrefix + "/keepAlive", (_request, response) => {
response.json(true);
});
app.use(urlPrefix + "/login", routerLogin);
app.get(urlPrefix + "/logout", (request, response) => {
if (request.session.user && request.cookies[sessionCookieName]) {
// eslint-disable-next-line unicorn/no-null
request.session.destroy(null);
request.session = undefined;
response.clearCookie(sessionCookieName);
response.redirect(urlPrefix + "/");
} else {
response.redirect(urlPrefix + "/login");
}
});
// Catch 404 and forward to error handler
app.use((_request, _response, next) => {
next(createError(404));
});
// Error handler
app.use((error: { status: number; message: string },
request: express.Request, response: express.Response) => {
// Set locals, only providing error in development
response.locals.message = error.message;
response.locals.error = request.app.get("env") === "development" ? error : {};
// Render the error page
response.status(error.status || 500);
response.render("error");
});
export default app;

1
bin/www.d.ts vendored 100644
View File

@ -0,0 +1 @@
export {};

46
bin/www.js 100644
View File

@ -0,0 +1,46 @@
import { app } from "../app.js";
import http from "http";
import * as configFunctions from "../helpers/functions.config.js";
import exitHook from "exit-hook";
import debug from "debug";
const debugWWW = debug("general-licence-manager:www");
let httpServer;
const onError = (error) => {
if (error.syscall !== "listen") {
throw error;
}
switch (error.code) {
case "EACCES":
debugWWW("Requires elevated privileges");
process.exit(1);
case "EADDRINUSE":
debugWWW("Port is already in use.");
process.exit(1);
default:
throw error;
}
};
const onListening = (server) => {
const addr = server.address();
const bind = typeof addr === "string"
? "pipe " + addr
: "port " + addr.port.toString();
debugWWW("Listening on " + bind);
};
const httpPort = configFunctions.getProperty("application.httpPort");
if (httpPort) {
httpServer = http.createServer(app);
httpServer.listen(httpPort);
httpServer.on("error", onError);
httpServer.on("listening", () => {
onListening(httpServer);
});
debugWWW("HTTP listening on " + httpPort.toString());
}
exitHook(() => {
if (httpServer) {
debugWWW("Closing HTTP");
httpServer.close();
httpServer = undefined;
}
});

89
bin/www.ts 100644
View File

@ -0,0 +1,89 @@
/* eslint-disable no-process-exit, unicorn/no-process-exit */
import { app } from "../app.js";
import http from "http";
import * as configFunctions from "../helpers/functions.config.js";
import exitHook from "exit-hook";
import debug from "debug";
const debugWWW = debug("general-licence-manager:www");
let httpServer: http.Server;
interface ServerError extends Error {
syscall: string;
code: string;
}
const onError = (error: ServerError) => {
if (error.syscall !== "listen") {
throw error;
}
// handle specific listen errors with friendly messages
switch (error.code) {
// eslint-disable-next-line no-fallthrough
case "EACCES":
debugWWW("Requires elevated privileges");
process.exit(1);
// break;
// eslint-disable-next-line no-fallthrough
case "EADDRINUSE":
debugWWW("Port is already in use.");
process.exit(1);
// break;
// eslint-disable-next-line no-fallthrough
default:
throw error;
}
};
const onListening = (server: http.Server) => {
const addr = server.address();
const bind = typeof addr === "string"
? "pipe " + addr
: "port " + addr.port.toString();
debugWWW("Listening on " + bind);
};
/**
* Initialize HTTP
*/
const httpPort = configFunctions.getProperty("application.httpPort");
if (httpPort) {
httpServer = http.createServer(app);
httpServer.listen(httpPort);
httpServer.on("error", onError);
httpServer.on("listening", () => {
onListening(httpServer);
});
debugWWW("HTTP listening on " + httpPort.toString());
}
exitHook(() => {
if (httpServer) {
debugWWW("Closing HTTP");
httpServer.close();
httpServer = undefined;
}
});

4
data/databasePaths.d.ts vendored 100644
View File

@ -0,0 +1,4 @@
export declare const useTestDatabases: boolean;
export declare const lotOccupancyDB_live = "data/lotOccupancy.db";
export declare const lotOccupancyDB_testing = "data/lotOccupancy-testing.db";
export declare const lotOccupancyDB: string;

View File

@ -0,0 +1,12 @@
import * as configFunctions from "../helpers/functions.config.js";
import Debug from "debug";
const debug = Debug("lot-occupancy-system:databasePaths");
export const useTestDatabases = configFunctions.getProperty("application.useTestDatabases") || process.env.TEST_DATABASES === "true";
if (useTestDatabases) {
debug("Using \"-testing\" databases.");
}
export const lotOccupancyDB_live = "data/lotOccupancy.db";
export const lotOccupancyDB_testing = "data/lotOccupancy-testing.db";
export const lotOccupancyDB = useTestDatabases
? lotOccupancyDB_testing
: lotOccupancyDB_live;

View File

@ -0,0 +1,20 @@
import * as configFunctions from "../helpers/functions.config.js";
import Debug from "debug";
const debug = Debug("lot-occupancy-system:databasePaths");
// Determine if test databases should be used
export const useTestDatabases =
configFunctions.getProperty("application.useTestDatabases") || process.env.TEST_DATABASES === "true";
if (useTestDatabases) {
debug("Using \"-testing\" databases.");
}
export const lotOccupancyDB_live = "data/lotOccupancy.db";
export const lotOccupancyDB_testing = "data/lotOccupancy-testing.db";
export const lotOccupancyDB = useTestDatabases
? lotOccupancyDB_testing
: lotOccupancyDB_live;

1
gulpfile.d.ts vendored 100644
View File

@ -0,0 +1 @@
export {};

21
gulpfile.js 100644
View File

@ -0,0 +1,21 @@
import gulp from "gulp";
import changed from "gulp-changed";
import minify from "gulp-minify";
const publicJavascriptsDestination = "public/javascripts";
const publicJavascriptsMinFunction = () => {
return gulp.src("public-typescript/*.js", { allowEmpty: true })
.pipe(changed(publicJavascriptsDestination, {
extension: ".min.js"
}))
.pipe(minify({ noSource: true, ext: { min: ".min.js" } }))
.pipe(gulp.dest(publicJavascriptsDestination));
};
gulp.task("public-javascript-min", publicJavascriptsMinFunction);
const watchFunction = () => {
gulp.watch("public-typescript/*.js", publicJavascriptsMinFunction);
};
gulp.task("watch", watchFunction);
gulp.task("default", () => {
publicJavascriptsMinFunction();
watchFunction();
});

42
gulpfile.ts 100644
View File

@ -0,0 +1,42 @@
/* eslint-disable node/no-unpublished-import */
import gulp from "gulp";
import changed from "gulp-changed";
import minify from "gulp-minify";
/*
* Minify public/javascripts
*/
const publicJavascriptsDestination = "public/javascripts";
const publicJavascriptsMinFunction = () => {
return gulp.src("public-typescript/*.js", { allowEmpty: true })
.pipe(changed(publicJavascriptsDestination, {
extension: ".min.js"
}))
.pipe(minify({ noSource: true, ext: { min: ".min.js" } }))
.pipe(gulp.dest(publicJavascriptsDestination));
};
gulp.task("public-javascript-min", publicJavascriptsMinFunction);
/*
* Watch
*/
const watchFunction = () => {
gulp.watch("public-typescript/*.js", publicJavascriptsMinFunction);
};
gulp.task("watch", watchFunction);
/*
* Initialize default
*/
gulp.task("default", () => {
publicJavascriptsMinFunction();
watchFunction();
});

View File

@ -0,0 +1,3 @@
import type { RequestHandler } from "express";
export declare const handler: RequestHandler;
export default handler;

View File

@ -0,0 +1,6 @@
export const handler = (_request, response) => {
response.render("admin-licenceCategories", {
headTitle: "Licence Categories"
});
};
export default handler;

View File

@ -0,0 +1,12 @@
import type { RequestHandler } from "express";
export const handler: RequestHandler = (_request, response) => {
response.render("admin-licenceCategories", {
headTitle: "Licence Categories"
});
};
export default handler;

View File

@ -0,0 +1,3 @@
import type { RequestHandler } from "express";
export declare const handler: RequestHandler;
export default handler;

View File

@ -0,0 +1,6 @@
export const handler = (_request, response) => {
response.render("dashboard", {
headTitle: "Dashboard"
});
};
export default handler;

View File

@ -0,0 +1,12 @@
import type { RequestHandler } from "express";
export const handler: RequestHandler = (_request, response) => {
response.render("dashboard", {
headTitle: "Dashboard"
});
};
export default handler;

View File

@ -0,0 +1,3 @@
import type { RequestHandler } from "express";
export declare const handler: RequestHandler;
export default handler;

View File

@ -0,0 +1,7 @@
export const handler = (_request, response) => {
return response.render("licence-edit", {
headTitle: "Licence Update",
isCreate: false
});
};
export default handler;

View File

@ -0,0 +1,13 @@
import type { RequestHandler } from "express";
export const handler: RequestHandler = (_request, response) => {
return response.render("licence-edit", {
headTitle: "Licence Update",
isCreate: false
});
};
export default handler;

3
handlers/licences-get/new.d.ts vendored 100644
View File

@ -0,0 +1,3 @@
import type { RequestHandler } from "express";
export declare const handler: RequestHandler;
export default handler;

View File

@ -0,0 +1,7 @@
export const handler = (_request, response) => {
response.render("licence-edit", {
headTitle: "Licence Create",
isCreate: true
});
};
export default handler;

View File

@ -0,0 +1,13 @@
import type { RequestHandler } from "express";
export const handler: RequestHandler = (_request, response) => {
response.render("licence-edit", {
headTitle: "Licence Create",
isCreate: true
});
};
export default handler;

View File

@ -0,0 +1,3 @@
import type { RequestHandler } from "express";
export declare const handler: RequestHandler;
export default handler;

View File

@ -0,0 +1,6 @@
import * as configFunctions from "../../helpers/functions.config.js";
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
const __dirname = ".";
export const handler = async (request, response, next) => {
};
export default handler;

View File

@ -0,0 +1,59 @@
import type { RequestHandler } from "express";
import path from "path";
import * as ejs from "ejs";
import * as configFunctions from "../../helpers/functions.config.js";
import convertHTMLToPDF from "pdf-puppeteer";
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
const __dirname = ".";
export const handler: RequestHandler = async(request, response, next) => {
/*
const reportPath = path.join(__dirname, "reports", printTemplate);
const pdfCallbackFunction = (pdf: Buffer) => {
response.setHeader("Content-Disposition",
"attachment;" +
" filename=licence-" + licenceID.toString() + "-" + licence.recordUpdate_timeMillis.toString() + ".pdf"
);
response.setHeader("Content-Type", "application/pdf");
response.send(pdf);
};
await ejs.renderFile(
reportPath, {
configFunctions,
licence,
licenceTicketTypeSummary,
organization
}, {},
async(ejsError, ejsData) => {
if (ejsError) {
return next(ejsError);
}
await convertHTMLToPDF(ejsData, pdfCallbackFunction, {
format: "letter",
printBackground: true,
preferCSSPageSize: true
});
return;
}
);
*/
};
export default handler;

View File

@ -0,0 +1,3 @@
import type { RequestHandler } from "express";
export declare const handler: RequestHandler;
export default handler;

View File

@ -0,0 +1,8 @@
import * as configFunctions from "../../helpers/functions.config.js";
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
export const handler = (_request, response) => {
return response.render("licence-view", {
headTitle: "Licence View"
});
};
export default handler;

View File

@ -0,0 +1,19 @@
import type { RequestHandler } from "express";
import * as configFunctions from "../../helpers/functions.config.js";
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
export const handler: RequestHandler = (_request, response) => {
//const licenceID = Number(request.params.licenceID);
return response.render("licence-view", {
headTitle: "Licence View"
});
};
export default handler;

6
handlers/permissions.d.ts vendored 100644
View File

@ -0,0 +1,6 @@
import type { RequestHandler, Response } from "express";
export declare const forbiddenJSON: (response: Response) => Response;
export declare const adminGetHandler: RequestHandler;
export declare const adminPostHandler: RequestHandler;
export declare const updateGetHandler: RequestHandler;
export declare const updatePostHandler: RequestHandler;

View File

@ -0,0 +1,35 @@
import * as configFunctions from "../helpers/functions.config.js";
import * as userFunctions from "../helpers/functions.user.js";
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
export const forbiddenJSON = (response) => {
return response
.status(403)
.json({
success: false,
message: "Forbidden"
});
};
export const adminGetHandler = (request, response, next) => {
if (userFunctions.userIsAdmin(request)) {
return next();
}
return response.redirect(urlPrefix + "/dashboard");
};
export const adminPostHandler = (request, response, next) => {
if (userFunctions.userIsAdmin(request)) {
return next();
}
return response.json(forbiddenJSON);
};
export const updateGetHandler = (request, response, next) => {
if (userFunctions.userCanUpdate(request)) {
return next();
}
return response.redirect(urlPrefix + "/dashboard");
};
export const updatePostHandler = (request, response, next) => {
if (userFunctions.userCanUpdate(request)) {
return next();
}
return response.json(forbiddenJSON);
};

View File

@ -0,0 +1,59 @@
import type { RequestHandler, Response } from "express";
import * as configFunctions from "../helpers/functions.config.js";
import * as userFunctions from "../helpers/functions.user.js";
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
export const forbiddenJSON = (response: Response): Response => {
return response
.status(403)
.json({
success: false,
message: "Forbidden"
});
};
export const adminGetHandler: RequestHandler = (request, response, next) => {
if (userFunctions.userIsAdmin(request)) {
return next();
}
return response.redirect(urlPrefix + "/dashboard");
};
export const adminPostHandler: RequestHandler = (request, response, next) => {
if (userFunctions.userIsAdmin(request)) {
return next();
}
return response.json(forbiddenJSON);
};
export const updateGetHandler: RequestHandler = (request, response, next) => {
if (userFunctions.userCanUpdate(request)) {
return next();
}
return response.redirect(urlPrefix + "/dashboard");
};
export const updatePostHandler: RequestHandler = (request, response, next) => {
if (userFunctions.userCanUpdate(request)) {
return next();
}
return response.json(forbiddenJSON);
};

View File

@ -0,0 +1,3 @@
import type { RequestHandler } from "express";
export declare const handler: RequestHandler;
export default handler;

View File

@ -0,0 +1,3 @@
export const handler = (request, response) => {
};
export default handler;

View File

@ -0,0 +1,23 @@
import type { RequestHandler } from "express";
// import * as configFunctions from "../../helpers/functions.config.js";
export const handler: RequestHandler = (request, response) => {
// const reportName = request.params.reportName;
/*
const csv = rawToCSV(rowsColumnsObject);
response.setHeader("Content-Disposition",
"attachment; filename=" + reportName + "-" + Date.now().toString() + ".csv");
response.setHeader("Content-Type", "text/csv");
response.send(csv);
*/
};
export default handler;

View File

@ -0,0 +1 @@
export declare const initLotsDB: () => boolean;

View File

@ -0,0 +1,95 @@
import sqlite from "better-sqlite3";
import { lotOccupancyDB as databasePath } from "../data/databasePaths.js";
import debug from "debug";
const debugSQL = debug("lot-occupancy-system:databaseInitializer");
const recordColumns = " recordCreate_userName varchar(30) not null," +
" recordCreate_timeMillis integer not null," +
" recordUpdate_userName varchar(30) not null," +
" recordUpdate_timeMillis integer not null," +
" recordDelete_userName varchar(30)," +
" recordDelete_timeMillis integer";
export const initLotsDB = () => {
const lotOccupancyDB = sqlite(databasePath);
const row = lotOccupancyDB
.prepare("select name from sqlite_master where type = 'table' and name = 'Lots'")
.get();
if (!row) {
debugSQL("Creating " + databasePath);
lotOccupancyDB.prepare("create table if not exists ContactTypes (" +
"contactTypeId integer not null primary key autoincrement," +
" contactType varchar(100) not null," +
" isLotContactType bit not null default 0," +
" isOccupantContactType bit not null default 0," +
" orderNumber smallint not null default 0," +
recordColumns +
")").run();
lotOccupancyDB.prepare("create table if not exists Contacts (" +
"contactId integer not null primary key autoincrement," +
" contactTypeId integer not null," +
" contactName varchar(200) not null," +
" contactDescription text," +
" contactLatitude decimal(10, 8) check (contactLatitude between -90 and 90)," +
" contactLongitude decimal(11, 8) check (contactLongitude between -180 and 180)," +
" contactAddress1 varchar(50)," +
" contactAddress2 varchar(50)," +
" contactCity varchar(20)," +
" contactProvince varchar(2)," +
" contactPostalCode varchar(7)," +
" contactPhoneNumber varchar(30)," +
recordColumns + "," +
" foreign key (contactTypeId) references ContactTypes (contactTypeId)" +
")").run();
lotOccupancyDB.prepare("create table if not exists LotTypes (" +
"lotTypeId integer not null primary key autoincrement," +
" lotType varchar(100) not null," +
" orderNumber smallint not null default 0," +
recordColumns +
")").run();
lotOccupancyDB.prepare("create table if not exists LotTypeFields (" +
"lotTypeFieldId integer not null primary key autoincrement," +
" lotTypeId integer not null," +
" lotTypeField varchar(100) not null," +
" lotTypeFieldValues text," +
" isRequired bit not null default 0," +
" pattern varchar(100)," +
" minimumLength smallint not null default 1 check (minimumLength >= 0)," +
" maximumLength smallint not null default 100 check (maximumLength >= 0)," +
" orderNumber smallint not null default 0," +
recordColumns + "," +
" foreign key (lotTypeId) references LotTypes (lotTypeId)" +
")").run();
lotOccupancyDB.prepare("create table if not exists LotTypeStatuses (" +
"lotTypeStatusId integer not null primary key autoincrement," +
" lotTypeId integer not null," +
" lotTypeStatus varchar(100) not null," +
" orderNumber smallint not null default 0," +
recordColumns + "," +
" foreign key (lotTypeId) references LotTypes (lotTypeId)" +
")").run();
lotOccupancyDB.prepare("create table if not exists Lots (" +
"lotId integer not null primary key autoincrement," +
" lotTypeId integer not null," +
" lotName varchar(100)," +
" lotContactId integer," +
" lotLatitude decimal(10, 8) check (lotLatitude between -90 and 90)," +
" lotLongitude decimal(11, 8) check (lotLongitude between -180 and 180)," +
" lotTypeStatusId integer," +
recordColumns + "," +
" foreign key (lotTypeId) references LotTypes (lotTypeId)," +
" foreign key (lotContactId) references Contacts (contactId)," +
" foreign key (lotTypeStatusId) references LotTypeStatuses (lotTypeStatusId)" +
")").run();
lotOccupancyDB.prepare("create table if not exists LotComments (" +
"lotCommentId integer not null primary key autoincrement," +
" lotId integer not null," +
" lotCommentDate integer not null," +
" lotCommentTime integer not null," +
" lotComment text not null," +
recordColumns + "," +
" foreign key (lotId) references Lots (lotId)" +
")").run();
lotOccupancyDB.close();
return true;
}
return false;
};

View File

@ -0,0 +1,126 @@
import sqlite from "better-sqlite3";
import { lotOccupancyDB as databasePath } from "../data/databasePaths.js";
import debug from "debug";
const debugSQL = debug("lot-occupancy-system:databaseInitializer");
const recordColumns = " recordCreate_userName varchar(30) not null," +
" recordCreate_timeMillis integer not null," +
" recordUpdate_userName varchar(30) not null," +
" recordUpdate_timeMillis integer not null," +
" recordDelete_userName varchar(30)," +
" recordDelete_timeMillis integer";
export const initLotsDB = (): boolean => {
const lotOccupancyDB = sqlite(databasePath);
const row = lotOccupancyDB
.prepare("select name from sqlite_master where type = 'table' and name = 'Lots'")
.get();
if (!row) {
debugSQL("Creating " + databasePath);
// Contacts
lotOccupancyDB.prepare("create table if not exists ContactTypes (" +
"contactTypeId integer not null primary key autoincrement," +
" contactType varchar(100) not null," +
" isLotContactType bit not null default 0," +
" isOccupantContactType bit not null default 0," +
" orderNumber smallint not null default 0," +
recordColumns +
")").run();
lotOccupancyDB.prepare("create table if not exists Contacts (" +
"contactId integer not null primary key autoincrement," +
" contactTypeId integer not null," +
" contactName varchar(200) not null," +
" contactDescription text," +
" contactLatitude decimal(10, 8) check (contactLatitude between -90 and 90)," +
" contactLongitude decimal(11, 8) check (contactLongitude between -180 and 180)," +
" contactAddress1 varchar(50)," +
" contactAddress2 varchar(50)," +
" contactCity varchar(20)," +
" contactProvince varchar(2)," +
" contactPostalCode varchar(7)," +
" contactPhoneNumber varchar(30)," +
recordColumns + "," +
" foreign key (contactTypeId) references ContactTypes (contactTypeId)" +
")").run();
// Lot Types
lotOccupancyDB.prepare("create table if not exists LotTypes (" +
"lotTypeId integer not null primary key autoincrement," +
" lotType varchar(100) not null," +
" orderNumber smallint not null default 0," +
recordColumns +
")").run();
lotOccupancyDB.prepare("create table if not exists LotTypeFields (" +
"lotTypeFieldId integer not null primary key autoincrement," +
" lotTypeId integer not null," +
" lotTypeField varchar(100) not null," +
" lotTypeFieldValues text," +
" isRequired bit not null default 0," +
" pattern varchar(100)," +
" minimumLength smallint not null default 1 check (minimumLength >= 0)," +
" maximumLength smallint not null default 100 check (maximumLength >= 0)," +
" orderNumber smallint not null default 0," +
recordColumns + "," +
" foreign key (lotTypeId) references LotTypes (lotTypeId)" +
")").run();
lotOccupancyDB.prepare("create table if not exists LotTypeStatuses (" +
"lotTypeStatusId integer not null primary key autoincrement," +
" lotTypeId integer not null," +
" lotTypeStatus varchar(100) not null," +
" orderNumber smallint not null default 0," +
recordColumns + "," +
" foreign key (lotTypeId) references LotTypes (lotTypeId)" +
")").run();
// Lots
lotOccupancyDB.prepare("create table if not exists Lots (" +
"lotId integer not null primary key autoincrement," +
" lotTypeId integer not null," +
" lotName varchar(100)," +
" lotContactId integer," +
" lotLatitude decimal(10, 8) check (lotLatitude between -90 and 90)," +
" lotLongitude decimal(11, 8) check (lotLongitude between -180 and 180)," +
" lotTypeStatusId integer," +
recordColumns + "," +
" foreign key (lotTypeId) references LotTypes (lotTypeId)," +
" foreign key (lotContactId) references Contacts (contactId)," +
" foreign key (lotTypeStatusId) references LotTypeStatuses (lotTypeStatusId)" +
")").run();
lotOccupancyDB.prepare("create table if not exists LotComments (" +
"lotCommentId integer not null primary key autoincrement," +
" lotId integer not null," +
" lotCommentDate integer not null," +
" lotCommentTime integer not null," +
" lotComment text not null," +
recordColumns + "," +
" foreign key (lotId) references Lots (lotId)" +
")").run();
lotOccupancyDB.close();
return true;
}
return false;
};

View File

@ -0,0 +1 @@
export declare const authenticate: (userName: string, password: string) => Promise<boolean>;

View File

@ -0,0 +1,26 @@
import * as configFunctions from "./functions.config.js";
import ActiveDirectory from "activedirectory2";
const userDomain = configFunctions.getProperty("application.userDomain");
const activeDirectoryConfig = configFunctions.getProperty("activeDirectory");
const authenticateViaActiveDirectory = async (userName, password) => {
return new Promise((resolve) => {
try {
const ad = new ActiveDirectory(activeDirectoryConfig);
ad.authenticate(userDomain + "\\" + userName, password, async (error, auth) => {
if (error) {
resolve(false);
}
resolve(auth);
});
}
catch (_a) {
resolve(false);
}
});
};
export const authenticate = async (userName, password) => {
if (!userName || userName === "" || !password || password === "") {
return false;
}
return await authenticateViaActiveDirectory(userName, password);
};

View File

@ -0,0 +1,42 @@
import * as configFunctions from "./functions.config.js";
import ActiveDirectory from "activedirectory2";
const userDomain = configFunctions.getProperty("application.userDomain");
const activeDirectoryConfig = configFunctions.getProperty("activeDirectory");
const authenticateViaActiveDirectory = async (userName: string, password: string): Promise<boolean> => {
return new Promise((resolve) => {
try {
const ad = new ActiveDirectory(activeDirectoryConfig);
ad.authenticate(userDomain + "\\" + userName, password, async (error, auth) => {
if (error) {
resolve(false);
}
resolve(auth);
});
} catch {
resolve(false);
}
});
};
export const authenticate = async (userName: string, password: string): Promise<boolean> => {
if (!userName || userName === "" || !password || password === "") {
return false;
}
return await authenticateViaActiveDirectory(userName, password);
};

18
helpers/functions.config.d.ts vendored 100644
View File

@ -0,0 +1,18 @@
import type * as configTypes from "../types/configTypes";
export declare function getProperty(propertyName: "application.applicationName"): string;
export declare function getProperty(propertyName: "application.logoURL"): string;
export declare function getProperty(propertyName: "application.httpPort"): number;
export declare function getProperty(propertyName: "application.userDomain"): string;
export declare function getProperty(propertyName: "application.useTestDatabases"): boolean;
export declare function getProperty(propertyName: "activeDirectory"): configTypes.ConfigActiveDirectory;
export declare function getProperty(propertyName: "users.canLogin"): string[];
export declare function getProperty(propertyName: "users.canUpdate"): string[];
export declare function getProperty(propertyName: "users.isAdmin"): string[];
export declare function getProperty(propertyName: "reverseProxy.disableCompression"): boolean;
export declare function getProperty(propertyName: "reverseProxy.disableEtag"): boolean;
export declare function getProperty(propertyName: "reverseProxy.urlPrefix"): string;
export declare function getProperty(propertyName: "session.cookieName"): string;
export declare function getProperty(propertyName: "session.doKeepAlive"): boolean;
export declare function getProperty(propertyName: "session.maxAgeMillis"): number;
export declare function getProperty(propertyName: "session.secret"): string;
export declare const keepAliveMillis: number;

View File

@ -0,0 +1,32 @@
import { config } from "../data/config.js";
const configFallbackValues = new Map();
configFallbackValues.set("application.applicationName", "Lot Occupancy System");
configFallbackValues.set("application.logoURL", "/images/stamp.png");
configFallbackValues.set("application.httpPort", 7000);
configFallbackValues.set("application.useTestDatabases", false);
configFallbackValues.set("reverseProxy.disableCompression", false);
configFallbackValues.set("reverseProxy.disableEtag", false);
configFallbackValues.set("reverseProxy.urlPrefix", "");
configFallbackValues.set("session.cookieName", "lot-occupancy-system-user-sid");
configFallbackValues.set("session.secret", "cityssm/lot-occupancy-system");
configFallbackValues.set("session.maxAgeMillis", 60 * 60 * 1000);
configFallbackValues.set("session.doKeepAlive", false);
configFallbackValues.set("users.canLogin", ["administrator"]);
configFallbackValues.set("users.canUpdate", []);
configFallbackValues.set("users.isAdmin", ["administrator"]);
export function getProperty(propertyName) {
const propertyNameSplit = propertyName.split(".");
let currentObject = config;
for (const propertyNamePiece of propertyNameSplit) {
if (currentObject[propertyNamePiece]) {
currentObject = currentObject[propertyNamePiece];
}
else {
return configFallbackValues.get(propertyName);
}
}
return currentObject;
}
export const keepAliveMillis = getProperty("session.doKeepAlive")
? Math.max(getProperty("session.maxAgeMillis") / 2, getProperty("session.maxAgeMillis") - (10 * 60 * 1000))
: 0;

View File

@ -0,0 +1,82 @@
// eslint-disable-next-line node/no-unpublished-import
import { config } from "../data/config.js";
import type * as configTypes from "../types/configTypes";
/*
* SET UP FALLBACK VALUES
*/
const configFallbackValues = new Map<string, unknown>();
configFallbackValues.set("application.applicationName", "Lot Occupancy System");
configFallbackValues.set("application.logoURL", "/images/stamp.png");
configFallbackValues.set("application.httpPort", 7000);
configFallbackValues.set("application.useTestDatabases", false);
configFallbackValues.set("reverseProxy.disableCompression", false);
configFallbackValues.set("reverseProxy.disableEtag", false);
configFallbackValues.set("reverseProxy.urlPrefix", "");
configFallbackValues.set("session.cookieName", "lot-occupancy-system-user-sid");
configFallbackValues.set("session.secret", "cityssm/lot-occupancy-system");
configFallbackValues.set("session.maxAgeMillis", 60 * 60 * 1000);
configFallbackValues.set("session.doKeepAlive", false);
configFallbackValues.set("users.canLogin", ["administrator"]);
configFallbackValues.set("users.canUpdate", []);
configFallbackValues.set("users.isAdmin", ["administrator"]);
/*
* Set up function overloads
*/
export function getProperty(propertyName: "application.applicationName"): string;
export function getProperty(propertyName: "application.logoURL"): string;
export function getProperty(propertyName: "application.httpPort"): number;
export function getProperty(propertyName: "application.userDomain"): string;
export function getProperty(propertyName: "application.useTestDatabases"): boolean;
export function getProperty(propertyName: "activeDirectory"): configTypes.ConfigActiveDirectory;
export function getProperty(propertyName: "users.canLogin"): string[];
export function getProperty(propertyName: "users.canUpdate"): string[];
export function getProperty(propertyName: "users.isAdmin"): string[];
export function getProperty(propertyName: "reverseProxy.disableCompression"): boolean;
export function getProperty(propertyName: "reverseProxy.disableEtag"): boolean;
export function getProperty(propertyName: "reverseProxy.urlPrefix"): string;
export function getProperty(propertyName: "session.cookieName"): string;
export function getProperty(propertyName: "session.doKeepAlive"): boolean;
export function getProperty(propertyName: "session.maxAgeMillis"): number;
export function getProperty(propertyName: "session.secret"): string;
export function getProperty(propertyName: string): unknown {
const propertyNameSplit = propertyName.split(".");
let currentObject = config;
for (const propertyNamePiece of propertyNameSplit) {
if (currentObject[propertyNamePiece]) {
currentObject = currentObject[propertyNamePiece];
} else {
return configFallbackValues.get(propertyName);
}
}
return currentObject;
}
export const keepAliveMillis =
getProperty("session.doKeepAlive")
? Math.max(
getProperty("session.maxAgeMillis") / 2,
getProperty("session.maxAgeMillis") - (10 * 60 * 1000)
)
: 0;

3
helpers/functions.user.d.ts vendored 100644
View File

@ -0,0 +1,3 @@
import type { Request } from "express";
export declare const userIsAdmin: (request: Request) => boolean;
export declare const userCanUpdate: (request: Request) => boolean;

View File

@ -0,0 +1,16 @@
export const userIsAdmin = (request) => {
var _a;
const user = (_a = request.session) === null || _a === void 0 ? void 0 : _a.user;
if (!user) {
return false;
}
return user.userProperties.isAdmin;
};
export const userCanUpdate = (request) => {
var _a;
const user = (_a = request.session) === null || _a === void 0 ? void 0 : _a.user;
if (!user) {
return false;
}
return user.userProperties.canUpdate;
};

View File

@ -0,0 +1,25 @@
import type { Request } from "express";
export const userIsAdmin = (request: Request): boolean => {
const user = request.session?.user;
if (!user) {
return false;
}
return user.userProperties.isAdmin;
};
export const userCanUpdate = (request: Request): boolean => {
const user = request.session?.user;
if (!user) {
return false;
}
return user.userProperties.canUpdate;
};

3
nodemon.json 100644
View File

@ -0,0 +1,3 @@
{
"ignore": ["data/sessions/*"]
}

16613
package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

78
package.json 100644
View File

@ -0,0 +1,78 @@
{
"name": "lot-occupancy-system",
"version": "1.0.0-dev",
"type": "module",
"description": "A system for managing the occupancy of lots. (i.e. Cemetery management)",
"exports": "./app.js",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"build": "npx genversion --es6 --semi version.js",
"start": "cross-env NODE_ENV=production node ./bin/www",
"dev": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-manager:* nodemon ./bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cityssm/lot-occupancy-manager.git"
},
"author": "The Corporation of the City of Sault Ste. Marie",
"license": "MIT",
"bugs": {
"url": "https://github.com/cityssm/lot-occupancy-manager/issues"
},
"homepage": "https://github.com/cityssm/lot-occupancy-manager#readme",
"private": true,
"dependencies": {
"@cityssm/bulma-js": "^0.3.3",
"@cityssm/bulma-webapp-js": "^1.5.0",
"@cityssm/date-diff": "^2.2.3",
"@cityssm/expressjs-server-js": "^2.3.2",
"@fortawesome/fontawesome-free": "^5.15.4",
"activedirectory2": "^2.1.0",
"better-sqlite3": "^7.6.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cross-env": "^7.0.3",
"csurf": "^1.11.0",
"debug": "^4.3.4",
"ejs": "^3.1.8",
"exit-hook": "^3.0.0",
"express": "^4.18.1",
"express-rate-limit": "^6.4.0",
"express-session": "^1.17.3",
"http-errors": "^2.0.0",
"session-file-store": "^1.5.0"
},
"devDependencies": {
"@cityssm/bulma-webapp-css": "^0.11.0",
"@types/activedirectory2": "^1.2.3",
"@types/better-sqlite3": "^7.5.0",
"@types/compression": "^1.7.2",
"@types/cookie-parser": "^1.4.3",
"@types/csurf": "^1.11.2",
"@types/debug": "^4.1.7",
"@types/ejs": "^3.1.1",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.5",
"@types/gulp": "^4.0.9",
"@types/gulp-changed": "^0.0.35",
"@types/gulp-minify": "^3.1.1",
"@types/http-errors": "^1.8.2",
"@types/mocha": "^9.1.1",
"@types/node-windows": "^0.1.2",
"@types/session-file-store": "^1.2.2",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"eslint": "^8.19.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-unicorn": "^43.0.1",
"gulp": "^4.0.2",
"gulp-changed": "^4.0.3",
"gulp-minify": "^3.1.0",
"nodemon": "^2.0.19"
}
}

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,93 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
(() => {
const urlPrefix = document.querySelector("main").dataset.urlPrefix;
const formElement = document.querySelector("#form--filters");
const limitElement = document.querySelector("#filter--limit");
const offsetElement = document.querySelector("#filter--offset");
const searchResultsElement = document.querySelector("#container--searchResults");
const doLicenceSearchFunction = () => {
const currentLimit = Number.parseInt(limitElement.value, 10);
const currentOffset = Number.parseInt(offsetElement.value, 10);
searchResultsElement.innerHTML = "<p class=\"has-text-centered has-text-grey-lighter\">" +
"<i class=\"fas fa-3x fa-circle-notch fa-spin\" aria-hidden=\"true\"></i><br />" +
"<em>Loading licences...</em>" +
"</p>";
cityssm.postJSON(urlPrefix + "/licences/doSearch", formElement, (licenceResults) => {
const licenceList = licenceResults.licences;
if (licenceList.length === 0) {
searchResultsElement.innerHTML = "<div class=\"message is-info\">" +
"<div class=\"message-body\">" +
"<strong>Your search returned no results.</strong><br />" +
"Please try expanding your search criteria." +
"</div>" +
"</div>";
return;
}
searchResultsElement.innerHTML = "<table class=\"table is-fullwidth is-striped is-hoverable has-sticky-header\">" +
"<thead><tr>" +
"</tr></thead>" +
"<tbody></tbody>" +
"</table>";
const tbodyElement = searchResultsElement.querySelector("tbody");
for (const licenceObject of licenceList) {
const trElement = document.createElement("tr");
trElement.innerHTML = "";
tbodyElement.append(trElement);
}
searchResultsElement.insertAdjacentHTML("beforeend", "<div class=\"level is-block-print\">" +
"<div class=\"level-left has-text-weight-bold\">" +
"Displaying licences " +
(currentOffset + 1).toString() +
" to " +
Math.min(currentLimit + currentOffset, licenceResults.count).toString() +
" of " +
licenceResults.count.toString() +
"</div>" +
"</div>");
if (currentLimit < licenceResults.count) {
const paginationElement = document.createElement("nav");
paginationElement.className = "level-right is-hidden-print";
paginationElement.setAttribute("role", "pagination");
paginationElement.setAttribute("aria-label", "pagination");
if (currentOffset > 0) {
const previousElement = document.createElement("a");
previousElement.className = "button";
previousElement.textContent = "Previous";
previousElement.addEventListener("click", (clickEvent) => {
clickEvent.preventDefault();
offsetElement.value = Math.max(0, currentOffset - currentLimit).toString();
doLicenceSearchFunction();
});
paginationElement.append(previousElement);
}
if (currentLimit + currentOffset < licenceResults.count) {
const nextElement = document.createElement("a");
nextElement.className = "button ml-3";
nextElement.innerHTML =
"<span>Next Licences</span>" +
"<span class=\"icon\"><i class=\"fas fa-chevron-right\" aria-hidden=\"true\"></i></span>";
nextElement.addEventListener("click", (clickEvent) => {
clickEvent.preventDefault();
offsetElement.value = (currentOffset + currentLimit).toString();
doLicenceSearchFunction();
});
paginationElement.append(nextElement);
}
searchResultsElement.querySelector(".level").append(paginationElement);
}
});
};
const resetOffsetAndDoLicenceSearchFunction = () => {
offsetElement.value = "0";
doLicenceSearchFunction();
};
formElement.addEventListener("submit", (formEvent) => {
formEvent.preventDefault();
});
const inputElements = formElement.querySelectorAll(".input, .select select");
for (const inputElement of inputElements) {
inputElement.addEventListener("change", resetOffsetAndDoLicenceSearchFunction);
}
resetOffsetAndDoLicenceSearchFunction();
})();

View File

@ -0,0 +1,142 @@
/* eslint-disable unicorn/filename-case */
import type { cityssmGlobal } from "@cityssm/bulma-webapp-js/src/types";
import type * as recordTypes from "../types/recordTypes";
declare const cityssm: cityssmGlobal;
(() => {
const urlPrefix = document.querySelector("main").dataset.urlPrefix;
const formElement = document.querySelector("#form--filters") as HTMLFormElement;
const limitElement = document.querySelector("#filter--limit") as HTMLInputElement;
const offsetElement = document.querySelector("#filter--offset") as HTMLInputElement;
const searchResultsElement = document.querySelector("#container--searchResults") as HTMLElement;
const doLicenceSearchFunction = () => {
const currentLimit = Number.parseInt(limitElement.value, 10);
const currentOffset = Number.parseInt(offsetElement.value, 10);
searchResultsElement.innerHTML = "<p class=\"has-text-centered has-text-grey-lighter\">" +
"<i class=\"fas fa-3x fa-circle-notch fa-spin\" aria-hidden=\"true\"></i><br />" +
"<em>Loading licences...</em>" +
"</p>";
cityssm.postJSON(urlPrefix + "/licences/doSearch",
formElement,
(licenceResults: { count: number; licences: recordTypes.Licence[] }) => {
const licenceList = licenceResults.licences;
if (licenceList.length === 0) {
searchResultsElement.innerHTML = "<div class=\"message is-info\">" +
"<div class=\"message-body\">" +
"<strong>Your search returned no results.</strong><br />" +
"Please try expanding your search criteria." +
"</div>" +
"</div>";
return;
}
searchResultsElement.innerHTML = "<table class=\"table is-fullwidth is-striped is-hoverable has-sticky-header\">" +
"<thead><tr>" +
"</tr></thead>" +
"<tbody></tbody>" +
"</table>";
const tbodyElement = searchResultsElement.querySelector("tbody");
for (const licenceObject of licenceList) {
const trElement = document.createElement("tr");
trElement.innerHTML = "";
tbodyElement.append(trElement);
}
searchResultsElement.insertAdjacentHTML("beforeend", "<div class=\"level is-block-print\">" +
"<div class=\"level-left has-text-weight-bold\">" +
"Displaying licences " +
(currentOffset + 1).toString() +
" to " +
Math.min(currentLimit + currentOffset, licenceResults.count).toString() +
" of " +
licenceResults.count.toString() +
"</div>" +
"</div>");
if (currentLimit < licenceResults.count) {
const paginationElement = document.createElement("nav");
paginationElement.className = "level-right is-hidden-print";
paginationElement.setAttribute("role", "pagination");
paginationElement.setAttribute("aria-label", "pagination");
if (currentOffset > 0) {
const previousElement = document.createElement("a");
previousElement.className = "button";
previousElement.textContent = "Previous";
previousElement.addEventListener("click", (clickEvent) => {
clickEvent.preventDefault();
offsetElement.value = Math.max(0, currentOffset - currentLimit).toString();
doLicenceSearchFunction();
});
paginationElement.append(previousElement);
}
if (currentLimit + currentOffset < licenceResults.count) {
const nextElement = document.createElement("a");
nextElement.className = "button ml-3";
nextElement.innerHTML =
"<span>Next Licences</span>" +
"<span class=\"icon\"><i class=\"fas fa-chevron-right\" aria-hidden=\"true\"></i></span>";
nextElement.addEventListener("click", (clickEvent) => {
clickEvent.preventDefault();
offsetElement.value = (currentOffset + currentLimit).toString();
doLicenceSearchFunction();
});
paginationElement.append(nextElement);
}
searchResultsElement.querySelector(".level").append(paginationElement);
}
}
);
};
const resetOffsetAndDoLicenceSearchFunction = () => {
offsetElement.value = "0";
doLicenceSearchFunction();
};
formElement.addEventListener("submit", (formEvent) => {
formEvent.preventDefault();
});
const inputElements = formElement.querySelectorAll(".input, .select select");
for (const inputElement of inputElements) {
inputElement.addEventListener("change", resetOffsetAndDoLicenceSearchFunction);
}
resetOffsetAndDoLicenceSearchFunction();
})();

View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"moduleResolution": "Node",
"isolatedModules": false,
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"allowUnreachableCode": false
},
"compileOnSave": true,
"buildOnSave": true,
"atom": {
"rewriteTsconfig": false,
"formatOnSave": true
},
"formatCodeOptions": {
"indentSize": 2,
"tabSize": 2,
"insertSpaceAfterCommaDelimiter": true,
"insertSpaceAfterSemicolonInForStatements": true,
"insertSpaceBeforeAndAfterBinaryOperators": true,
"insertSpaceAfterKeywordsInControlFlowStatements": true,
"insertSpaceAfterFunctionKeywordForAnonymousFunctions": false,
"insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false,
"placeOpenBraceOnNewLineForFunctions": false,
"placeOpenBraceOnNewLineForControlBlocks": false
}
}

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,86 @@
@import '@cityssm/bulma-webapp-css/cityssm.min';
$white: #fff;
$black: #000;
.is-linethrough {
text-decoration: line-through;
}
.has-width-10 {
width: 10px;
}
/*
* Status containers
*/
.has-status-loaded .is-hidden-status-loaded,
.has-status-loading .is-hidden-status-loading,
.has-status-view .is-hidden-status-view,
fieldset:enabled .is-hidden-enabled {
display: none;
}
.has-status-view .is-noninteractive-status-view {
pointer-events: none;
}
// to fix page titles inside level components
// set on .level-left
.has-flex-shrink-1 {
flex-shrink: 1;
}
.has-border-radius-3 {
border-radius: 3px;
}
/*
* Tabs
*/
.tab-content {
display: none;
&.is-active {
display: block;
}
}
#is-login-page {
overflow: auto;
background-image: url('../images/login.jpg');
background-position: top center;
background-size: cover;
body > .columns {
min-height: 100vh;
}
}
.button.is-xsmall {
height: 2em;
padding-top: 0;
padding-bottom: 0;
font-size: 0.75rem;
}
/*
* Print
*/
.container.is-page {
width: 8.5in;
padding: 10px 20px 20px;
margin: 20px auto;
background-color: $white;
border: 1px solid $black;
@media print {
width: 100%;
padding: 0;
border: 0;
}
}

2
routes/admin.d.ts vendored 100644
View File

@ -0,0 +1,2 @@
export declare const router: import("express-serve-static-core").Router;
export default router;

6
routes/admin.js 100644
View File

@ -0,0 +1,6 @@
import { Router } from "express";
import * as permissionHandlers from "../handlers/permissions.js";
import handler_licenceCategories from "../handlers/admin-get/licenceCategories.js";
export const router = Router();
router.get("/licenceCategories", permissionHandlers.adminGetHandler, handler_licenceCategories);
export default router;

18
routes/admin.ts 100644
View File

@ -0,0 +1,18 @@
import { Router } from "express";
import * as permissionHandlers from "../handlers/permissions.js";
import handler_licenceCategories from "../handlers/admin-get/licenceCategories.js";
export const router = Router();
// Licence Categories
router.get("/licenceCategories",
permissionHandlers.adminGetHandler,
handler_licenceCategories);
export default router;

2
routes/dashboard.d.ts vendored 100644
View File

@ -0,0 +1,2 @@
export declare const router: import("express-serve-static-core").Router;
export default router;

View File

@ -0,0 +1,5 @@
import { Router } from "express";
import handler_dashboard from "../handlers/dashboard-get/dashboard.js";
export const router = Router();
router.get("/", handler_dashboard);
export default router;

View File

@ -0,0 +1,12 @@
import { Router } from "express";
import handler_dashboard from "../handlers/dashboard-get/dashboard.js";
export const router = Router();
router.get("/", handler_dashboard);
export default router;

2
routes/licences.d.ts vendored 100644
View File

@ -0,0 +1,2 @@
export declare const router: import("express-serve-static-core").Router;
export default router;

17
routes/licences.js 100644
View File

@ -0,0 +1,17 @@
import { Router } from "express";
import * as permissionHandlers from "../handlers/permissions.js";
import handler_view from "../handlers/licences-get/view.js";
import handler_new from "../handlers/licences-get/new.js";
import handler_edit from "../handlers/licences-get/edit.js";
import handler_print from "../handlers/licences-get/print.js";
export const router = Router();
router.get("/", (_request, response) => {
response.render("licence-search", {
headTitle: "Licences"
});
});
router.get("/new", permissionHandlers.updateGetHandler, handler_new);
router.get("/:licenceID", handler_view);
router.get("/:licenceID/edit", permissionHandlers.updateGetHandler, handler_edit);
router.get("/:licenceID/print", handler_print);
export default router;

53
routes/licences.ts 100644
View File

@ -0,0 +1,53 @@
import { Router } from "express";
import * as permissionHandlers from "../handlers/permissions.js";
import handler_view from "../handlers/licences-get/view.js";
import handler_new from "../handlers/licences-get/new.js";
import handler_edit from "../handlers/licences-get/edit.js";
import handler_print from "../handlers/licences-get/print.js";
export const router = Router();
/*
* Licence Search
*/
router.get("/", (_request, response) => {
response.render("licence-search", {
headTitle: "Licences"
});
});
/*
* Licence View / Edit
*/
router.get("/new",
permissionHandlers.updateGetHandler,
handler_new);
router.get("/:licenceID",
handler_view);
router.get("/:licenceID/edit",
permissionHandlers.updateGetHandler,
handler_edit);
router.get("/:licenceID/print",
handler_print);
export default router;

2
routes/login.d.ts vendored 100644
View File

@ -0,0 +1,2 @@
export declare const router: import("express-serve-static-core").Router;
export default router;

74
routes/login.js 100644
View File

@ -0,0 +1,74 @@
import { Router } from "express";
import * as configFunctions from "../helpers/functions.config.js";
import * as authenticationFunctions from "../helpers/functions.authentication.js";
export const router = Router();
const getSafeRedirectURL = (possibleRedirectURL = "") => {
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
const urlToCheck = (possibleRedirectURL.startsWith(urlPrefix)
? possibleRedirectURL.slice(urlPrefix.length)
: possibleRedirectURL).toLowerCase();
switch (urlToCheck) {
case "/licences":
case "/reports":
return urlPrefix + urlToCheck;
}
return urlPrefix + "/dashboard";
};
router.route("/")
.get((request, response) => {
const sessionCookieName = configFunctions.getProperty("session.cookieName");
if (request.session.user && request.cookies[sessionCookieName]) {
const redirectURL = getSafeRedirectURL((request.query.redirect || ""));
response.redirect(redirectURL);
}
else {
response.render("login", {
userName: "",
message: "",
redirect: request.query.redirect
});
}
})
.post(async (request, response) => {
const userName = request.body.userName;
const passwordPlain = request.body.password;
const redirectURL = getSafeRedirectURL(request.body.redirect);
const isAuthenticated = await authenticationFunctions.authenticate(userName, passwordPlain);
let userObject;
if (isAuthenticated) {
const userNameLowerCase = userName.toLowerCase();
const canLogin = configFunctions.getProperty("users.canLogin")
.some((currentUserName) => {
return userNameLowerCase === currentUserName.toLowerCase();
});
if (canLogin) {
const canUpdate = configFunctions.getProperty("users.canUpdate")
.some((currentUserName) => {
return userNameLowerCase === currentUserName.toLowerCase();
});
const isAdmin = configFunctions.getProperty("users.isAdmin")
.some((currentUserName) => {
return userNameLowerCase === currentUserName.toLowerCase();
});
userObject = {
userName: userNameLowerCase,
userProperties: {
canUpdate,
isAdmin
}
};
}
}
if (isAuthenticated && userObject) {
request.session.user = userObject;
response.redirect(redirectURL);
}
else {
response.render("login", {
userName,
message: "Login Failed",
redirect: redirectURL
});
}
});
export default router;

109
routes/login.ts 100644
View File

@ -0,0 +1,109 @@
import { Router } from "express";
import * as configFunctions from "../helpers/functions.config.js";
import * as authenticationFunctions from "../helpers/functions.authentication.js";
import type * as recordTypes from "../types/recordTypes";
export const router = Router();
const getSafeRedirectURL = (possibleRedirectURL = "") => {
const urlPrefix = configFunctions.getProperty("reverseProxy.urlPrefix");
const urlToCheck = (possibleRedirectURL.startsWith(urlPrefix)
? possibleRedirectURL.slice(urlPrefix.length)
: possibleRedirectURL).toLowerCase();
switch (urlToCheck) {
case "/licences":
case "/reports":
return urlPrefix + urlToCheck;
}
return urlPrefix + "/dashboard";
};
router.route("/")
.get((request, response) => {
const sessionCookieName = configFunctions.getProperty("session.cookieName");
if (request.session.user && request.cookies[sessionCookieName]) {
const redirectURL = getSafeRedirectURL((request.query.redirect || "") as string);
response.redirect(redirectURL);
} else {
response.render("login", {
userName: "",
message: "",
redirect: request.query.redirect
});
}
})
.post(async (request, response) => {
const userName = request.body.userName as string;
const passwordPlain = request.body.password as string;
const redirectURL = getSafeRedirectURL(request.body.redirect);
const isAuthenticated = await authenticationFunctions.authenticate(userName, passwordPlain)
let userObject: recordTypes.User;
if (isAuthenticated) {
const userNameLowerCase = userName.toLowerCase();
const canLogin = configFunctions.getProperty("users.canLogin")
.some((currentUserName) => {
return userNameLowerCase === currentUserName.toLowerCase();
});
if (canLogin) {
const canUpdate = configFunctions.getProperty("users.canUpdate")
.some((currentUserName) => {
return userNameLowerCase === currentUserName.toLowerCase();
});
const isAdmin = configFunctions.getProperty("users.isAdmin")
.some((currentUserName) => {
return userNameLowerCase === currentUserName.toLowerCase();
});
userObject = {
userName: userNameLowerCase,
userProperties: {
canUpdate,
isAdmin
}
};
}
}
if (isAuthenticated && userObject) {
request.session.user = userObject;
response.redirect(redirectURL);
} else {
response.render("login", {
userName,
message: "Login Failed",
redirect: redirectURL
});
}
});
export default router;

2
routes/reports.d.ts vendored 100644
View File

@ -0,0 +1,2 @@
export declare const router: import("express-serve-static-core").Router;
export default router;

13
routes/reports.js 100644
View File

@ -0,0 +1,13 @@
import { Router } from "express";
import handler_reportName from "../handlers/reports-get/reportName.js";
import * as dateTimeFns from "@cityssm/expressjs-server-js/dateTimeFns.js";
export const router = Router();
router.get("/", (_request, response) => {
const rightNow = new Date();
response.render("report-search", {
headTitle: "Reports",
todayDateString: dateTimeFns.dateToString(rightNow)
});
});
router.all("/:reportName", handler_reportName);
export default router;

26
routes/reports.ts 100644
View File

@ -0,0 +1,26 @@
import { Router } from "express";
import handler_reportName from "../handlers/reports-get/reportName.js";
import * as dateTimeFns from "@cityssm/expressjs-server-js/dateTimeFns.js";
export const router = Router();
router.get("/", (_request, response) => {
const rightNow = new Date();
response.render("report-search", {
headTitle: "Reports",
todayDateString: dateTimeFns.dateToString(rightNow)
});
});
router.all("/:reportName", handler_reportName);
export default router;

36
tsconfig.json 100644
View File

@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "ES2020",
"moduleResolution": "Node",
"isolatedModules": false,
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"allowUnreachableCode": false,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"esModuleInterop": true
},
"compileOnSave": true,
"buildOnSave": true,
"atom": {
"rewriteTsconfig": false,
"formatOnSave": true
},
"formatCodeOptions": {
"indentSize": 2,
"tabSize": 2,
"insertSpaceAfterCommaDelimiter": true,
"insertSpaceAfterSemicolonInForStatements": true,
"insertSpaceBeforeAndAfterBinaryOperators": true,
"insertSpaceAfterKeywordsInControlFlowStatements": true,
"insertSpaceAfterFunctionKeywordForAnonymousFunctions": false,
"insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false,
"placeOpenBraceOnNewLineForFunctions": false,
"placeOpenBraceOnNewLineForControlBlocks": false
},
"exclude": [
"public-typescript/*"
]
}

39
types/configTypes.d.ts vendored 100644
View File

@ -0,0 +1,39 @@
export interface Config {
application?: ConfigApplication;
session?: ConfigSession;
reverseProxy?: {
disableCompression: boolean;
disableEtag: boolean;
urlPrefix: string;
};
activeDirectory?: ConfigActiveDirectory;
users?: {
canLogin?: string[];
canUpdate?: string[];
isAdmin?: string[];
};
defaults?: ConfigDefaults;
}
interface ConfigApplication {
applicationName?: string;
logoURL?: string;
httpPort?: number;
userDomain?: string;
}
interface ConfigSession {
cookieName?: string;
secret?: string;
maxAgeMillis?: number;
doKeepAlive?: boolean;
}
export interface ConfigActiveDirectory {
url: string;
baseDN: string;
username: string;
password: string;
}
interface ConfigDefaults {
licenseeCity: string;
licenseeProvince: string;
}
export {};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,44 @@
export interface Config {
application?: ConfigApplication;
session?: ConfigSession;
reverseProxy?: {
disableCompression: boolean;
disableEtag: boolean;
urlPrefix: string;
};
activeDirectory?: ConfigActiveDirectory;
users?: {
canLogin?: string[];
canUpdate?: string[];
isAdmin?: string[];
},
defaults?: ConfigDefaults;
}
interface ConfigApplication {
applicationName?: string;
logoURL?: string;
httpPort?: number;
userDomain?: string;
}
interface ConfigSession {
cookieName?: string;
secret?: string;
maxAgeMillis?: number;
doKeepAlive?: boolean;
}
export interface ConfigActiveDirectory {
url: string;
baseDN: string;
username: string;
password: string;
}
interface ConfigDefaults {
licenseeCity: string;
licenseeProvince: string;
}

70
types/recordTypes.d.ts vendored 100644
View File

@ -0,0 +1,70 @@
export interface Record {
recordCreate_userName?: string;
recordCreate_timeMillis?: number;
recordCreate_dateString?: string;
recordUpdate_userName?: string;
recordUpdate_timeMillis?: number;
recordUpdate_dateString?: string;
recordUpdate_timeString?: string;
recordDelete_userName?: string;
recordDelete_timeMillis?: number;
recordDelete_dateString?: string;
}
export interface Licence extends Record {
licenceId: number;
licenceCategoryKey: string;
licenceNumber: string;
licenseeName: string;
licenseeBusinessName: string;
licenseeAddress1: string;
licenseeAddress2: string;
licenseeCity: string;
licenseeProvince: string;
licenseePostalCode: string;
isRenewal: boolean;
startDate: number;
startDateString: string;
endDate: number;
endDateString: string;
issueDate: number;
issueDateString: string;
issueTime: number;
issueTimeString: string;
licenceFee: number;
replacementFee: number;
licenceFields?: LicenceField[];
licenceApprovals?: LicenceApproval[];
licenceTransactions?: LicenceTransaction[];
}
export interface LicenceField {
licenceId?: number;
licenceFieldKey: string;
licenceFieldValue: string;
}
export interface LicenceApproval {
licenceId?: number;
licenceApprovalKey: string;
}
export interface LicenceTransaction extends Record {
transactionIndex: number;
transactionDate: number;
transactionDateString?: string;
transactionTime: number;
transactionTimeString?: string;
externalReceiptNumber: string;
transactionAmount: number;
transactionNote: string;
}
export interface User {
userName: string;
userProperties?: UserProperties;
}
export interface UserProperties {
canUpdate: boolean;
isAdmin: boolean;
}
declare module "express-session" {
interface Session {
user: User;
}
}

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,94 @@
/*
* LICENCE DB TYPES
*/
export interface Record {
recordCreate_userName?: string;
recordCreate_timeMillis?: number;
recordCreate_dateString?: string;
recordUpdate_userName?: string;
recordUpdate_timeMillis?: number;
recordUpdate_dateString?: string;
recordUpdate_timeString?: string;
recordDelete_userName?: string;
recordDelete_timeMillis?: number;
recordDelete_dateString?: string;
}
export interface Licence extends Record {
licenceId: number;
licenceCategoryKey: string;
licenceNumber: string;
licenseeName: string;
licenseeBusinessName: string;
licenseeAddress1: string;
licenseeAddress2: string;
licenseeCity: string;
licenseeProvince: string;
licenseePostalCode: string;
isRenewal: boolean;
startDate: number;
startDateString: string;
endDate: number;
endDateString: string;
issueDate: number;
issueDateString: string;
issueTime: number;
issueTimeString: string;
licenceFee: number;
replacementFee: number;
licenceFields?: LicenceField[];
licenceApprovals?: LicenceApproval[];
licenceTransactions?: LicenceTransaction[];
}
export interface LicenceField {
licenceId?: number;
licenceFieldKey: string;
licenceFieldValue: string;
}
export interface LicenceApproval {
licenceId?: number;
licenceApprovalKey: string;
}
export interface LicenceTransaction extends Record {
transactionIndex: number;
transactionDate: number;
transactionDateString?: string;
transactionTime: number;
transactionTimeString?: string;
externalReceiptNumber: string;
transactionAmount: number;
transactionNote: string;
}
export interface User {
userName: string;
userProperties?: UserProperties;
}
export interface UserProperties {
canUpdate: boolean;
isAdmin: boolean;
}
declare module "express-session" {
interface Session {
user: User;
}
}

2
version.js 100644
View File

@ -0,0 +1,2 @@
// generated by genversion
export const version = '1.0.0-dev';

View File

@ -0,0 +1,3 @@
</main>
</body>
</html>

23
views/_footerA.ejs 100644
View File

@ -0,0 +1,23 @@
</main>
<footer class="footer has-background-grey-lighter has-text-dark is-hidden-print mt-4">
<div class="container">
<div class="content has-text-right">
<p class="has-text-grey-dark">
<strong>
<%= configFunctions.getProperty("application.applicationName") %>
</strong><br />
Build <%= buildNumber %>
</p>
</div>
</div>
</footer>
<script>
window.exports = window.exports || {};
</script>
<script src="<%= urlPrefix %>/lib/cityssm-bulma-js/bulma-js.js"></script>
<script src="<%= urlPrefix %>/lib/cityssm-bulma-webapp-js/dist/cityssm.min.js"></script>
<script>
cityssm.htmlModalFolder ="<%= urlPrefix %>/html/";
bulmaJS.init();
</script>
<script src="<%= urlPrefix %>/lib/cityssm-bulma-webapp-js/dist/cityssm-theme.min.js" defer></script>

View File

@ -0,0 +1,3 @@
</body>
</html>

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html class="has-background-dark m-0 p-0" lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>
<%= headTitle %>: <%= configFunctions.getProperty("application.applicationName") %>
</title>
<link rel="icon" href="<%= urlPrefix %>/images/favicon.png" />
<link rel="stylesheet" href="<%= urlPrefix %>/stylesheets/style.min.css" />
<link rel="stylesheet" href="<%= urlPrefix %>/lib/fa/css/all.min.css" />
</head>
<body class="m-0 p-0" style="min-height:100vh;">
<div class="fixed-container is-fixed-bottom-right mx-4 my-4 has-text-right is-hidden-print">
<button class="button is-circle is-primary has-tooltip-left" data-tooltip="Print" type="button" onclick="window.print()" autofocus>
<i class="fas fa-print" aria-hidden="true"></i>
<span class="sr-only">Print</span>
</button>
</div>
<main class="container is-page">

87
views/_header.ejs 100644
View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html class="has-navbar-fixed-top is-fullwidth" lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content="<%= csrfToken %>">
<title>
<%= headTitle %>: <%= configFunctions.getProperty("application.applicationName") %>
</title>
<link rel="icon" href="<%= urlPrefix %>/images/favicon.png" />
<link rel="stylesheet" href="<%= urlPrefix %>/stylesheets/style.min.css" />
<link rel="stylesheet" href="<%= urlPrefix %>/lib/fa/css/all.min.css" />
</head>
<body>
<nav class="navbar is-light is-fixed-top is-static-print" id="cityssm-theme--navbar" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item" href="<%= urlPrefix %>/dashboard">
<img class="mr-3" src="<%= urlPrefix + configFunctions.getProperty("application.logoURL") %>" alt="" height="28" />
<strong><%= configFunctions.getProperty("application.applicationName") %></strong>
</a>
<a class="navbar-burger burger is-hidden-print" role="button" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="<%= urlPrefix %>/licences">
<span class="icon mr-1">
<i class="fas fa-fw fa-certificate" aria-hidden="true"></i>
</span>
<span>Licences</span>
</a>
<div class="navbar-item has-dropdown">
<a class="navbar-link is-arrowless" href="#">
<span>More</span>
<span class="icon ml-1">
<i class="fas fa-angle-down" aria-hidden="true"></i>
</span>
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="<%= urlPrefix %>/reports">
<span class="icon mr-1">
<i class="fas fa-fw fa-file" aria-hidden="true"></i>
</span>
<span>Reports</span>
</a>
<hr class="navbar-divider" />
<a class="navbar-item" href="https://cityssm.github.io/general-licence-manager/" target="_blank" rel="noopener noreferrer">
<span class="icon mr-1">
<i class="fas fa-fw fa-question-circle" aria-hidden="true"></i>
</span>
<span>Help</span>
</a>
</div>
</div>
</div>
<div class="navbar-end">
<a class="navbar-item" id="cityssm-theme--logout-button" role="button" href="#">
<span class="icon mr-1">
<i class="fas fa-fw fa-sign-out-alt" aria-hidden="true"></i>
</span>
<span>Log Out <%=user.userName %></span>
</a>
</div>
</div>
</div>
</nav>
<main class="container pt-2 px-3 mr-auto has-min-page-height"
data-session-keep-alive-millis="<%= configFunctions.keepAliveMillis %>"
data-url-prefix="<%= urlPrefix %>"
data-can-update="<%= user.userProperties.canUpdate ? "true" : "false" %>"
data-is-admin="<%= user.userProperties.isAdmin ? "true" : "false" %>">

View File

111
views/dashboard.ejs 100644
View File

@ -0,0 +1,111 @@
<%- include('_header'); -%>
<div class="level">
<div class="level-left has-flex-shrink-1">
<h1 class="title is-1">
<%= configFunctions.getProperty("application.applicationName") %>
</h1>
</div>
</div>
<div class="columns">
<div class="column">
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<i class="fas fa-3x fa-fw fa-certificate" aria-hidden="true"></i>
</div>
<div class="media-content has-text-black">
<h2 class="title is-4 is-marginless">
<a href="<%= urlPrefix %>/licences">Licences</a>
</h2>
<p>View and maintain licences.</p>
</div>
</div>
</div>
<% if (user.userProperties.canUpdate) { %>
<div class="card-footer">
<a class="card-footer-item" href="<%= urlPrefix %>/licences/new">
<span class="icon">
<i class="fas fa-plus" aria-hidden="true"></i>
</span>
<span>Create a New Licence</span>
</a>
</div>
<% } %>
</div>
</div>
<div class="column">
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<i class="fas fa-3x fa-fw fa-file" aria-hidden="true"></i>
</div>
<div class="media-content has-text-black">
<h2 class="title is-4 is-marginless">
<a href="<%= urlPrefix %>/reports">Report Library</a>
</h2>
<p>Produce reports and export data.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns">
<div class="column">
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<i class="fas fa-3x fa-fw fa-question-circle" aria-hidden="true"></i>
</div>
<div class="media-content has-text-black">
<h2 class="title is-4 is-marginless">
<a href="https://cityssm.github.io/general-licence-manager/" target="_blank" rel="noopener noreferrer">Help Documentation</a>
</h2>
<p>Instructions on how to use this application.</p>
</div>
</div>
</div>
<div class="card-footer">
<a class="card-footer-item has-tooltip-bottom" data-tooltip="Latest Updates, Issue Tracker, Say Hello" href="https://github.com/cityssm/general-licence-manager" target="_blank" rel="noreferrer">
<span class="icon">
<i class="fab fa-github" aria-hidden="true"></i>
</span>
GitHub
</a>
</div>
</div>
</div>
</div>
<% if (user.userProperties.isAdmin) { %>
<h2 class="title is-3">Administrator Tools</h2>
<div class="columns">
<div class="column">
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<i class="fas fa-3x fa-fw fa-cogs" aria-hidden="true"></i>
</div>
<div class="media-content has-text-black">
<h2 class="title is-4 is-marginless">
<a href="<%= urlPrefix %>/admin/licenceCategories">Licence Categories</a>
</h2>
<p>Add new licence types. Maintain existing licence types.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<% } %>
<%- include('_footerA'); -%>
<%- include('_footerB'); -%>

3
views/error.ejs 100644
View File

@ -0,0 +1,3 @@
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>

View File

View File

@ -0,0 +1,80 @@
<%- include('_header'); -%>
<div class="columns is-variable is-4-mobile is-4-tablet is-block-print" id="is-site-layout">
<div class="column is-block-print">
<nav class="breadcrumb">
<ul>
<li><a href="<%= urlPrefix %>/dashboard">Home</a></li>
<li class="is-active"><a href="#" aria-current="page">
<span class="icon is-small"><i class="fas fa-certificate" aria-hidden="true"></i></span>
<span>Licences</span>
</a></li>
</ul>
</nav>
<h1 class="title is-1">
Find a Licence
</h1>
<% if (user.userProperties.canUpdate) { %>
<div class="fixed-container is-fixed-bottom-right mx-4 my-4 has-text-right is-hidden-print">
<a class="button is-circle is-primary has-tooltip-left" data-tooltip="Create a New Licence" href="<%= urlPrefix %>/licences/new">
<i class="fas fa-plus" aria-hidden="true"></i>
<span class="sr-only">Create a New Licence</span>
</a>
</div>
<% } %>
<div class="box">
<form id="form--filters">
<input id="filter--limit" name="limit" type="hidden" value="50" />
<input id="filter--offset" name="offset" type="hidden" value="0" />
<div class="columns">
<div class="column">
<div class="field">
<label class="label" for="filter--licenceTypeKey">Licence Type</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="filter--licenceTypeKey" name="licenceTypeKey">
<option value="">(All Types)</option>
</select>
</div>
<span class="icon is-small is-left">
<i class="fas fa-filter" aria-hidden="true"></i>
</span>
</div>
</div>
</div>
<div class="column">
<div class="field">
<label class="label" for="filter--licenceStatus">Status</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="filter--licenceStatus" name="licenceStatus">
<option value="">(All Statuses)</option>
<option value="active">Active</option>
<option value="past">Past</option>
</select>
</div>
<span class="icon is-small is-left">
<i class="fas fa-filter" aria-hidden="true"></i>
</span>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="is-block-print" id="container--searchResults" data-external-licence-number-label="<%= configFunctions.getProperty("licences.externalLicenceNumber.fieldLabel") %>"></div>
</div>
</div>
<%- include('_footerA'); -%>
<script src="<%= urlPrefix %>/javascripts/licence-search.min.js"></script>
<%- include('_footerB'); -%>

Some files were not shown because too many files have changed in this diff Show More