initial commit
parent
a0d78854f1
commit
ed5d7b8d4b
|
|
@ -0,0 +1 @@
|
||||||
|
*.db-journal
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
version: "2"
|
||||||
|
checks:
|
||||||
|
file-lines:
|
||||||
|
config:
|
||||||
|
threshold: 1000
|
||||||
|
method-complexity:
|
||||||
|
config:
|
||||||
|
threshold: 15
|
||||||
|
method-lines:
|
||||||
|
config:
|
||||||
|
threshold: 300
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
**/*.d.ts
|
||||||
|
**/*.ejs
|
||||||
|
**/*.js
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"reject": [
|
||||||
|
"@fortawesome/fontawesome-free"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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'
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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!
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export declare const app: import("express-serve-static-core").Express;
|
||||||
|
export default app;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import type { RequestHandler } from "express";
|
||||||
|
export declare const handler: RequestHandler;
|
||||||
|
export default handler;
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const handler = (_request, response) => {
|
||||||
|
response.render("admin-licenceCategories", {
|
||||||
|
headTitle: "Licence Categories"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export default handler;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import type { RequestHandler } from "express";
|
||||||
|
export declare const handler: RequestHandler;
|
||||||
|
export default handler;
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const handler = (_request, response) => {
|
||||||
|
response.render("dashboard", {
|
||||||
|
headTitle: "Dashboard"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export default handler;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { RequestHandler } from "express";
|
||||||
|
|
||||||
|
|
||||||
|
export const handler: RequestHandler = (_request, response) => {
|
||||||
|
|
||||||
|
response.render("dashboard", {
|
||||||
|
headTitle: "Dashboard"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default handler;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import type { RequestHandler } from "express";
|
||||||
|
export declare const handler: RequestHandler;
|
||||||
|
export default handler;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const handler = (_request, response) => {
|
||||||
|
return response.render("licence-edit", {
|
||||||
|
headTitle: "Licence Update",
|
||||||
|
isCreate: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export default handler;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import type { RequestHandler } from "express";
|
||||||
|
export declare const handler: RequestHandler;
|
||||||
|
export default handler;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const handler = (_request, response) => {
|
||||||
|
response.render("licence-edit", {
|
||||||
|
headTitle: "Licence Create",
|
||||||
|
isCreate: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export default handler;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import type { RequestHandler } from "express";
|
||||||
|
export declare const handler: RequestHandler;
|
||||||
|
export default handler;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import type { RequestHandler } from "express";
|
||||||
|
export declare const handler: RequestHandler;
|
||||||
|
export default handler;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import type { RequestHandler } from "express";
|
||||||
|
export declare const handler: RequestHandler;
|
||||||
|
export default handler;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const handler = (request, response) => {
|
||||||
|
};
|
||||||
|
export default handler;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export declare const initLotsDB: () => boolean;
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export declare const authenticate: (userName: string, password: string) => Promise<boolean>;
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import type { Request } from "express";
|
||||||
|
export declare const userIsAdmin: (request: Request) => boolean;
|
||||||
|
export declare const userCanUpdate: (request: Request) => boolean;
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"ignore": ["data/sessions/*"]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
||||||
|
|
@ -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();
|
||||||
|
})();
|
||||||
|
|
@ -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();
|
||||||
|
})();
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export declare const router: import("express-serve-static-core").Router;
|
||||||
|
export default router;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export declare const router: import("express-serve-static-core").Router;
|
||||||
|
export default router;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export declare const router: import("express-serve-static-core").Router;
|
||||||
|
export default router;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export declare const router: import("express-serve-static-core").Router;
|
||||||
|
export default router;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export declare const router: import("express-serve-static-core").Router;
|
||||||
|
export default router;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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 {};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// generated by genversion
|
||||||
|
export const version = '1.0.0-dev';
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -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">
|
||||||
|
|
@ -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" %>">
|
||||||
|
|
@ -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'); -%>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<h1><%= message %></h1>
|
||||||
|
<h2><%= error.status %></h2>
|
||||||
|
<pre><%= error.stack %></pre>
|
||||||
|
|
@ -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
Loading…
Reference in New Issue