commit
8f491e01ed
119 changed files with 2866 additions and 1420 deletions
@ -1,18 +1,38 @@ |
|||||||
import cors from 'cors'; |
import cors from 'cors'; |
||||||
import express from 'express'; |
import express, { json } from 'express'; |
||||||
import path from 'path'; |
|
||||||
|
|
||||||
import API_ROOT_PATH from './lib/consts/API_ROOT_PATH'; |
|
||||||
|
|
||||||
|
import { guardApi } from './lib/assertAuthentication'; |
||||||
|
import passport from './passport'; |
||||||
import routes from './routes'; |
import routes from './routes'; |
||||||
|
import { rrouters } from './lib/rrouters'; |
||||||
|
import session from './session'; |
||||||
|
|
||||||
|
export default (async () => { |
||||||
|
const app = express(); |
||||||
|
|
||||||
|
app.use(json()); |
||||||
|
|
||||||
|
app.use( |
||||||
|
cors({ |
||||||
|
origin: true, |
||||||
|
credentials: true, |
||||||
|
}), |
||||||
|
); |
||||||
|
|
||||||
|
// Add session handler to the chain **after** adding other handlers that do
|
||||||
|
// not depend on session(s).
|
||||||
|
app.use(await session); |
||||||
|
|
||||||
const app = express(); |
app.use(passport.initialize()); |
||||||
|
app.use(passport.authenticate('session')); |
||||||
|
|
||||||
app.use(express.json()); |
rrouters(app, routes.private, { |
||||||
app.use(cors()); |
assign: (router) => [guardApi, router], |
||||||
|
route: '/api', |
||||||
|
}); |
||||||
|
rrouters(app, routes.public, { route: '/api' }); |
||||||
|
|
||||||
Object.entries(routes).forEach(([route, router]) => { |
app.use(routes.static); |
||||||
app.use(path.join(API_ROOT_PATH, route), router); |
|
||||||
}); |
|
||||||
|
|
||||||
export default app; |
return app; |
||||||
|
})(); |
||||||
|
@ -1,7 +1,26 @@ |
|||||||
|
import { getgid, getuid, setgid, setuid } from 'process'; |
||||||
|
|
||||||
|
import { PGID, PUID, PORT, ECODE_DROP_PRIVILEGES } from './lib/consts'; |
||||||
|
|
||||||
import app from './app'; |
import app from './app'; |
||||||
|
import { stderr, stdout } from './lib/shell'; |
||||||
|
|
||||||
|
(async () => { |
||||||
|
stdout(`Starting process with ownership ${getuid()}:${getgid()}`); |
||||||
|
|
||||||
|
(await app).listen(PORT, () => { |
||||||
|
try { |
||||||
|
// Group must be set before user to avoid permission error.
|
||||||
|
setgid(PGID); |
||||||
|
setuid(PUID); |
||||||
|
|
||||||
|
stdout(`Process ownership changed to ${getuid()}:${getgid()}.`); |
||||||
|
} catch (error) { |
||||||
|
stderr(`Failed to change process ownership; CAUSE: ${error}`); |
||||||
|
|
||||||
import SERVER_PORT from './lib/consts/SERVER_PORT'; |
process.exit(ECODE_DROP_PRIVILEGES); |
||||||
|
} |
||||||
|
|
||||||
app.listen(SERVER_PORT, () => { |
stdout(`Listening on localhost:${PORT}.`); |
||||||
console.log(`Listening on localhost:${SERVER_PORT}.`); |
}); |
||||||
}); |
})(); |
||||||
|
@ -0,0 +1,55 @@ |
|||||||
|
import { Handler } from 'express'; |
||||||
|
|
||||||
|
import { stdout } from './shell'; |
||||||
|
|
||||||
|
type AssertAuthenticationOptions = { |
||||||
|
fail?: string | ((...args: Parameters<Handler>) => void); |
||||||
|
failReturnTo?: boolean | string; |
||||||
|
succeed?: string | ((...args: Parameters<Handler>) => void); |
||||||
|
}; |
||||||
|
|
||||||
|
type AssertAuthenticationFunction = ( |
||||||
|
options?: AssertAuthenticationOptions, |
||||||
|
) => Handler; |
||||||
|
|
||||||
|
export const assertAuthentication: AssertAuthenticationFunction = ({ |
||||||
|
fail: initFail = (request, response) => response.status(404).send(), |
||||||
|
failReturnTo, |
||||||
|
succeed: initSucceed = (request, response, next) => next(), |
||||||
|
}: AssertAuthenticationOptions = {}) => { |
||||||
|
const fail: (...args: Parameters<Handler>) => void = |
||||||
|
typeof initFail === 'string' |
||||||
|
? (request, response) => response.redirect(initFail) |
||||||
|
: initFail; |
||||||
|
|
||||||
|
const succeed: (...args: Parameters<Handler>) => void = |
||||||
|
typeof initSucceed === 'string' |
||||||
|
? (request, response) => response.redirect(initSucceed) |
||||||
|
: initSucceed; |
||||||
|
|
||||||
|
let getReturnTo: ((...args: Parameters<Handler>) => string) | undefined; |
||||||
|
|
||||||
|
if (failReturnTo === true) { |
||||||
|
getReturnTo = ({ originalUrl, url }) => originalUrl || url; |
||||||
|
} else if (typeof failReturnTo === 'string') { |
||||||
|
getReturnTo = () => failReturnTo; |
||||||
|
} |
||||||
|
|
||||||
|
return (...args) => { |
||||||
|
const { 0: request } = args; |
||||||
|
const { originalUrl, session } = request; |
||||||
|
const { passport } = session; |
||||||
|
|
||||||
|
if (passport?.user) return succeed(...args); |
||||||
|
|
||||||
|
session.returnTo = getReturnTo?.call(null, ...args); |
||||||
|
|
||||||
|
stdout( |
||||||
|
`Unauthenticated access to ${originalUrl}; set return to ${session.returnTo}`, |
||||||
|
); |
||||||
|
|
||||||
|
return fail(...args); |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export const guardApi = assertAuthentication(); |
@ -0,0 +1 @@ |
|||||||
|
export const VNAME_SESSION_SECRET = 'striker-ui-api::session::secret'; |
@ -1,3 +0,0 @@ |
|||||||
const API_ROOT_PATH = '/api'; |
|
||||||
|
|
||||||
export default API_ROOT_PATH; |
|
@ -0,0 +1 @@ |
|||||||
|
export const DELETED = 'DELETED'; |
@ -0,0 +1,2 @@ |
|||||||
|
export const ECODE_DROP_PRIVILEGES = 1; |
||||||
|
export const ECODE_SESSION_SECRET = 2; |
@ -0,0 +1,5 @@ |
|||||||
|
import { resolveGid, resolveUid } from '../shell'; |
||||||
|
|
||||||
|
export const PUID = resolveUid(process.env.PUID ?? 'striker-ui-api'); |
||||||
|
|
||||||
|
export const PGID = resolveGid(process.env.PGID ?? PUID); |
@ -1,23 +1,21 @@ |
|||||||
const hex = '[0-9a-f]'; |
export const P_HEX = '[[:xdigit:]]'; |
||||||
const octet = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])'; |
export const P_OCTET = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])'; |
||||||
const alphanumeric = '[a-z0-9]'; |
export const P_ALPHANUM = '[a-z0-9]'; |
||||||
const alphanumericDash = '[a-z0-9-]'; |
export const P_ALPHANUM_DASH = '[a-z0-9-]'; |
||||||
const ipv4 = `(?:${octet}[.]){3}${octet}`; |
export const P_IPV4 = `(?:${P_OCTET}[.]){3}${P_OCTET}`; |
||||||
|
export const P_UUID = `${P_HEX}{8}-${P_HEX}{4}-[1-5]${P_HEX}{3}-[89ab]${P_HEX}{3}-${P_HEX}{12}`; |
||||||
|
|
||||||
export const REP_DOMAIN = new RegExp( |
export const REP_DOMAIN = new RegExp( |
||||||
`^(?:${alphanumeric}(?:${alphanumericDash}{0,61}${alphanumeric})?[.])+${alphanumeric}${alphanumericDash}{0,61}${alphanumeric}$`, |
`^(?:${P_ALPHANUM}(?:${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM})?[.])+${P_ALPHANUM}${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM}$`, |
||||||
); |
); |
||||||
|
|
||||||
export const REP_INTEGER = /^\d+$/; |
export const REP_INTEGER = /^\d+$/; |
||||||
|
|
||||||
export const REP_IPV4 = new RegExp(`^${ipv4}$`); |
export const REP_IPV4 = new RegExp(`^${P_IPV4}$`); |
||||||
|
|
||||||
export const REP_IPV4_CSV = new RegExp(`(?:${ipv4},)*${ipv4}`); |
export const REP_IPV4_CSV = new RegExp(`(?:${P_IPV4},)*${P_IPV4}`); |
||||||
|
|
||||||
// Peaceful string is temporarily defined as a string without single-quote, double-quote, slash (/), backslash (\\), angle brackets (< >), and curly brackets ({ }).
|
// Peaceful string is temporarily defined as a string without single-quote, double-quote, slash (/), backslash (\\), angle brackets (< >), and curly brackets ({ }).
|
||||||
export const REP_PEACEFUL_STRING = /^[^'"/\\><}{]+$/; |
export const REP_PEACEFUL_STRING = /^[^'"/\\><}{]+$/; |
||||||
|
|
||||||
export const REP_UUID = new RegExp( |
export const REP_UUID = new RegExp(`^${P_UUID}$`); |
||||||
`^${hex}{8}-${hex}{4}-[1-5]${hex}{3}-[89ab]${hex}{3}-${hex}{12}$`, |
|
||||||
'i', |
|
||||||
); |
|
||||||
|
@ -1,3 +1 @@ |
|||||||
const SERVER_PORT = process.env.SERVER_PORT ?? 8080; |
export const PORT = process.env.PORT ?? 8080; |
||||||
|
|
||||||
export default SERVER_PORT; |
|
||||||
|
@ -0,0 +1,10 @@ |
|||||||
|
import SERVER_PATHS from './SERVER_PATHS'; |
||||||
|
|
||||||
|
export { SERVER_PATHS }; |
||||||
|
|
||||||
|
export * from './AN_VARIABLE_NAME_LIST'; |
||||||
|
export * from './DELETED'; |
||||||
|
export * from './EXIT_CODE_LIST'; |
||||||
|
export * from './PROCESS_OWNER'; |
||||||
|
export * from './REG_EXP_PATTERNS'; |
||||||
|
export * from './SERVER_PORT'; |
@ -0,0 +1 @@ |
|||||||
|
export const formatSql = (script: string) => script.replace(/\s+/g, ' '); |
@ -0,0 +1,56 @@ |
|||||||
|
import assert from 'assert'; |
||||||
|
|
||||||
|
import { ECODE_SESSION_SECRET, VNAME_SESSION_SECRET } from './consts'; |
||||||
|
|
||||||
|
import { query, variable } from './accessModule'; |
||||||
|
import { openssl, stderr, stdout } from './shell'; |
||||||
|
|
||||||
|
export const getSessionSecret = async (): Promise<string> => { |
||||||
|
let sessionSecret: string; |
||||||
|
|
||||||
|
try { |
||||||
|
const rows: [sessionSecret: string][] = await query( |
||||||
|
`SELECT variable_value
|
||||||
|
FROM variables |
||||||
|
WHERE variable_name = '${VNAME_SESSION_SECRET}';`,
|
||||||
|
); |
||||||
|
|
||||||
|
assert(rows.length > 0, 'No existing session secret found.'); |
||||||
|
|
||||||
|
({ |
||||||
|
0: [sessionSecret], |
||||||
|
} = rows); |
||||||
|
|
||||||
|
stdout('Found an existing session secret.'); |
||||||
|
|
||||||
|
return sessionSecret; |
||||||
|
} catch (queryError) { |
||||||
|
stderr(`Failed to get session secret from database; CAUSE: ${queryError}`); |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
sessionSecret = openssl('rand', '-base64', '32').trim(); |
||||||
|
|
||||||
|
stdout('Generated a new session secret.'); |
||||||
|
} catch (sysError) { |
||||||
|
stderr(`Failed to generate session secret; CAUSE: ${sysError}`); |
||||||
|
|
||||||
|
process.exit(ECODE_SESSION_SECRET); |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
const vuuid = await variable({ |
||||||
|
file: __filename, |
||||||
|
variable_name: VNAME_SESSION_SECRET, |
||||||
|
variable_value: sessionSecret, |
||||||
|
}); |
||||||
|
|
||||||
|
stdout(`Recorded session secret as variable identified by ${vuuid}.`); |
||||||
|
} catch (subError) { |
||||||
|
stderr(`Failed to record session secret; CAUSE: ${subError}`); |
||||||
|
|
||||||
|
process.exit(ECODE_SESSION_SECRET); |
||||||
|
} |
||||||
|
|
||||||
|
return sessionSecret; |
||||||
|
}; |
@ -0,0 +1,10 @@ |
|||||||
|
export const isObject = (value: unknown) => { |
||||||
|
const result: { is: boolean; obj: object } = { is: false, obj: {} }; |
||||||
|
|
||||||
|
if (typeof value === 'object' && value !== null) { |
||||||
|
result.is = true; |
||||||
|
result.obj = value; |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
}; |
@ -0,0 +1,2 @@ |
|||||||
|
export * from './login'; |
||||||
|
export * from './logout'; |
@ -0,0 +1,18 @@ |
|||||||
|
import { RequestHandler } from 'express'; |
||||||
|
|
||||||
|
import { stdout } from '../../shell'; |
||||||
|
|
||||||
|
export const login: RequestHandler<unknown, unknown, AuthLoginRequestBody> = ( |
||||||
|
request, |
||||||
|
response, |
||||||
|
) => { |
||||||
|
const { user } = request; |
||||||
|
|
||||||
|
if (user) { |
||||||
|
const { name: userName } = user; |
||||||
|
|
||||||
|
stdout(`Successfully authenticated user [${userName}]`); |
||||||
|
} |
||||||
|
|
||||||
|
response.status(204).send(); |
||||||
|
}; |
@ -0,0 +1,17 @@ |
|||||||
|
import { RequestHandler } from 'express'; |
||||||
|
|
||||||
|
import { stdout } from '../../shell'; |
||||||
|
|
||||||
|
export const logout: RequestHandler = (request, response) => { |
||||||
|
request.session.destroy((error) => { |
||||||
|
let scode = 204; |
||||||
|
|
||||||
|
if (error) { |
||||||
|
scode = 500; |
||||||
|
|
||||||
|
stdout(`Failed to destroy session upon logout; CAUSE: ${error}`); |
||||||
|
} |
||||||
|
|
||||||
|
response.status(scode).send(); |
||||||
|
}); |
||||||
|
}; |
@ -1,64 +1,53 @@ |
|||||||
import { Request, Response } from 'express'; |
import { Request, Response } from 'express'; |
||||||
|
|
||||||
import { dbQuery } from '../accessModule'; |
import { query } from '../accessModule'; |
||||||
import call from '../call'; |
import call from '../call'; |
||||||
|
import { stderr, stdout, stdoutVar } from '../shell'; |
||||||
|
|
||||||
const buildGetRequestHandler = |
const buildGetRequestHandler = |
||||||
( |
( |
||||||
query: string | BuildQueryFunction, |
scriptOrCallback: string | BuildQueryFunction, |
||||||
{ beforeRespond }: BuildGetRequestHandlerOptions = {}, |
{ beforeRespond }: BuildGetRequestHandlerOptions = {}, |
||||||
) => |
) => |
||||||
(request: Request, response: Response) => { |
async (request: Request, response: Response) => { |
||||||
console.log('Calling CLI script to get data.'); |
stdout('Calling CLI script to get data.'); |
||||||
|
|
||||||
const buildQueryOptions: BuildQueryOptions = {}; |
const buildQueryOptions: BuildQueryOptions = {}; |
||||||
|
|
||||||
let queryStdout; |
let result: (number | null | string)[][]; |
||||||
|
|
||||||
try { |
try { |
||||||
({ stdout: queryStdout } = dbQuery( |
const sqlscript: string = |
||||||
call<string>(query, { |
typeof scriptOrCallback === 'function' |
||||||
parameters: [request, buildQueryOptions], |
? await scriptOrCallback(request, buildQueryOptions) |
||||||
notCallableReturn: query, |
: scriptOrCallback; |
||||||
}), |
|
||||||
)); |
result = await query(sqlscript); |
||||||
} catch (queryError) { |
} catch (queryError) { |
||||||
console.log(`Failed to execute query; CAUSE: ${queryError}`); |
stderr(`Failed to execute query; CAUSE: ${queryError}`); |
||||||
|
|
||||||
response.status(500).send(); |
response.status(500).send(); |
||||||
|
|
||||||
return; |
return; |
||||||
} |
} |
||||||
|
|
||||||
console.log( |
stdoutVar(result, `Query stdout pre-hooks (type=[${typeof result}]): `); |
||||||
`Query stdout pre-hooks (type=[${typeof queryStdout}]): ${JSON.stringify( |
|
||||||
queryStdout, |
|
||||||
null, |
|
||||||
2, |
|
||||||
)}`,
|
|
||||||
); |
|
||||||
|
|
||||||
const { afterQueryReturn } = buildQueryOptions; |
const { afterQueryReturn } = buildQueryOptions; |
||||||
|
|
||||||
queryStdout = call(afterQueryReturn, { |
result = call(afterQueryReturn, { |
||||||
parameters: [queryStdout], |
parameters: [result], |
||||||
notCallableReturn: queryStdout, |
notCallableReturn: result, |
||||||
}); |
}); |
||||||
|
|
||||||
queryStdout = call(beforeRespond, { |
result = call(beforeRespond, { |
||||||
parameters: [queryStdout], |
parameters: [result], |
||||||
notCallableReturn: queryStdout, |
notCallableReturn: result, |
||||||
}); |
}); |
||||||
|
|
||||||
console.log( |
stdoutVar(result, `Query stdout post-hooks (type=[${typeof result}]): `); |
||||||
`Query stdout post-hooks (type=[${typeof queryStdout}]): ${JSON.stringify( |
|
||||||
queryStdout, |
|
||||||
null, |
|
||||||
2, |
|
||||||
)}`,
|
|
||||||
); |
|
||||||
|
|
||||||
response.json(queryStdout); |
response.json(result); |
||||||
}; |
}; |
||||||
|
|
||||||
export default buildGetRequestHandler; |
export default buildGetRequestHandler; |
||||||
|
@ -0,0 +1,16 @@ |
|||||||
|
import { RequestHandler } from 'express'; |
||||||
|
|
||||||
|
import { anvilSyncShared } from '../../accessModule'; |
||||||
|
import { stdout, stdoutVar } from '../../shell'; |
||||||
|
|
||||||
|
export const createFile: RequestHandler = async ({ file, body }, response) => { |
||||||
|
stdout('Receiving shared file.'); |
||||||
|
|
||||||
|
if (!file) return response.status(400).send(); |
||||||
|
|
||||||
|
stdoutVar({ body, file }); |
||||||
|
|
||||||
|
await anvilSyncShared('move_incoming', `file=${file.path}`, '0132', '0133'); |
||||||
|
|
||||||
|
response.status(201).send(); |
||||||
|
}; |
@ -0,0 +1,29 @@ |
|||||||
|
import { RequestHandler } from 'express'; |
||||||
|
|
||||||
|
import { DELETED } from '../../consts'; |
||||||
|
|
||||||
|
import { anvilSyncShared, query, timestamp, write } from '../../accessModule'; |
||||||
|
|
||||||
|
export const deleteFile: RequestHandler = async (request, response) => { |
||||||
|
const { fileUUID } = request.params; |
||||||
|
|
||||||
|
const [[oldFileType]] = await query( |
||||||
|
`SELECT file_type FROM files WHERE file_uuid = '${fileUUID}';`, |
||||||
|
); |
||||||
|
|
||||||
|
if (oldFileType !== DELETED) { |
||||||
|
await write( |
||||||
|
`UPDATE files
|
||||||
|
SET |
||||||
|
file_type = '${DELETED}', |
||||||
|
modified_date = '${timestamp()}' |
||||||
|
WHERE file_uuid = '${fileUUID}';`,
|
||||||
|
); |
||||||
|
|
||||||
|
await anvilSyncShared('purge', `file_uuid=${fileUUID}`, '0136', '0137', { |
||||||
|
jobHostUUID: 'all', |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
response.status(204).send(); |
||||||
|
}; |
@ -0,0 +1,5 @@ |
|||||||
|
export * from './createFile'; |
||||||
|
export * from './deleteFile'; |
||||||
|
export * from './getFile'; |
||||||
|
export * from './getFileDetail'; |
||||||
|
export * from './updateFile'; |
@ -0,0 +1,134 @@ |
|||||||
|
import { RequestHandler } from 'express'; |
||||||
|
|
||||||
|
import { anvilSyncShared, query, timestamp, write } from '../../accessModule'; |
||||||
|
import { stderr, stdoutVar } from '../../shell'; |
||||||
|
|
||||||
|
export const updateFile: RequestHandler = async (request, response) => { |
||||||
|
const { body = {}, params } = request; |
||||||
|
|
||||||
|
stdoutVar(body, 'Begin edit single file. body='); |
||||||
|
|
||||||
|
const { fileUUID } = params; |
||||||
|
const { fileName, fileLocations, fileType } = body; |
||||||
|
const anvilSyncSharedFunctions = []; |
||||||
|
|
||||||
|
let sqlscript = ''; |
||||||
|
|
||||||
|
if (fileName) { |
||||||
|
const [[oldFileName]] = await query( |
||||||
|
`SELECT file_name FROM files WHERE file_uuid = '${fileUUID}';`, |
||||||
|
); |
||||||
|
|
||||||
|
stdoutVar({ oldFileName, fileName }); |
||||||
|
|
||||||
|
if (fileName !== oldFileName) { |
||||||
|
sqlscript += ` |
||||||
|
UPDATE files |
||||||
|
SET |
||||||
|
file_name = '${fileName}', |
||||||
|
modified_date = '${timestamp()}' |
||||||
|
WHERE file_uuid = '${fileUUID}';`;
|
||||||
|
|
||||||
|
anvilSyncSharedFunctions.push(() => |
||||||
|
anvilSyncShared( |
||||||
|
'rename', |
||||||
|
`file_uuid=${fileUUID}\nold_name=${oldFileName}\nnew_name=${fileName}`, |
||||||
|
'0138', |
||||||
|
'0139', |
||||||
|
{ jobHostUUID: 'all' }, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (fileType) { |
||||||
|
sqlscript += ` |
||||||
|
UPDATE files |
||||||
|
SET |
||||||
|
file_type = '${fileType}', |
||||||
|
modified_date = '${timestamp()}' |
||||||
|
WHERE file_uuid = '${fileUUID}';`;
|
||||||
|
|
||||||
|
anvilSyncSharedFunctions.push(() => |
||||||
|
anvilSyncShared('check_mode', `file_uuid=${fileUUID}`, '0143', '0144', { |
||||||
|
jobHostUUID: 'all', |
||||||
|
}), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if (fileLocations) { |
||||||
|
fileLocations.forEach( |
||||||
|
async ({ |
||||||
|
fileLocationUUID, |
||||||
|
isFileLocationActive, |
||||||
|
}: { |
||||||
|
fileLocationUUID: string; |
||||||
|
isFileLocationActive: boolean; |
||||||
|
}) => { |
||||||
|
let fileLocationActive = 0; |
||||||
|
let jobName = 'purge'; |
||||||
|
let jobTitle = '0136'; |
||||||
|
let jobDescription = '0137'; |
||||||
|
|
||||||
|
if (isFileLocationActive) { |
||||||
|
fileLocationActive = 1; |
||||||
|
jobName = 'pull_file'; |
||||||
|
jobTitle = '0132'; |
||||||
|
jobDescription = '0133'; |
||||||
|
} |
||||||
|
|
||||||
|
sqlscript += ` |
||||||
|
UPDATE file_locations |
||||||
|
SET |
||||||
|
file_location_active = '${fileLocationActive}', |
||||||
|
modified_date = '${timestamp()}' |
||||||
|
WHERE file_location_uuid = '${fileLocationUUID}';`;
|
||||||
|
|
||||||
|
const targetHosts: [ |
||||||
|
n1uuid: string, |
||||||
|
n2uuid: string, |
||||||
|
dr1uuid: null | string, |
||||||
|
][] = await query( |
||||||
|
`SELECT
|
||||||
|
anv.anvil_node1_host_uuid, |
||||||
|
anv.anvil_node2_host_uuid, |
||||||
|
anv.anvil_dr1_host_uuid |
||||||
|
FROM anvils AS anv |
||||||
|
JOIN file_locations AS fil_loc |
||||||
|
ON anv.anvil_uuid = fil_loc.file_location_anvil_uuid |
||||||
|
WHERE fil_loc.file_location_uuid = '${fileLocationUUID}';`,
|
||||||
|
); |
||||||
|
|
||||||
|
targetHosts.flat().forEach((hostUUID: null | string) => { |
||||||
|
if (hostUUID) { |
||||||
|
anvilSyncSharedFunctions.push(() => |
||||||
|
anvilSyncShared( |
||||||
|
jobName, |
||||||
|
`file_uuid=${fileUUID}`, |
||||||
|
jobTitle, |
||||||
|
jobDescription, |
||||||
|
{ jobHostUUID: hostUUID }, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let wcode: number; |
||||||
|
|
||||||
|
try { |
||||||
|
wcode = await write(sqlscript); |
||||||
|
} catch (queryError) { |
||||||
|
stderr(`Failed to execute query; CAUSE: ${queryError}`); |
||||||
|
|
||||||
|
return response.status(500).send(); |
||||||
|
} |
||||||
|
|
||||||
|
anvilSyncSharedFunctions.forEach(async (fn, index) => |
||||||
|
stdoutVar(await fn(), `Anvil sync shared [${index}] output: `), |
||||||
|
); |
||||||
|
|
||||||
|
response.status(200).send(wcode); |
||||||
|
}; |
@ -0,0 +1,59 @@ |
|||||||
|
import assert from 'assert'; |
||||||
|
import { RequestHandler } from 'express'; |
||||||
|
|
||||||
|
import { DELETED, REP_UUID } from '../../consts'; |
||||||
|
|
||||||
|
import { write } from '../../accessModule'; |
||||||
|
import join from '../../join'; |
||||||
|
import { sanitize } from '../../sanitize'; |
||||||
|
import { stderr, stdoutVar } from '../../shell'; |
||||||
|
|
||||||
|
export const deleteUser: RequestHandler< |
||||||
|
DeleteUserParamsDictionary, |
||||||
|
undefined, |
||||||
|
DeleteUserRequestBody |
||||||
|
> = async (request, response) => { |
||||||
|
const { |
||||||
|
body: { uuids: rawUserUuidList } = {}, |
||||||
|
params: { userUuid }, |
||||||
|
} = request; |
||||||
|
|
||||||
|
const userUuidList = sanitize(rawUserUuidList, 'string[]'); |
||||||
|
|
||||||
|
const ulist = userUuidList.length > 0 ? userUuidList : [userUuid]; |
||||||
|
|
||||||
|
stdoutVar({ ulist }); |
||||||
|
|
||||||
|
try { |
||||||
|
let failedIndex = 0; |
||||||
|
|
||||||
|
assert( |
||||||
|
ulist.every((uuid, index) => { |
||||||
|
failedIndex = index; |
||||||
|
|
||||||
|
return REP_UUID.test(uuid); |
||||||
|
}), |
||||||
|
`All UUIDs must be valid UUIDv4; failed at ${failedIndex}, got [${ulist[failedIndex]}]`, |
||||||
|
); |
||||||
|
} catch (assertError) { |
||||||
|
stderr(`Failed to assert value during delete user; CAUSE: ${assertError}`); |
||||||
|
|
||||||
|
return response.status(400).send(); |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
const wcode = await write( |
||||||
|
`UPDATE users
|
||||||
|
SET user_algorithm = '${DELETED}' |
||||||
|
WHERE user_uuid IN (${join(ulist)});`,
|
||||||
|
); |
||||||
|
|
||||||
|
assert(wcode === 0, `Write exited with code ${wcode}`); |
||||||
|
} catch (error) { |
||||||
|
stderr(`Failed to delete user(s); CAUSE: ${error}`); |
||||||
|
|
||||||
|
return response.status(500).send(); |
||||||
|
} |
||||||
|
|
||||||
|
response.status(204).send(); |
||||||
|
}; |
@ -1 +1,2 @@ |
|||||||
|
export * from './deleteUser'; |
||||||
export * from './getUser'; |
export * from './getUser'; |
||||||
|
@ -0,0 +1,44 @@ |
|||||||
|
import { Application, Handler, Router } from 'express'; |
||||||
|
import path from 'path'; |
||||||
|
|
||||||
|
import { stdout } from './shell'; |
||||||
|
|
||||||
|
export const rrouters = < |
||||||
|
A extends Application, |
||||||
|
M extends MapToRouter<R>, |
||||||
|
R extends Router, |
||||||
|
H extends Handler, |
||||||
|
>( |
||||||
|
app: A, |
||||||
|
union: Readonly<M> | R, |
||||||
|
{ |
||||||
|
assign = (router) => [router], |
||||||
|
key, |
||||||
|
route = '/', |
||||||
|
}: { |
||||||
|
assign?: (router: R) => Array<R | H>; |
||||||
|
key?: keyof M; |
||||||
|
route?: string; |
||||||
|
} = {}, |
||||||
|
) => { |
||||||
|
if ('route' in union) { |
||||||
|
const handlers = assign(union as R); |
||||||
|
const { length: hcount } = handlers; |
||||||
|
|
||||||
|
stdout(`Set up route ${route} with ${hcount} handler(s)`); |
||||||
|
|
||||||
|
app.use(route, ...handlers); |
||||||
|
} else if (key) { |
||||||
|
rrouters(app, union[key], { |
||||||
|
assign, |
||||||
|
route: path.posix.join(route, String(key)), |
||||||
|
}); |
||||||
|
} else { |
||||||
|
Object.entries(union).forEach(([extend, subunion]) => { |
||||||
|
rrouters(app, subunion, { |
||||||
|
assign, |
||||||
|
route: path.posix.join(route, extend), |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,28 @@ |
|||||||
|
type NestedObject<T> = { |
||||||
|
[key: number | string]: NestedObject<T> | T; |
||||||
|
}; |
||||||
|
|
||||||
|
export const traverse = <T, O extends NestedObject<V>, V = unknown>( |
||||||
|
obj: O, |
||||||
|
init: T, |
||||||
|
onKey: (previous: T, obj: O, key: string) => { is: boolean; next: O }, |
||||||
|
{ |
||||||
|
onEnd, |
||||||
|
previous = init, |
||||||
|
}: { |
||||||
|
onEnd?: (previous: T, obj: O, key: string) => void; |
||||||
|
previous?: T; |
||||||
|
} = {}, |
||||||
|
) => { |
||||||
|
Object.keys(obj).forEach((key: string) => { |
||||||
|
const { is: proceed, next } = onKey(previous, obj, key); |
||||||
|
|
||||||
|
if (proceed) { |
||||||
|
traverse(next, init, onKey, { previous }); |
||||||
|
} else { |
||||||
|
onEnd?.call(null, previous, obj, key); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return previous; |
||||||
|
}; |
@ -0,0 +1,127 @@ |
|||||||
|
import passport from 'passport'; |
||||||
|
import { Strategy as LocalStrategy } from 'passport-local'; |
||||||
|
|
||||||
|
import { DELETED } from './lib/consts'; |
||||||
|
|
||||||
|
import { query, sub } from './lib/accessModule'; |
||||||
|
import { sanitize } from './lib/sanitize'; |
||||||
|
import { stdout } from './lib/shell'; |
||||||
|
|
||||||
|
passport.use( |
||||||
|
'login', |
||||||
|
new LocalStrategy(async (username, password, done) => { |
||||||
|
stdout(`Attempting passport local strategy "login" for user [${username}]`); |
||||||
|
|
||||||
|
let rows: [ |
||||||
|
userUuid: string, |
||||||
|
userName: string, |
||||||
|
userPasswordHash: string, |
||||||
|
userSalt: string, |
||||||
|
userAlgorithm: string, |
||||||
|
userHashCount: string, |
||||||
|
][]; |
||||||
|
|
||||||
|
try { |
||||||
|
rows = await query( |
||||||
|
`SELECT
|
||||||
|
user_uuid, |
||||||
|
user_name, |
||||||
|
user_password_hash, |
||||||
|
user_salt, |
||||||
|
user_algorithm, |
||||||
|
user_hash_count |
||||||
|
FROM users |
||||||
|
WHERE user_algorithm != 'DELETED' |
||||||
|
AND user_name = '${username}' |
||||||
|
LIMIT 1;`,
|
||||||
|
); |
||||||
|
} catch (queryError) { |
||||||
|
return done(queryError); |
||||||
|
} |
||||||
|
|
||||||
|
if (!rows.length) { |
||||||
|
return done(null, false); |
||||||
|
} |
||||||
|
|
||||||
|
const { |
||||||
|
0: [userUuid, , userPasswordHash, userSalt, userAlgorithm, userHashCount], |
||||||
|
} = rows; |
||||||
|
|
||||||
|
let encryptResult: { |
||||||
|
user_password_hash: string; |
||||||
|
user_salt: string; |
||||||
|
user_hash_count: number; |
||||||
|
user_algorithm: string; |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
[encryptResult] = await sub('encrypt_password', { |
||||||
|
params: [ |
||||||
|
{ |
||||||
|
algorithm: userAlgorithm, |
||||||
|
hash_count: userHashCount, |
||||||
|
password, |
||||||
|
salt: userSalt, |
||||||
|
}, |
||||||
|
], |
||||||
|
pre: ['Account'], |
||||||
|
}); |
||||||
|
} catch (subError) { |
||||||
|
return done(subError); |
||||||
|
} |
||||||
|
|
||||||
|
const { user_password_hash: inputPasswordHash } = encryptResult; |
||||||
|
|
||||||
|
if (inputPasswordHash !== userPasswordHash) { |
||||||
|
return done(null, false); |
||||||
|
} |
||||||
|
|
||||||
|
const user: Express.User = { |
||||||
|
name: username, |
||||||
|
uuid: userUuid, |
||||||
|
}; |
||||||
|
|
||||||
|
return done(null, user); |
||||||
|
}), |
||||||
|
); |
||||||
|
|
||||||
|
passport.serializeUser((user, done) => { |
||||||
|
const { name, uuid } = user; |
||||||
|
|
||||||
|
stdout(`Serialize user [${name}]`); |
||||||
|
|
||||||
|
return done(null, uuid); |
||||||
|
}); |
||||||
|
|
||||||
|
passport.deserializeUser(async (id, done) => { |
||||||
|
const uuid = sanitize(id, 'string', { modifierType: 'sql' }); |
||||||
|
|
||||||
|
stdout(`Deserialize user identified by ${uuid}`); |
||||||
|
|
||||||
|
let rows: [userName: string][]; |
||||||
|
|
||||||
|
try { |
||||||
|
rows = await query( |
||||||
|
`SELECT user_name
|
||||||
|
FROM users |
||||||
|
WHERE user_algorithm != '${DELETED}' |
||||||
|
AND user_uuid = '${uuid}';`,
|
||||||
|
); |
||||||
|
} catch (error) { |
||||||
|
return done(error); |
||||||
|
} |
||||||
|
|
||||||
|
if (!rows.length) { |
||||||
|
return done(null, false); |
||||||
|
} |
||||||
|
|
||||||
|
const { |
||||||
|
0: [userName], |
||||||
|
} = rows; |
||||||
|
|
||||||
|
const user: Express.User = { name: userName, uuid }; |
||||||
|
|
||||||
|
return done(null, user); |
||||||
|
}); |
||||||
|
|
||||||
|
export default passport; |
@ -0,0 +1,13 @@ |
|||||||
|
import express from 'express'; |
||||||
|
|
||||||
|
import { guardApi } from '../lib/assertAuthentication'; |
||||||
|
import { login, logout } from '../lib/request_handlers/auth'; |
||||||
|
import passport from '../passport'; |
||||||
|
|
||||||
|
const router = express.Router(); |
||||||
|
|
||||||
|
router |
||||||
|
.post('/login', passport.authenticate('login'), login) |
||||||
|
.put('/logout', guardApi, logout); |
||||||
|
|
||||||
|
export default router; |
@ -1,202 +1,21 @@ |
|||||||
import express from 'express'; |
import express from 'express'; |
||||||
|
|
||||||
import { |
import { |
||||||
dbJobAnvilSyncShared, |
createFile, |
||||||
dbQuery, |
deleteFile, |
||||||
dbSubRefreshTimestamp, |
getFile, |
||||||
dbWrite, |
getFileDetail, |
||||||
} from '../lib/accessModule'; |
updateFile, |
||||||
import getFile from '../lib/request_handlers/file/getFile'; |
} from '../lib/request_handlers/file'; |
||||||
import getFileDetail from '../lib/request_handlers/file/getFileDetail'; |
|
||||||
import uploadSharedFiles from '../middlewares/uploadSharedFiles'; |
import uploadSharedFiles from '../middlewares/uploadSharedFiles'; |
||||||
|
|
||||||
const router = express.Router(); |
const router = express.Router(); |
||||||
|
|
||||||
router |
router |
||||||
.delete('/:fileUUID', (request, response) => { |
.delete('/:fileUUID', deleteFile) |
||||||
const { fileUUID } = request.params; |
|
||||||
const FILE_TYPE_DELETED = 'DELETED'; |
|
||||||
|
|
||||||
const [[oldFileType]] = dbQuery( |
|
||||||
`SELECT file_type FROM files WHERE file_uuid = '${fileUUID}';`, |
|
||||||
).stdout; |
|
||||||
|
|
||||||
if (oldFileType !== FILE_TYPE_DELETED) { |
|
||||||
dbWrite( |
|
||||||
`UPDATE files
|
|
||||||
SET |
|
||||||
file_type = '${FILE_TYPE_DELETED}', |
|
||||||
modified_date = '${dbSubRefreshTimestamp()}' |
|
||||||
WHERE file_uuid = '${fileUUID}';`,
|
|
||||||
).stdout; |
|
||||||
|
|
||||||
dbJobAnvilSyncShared('purge', `file_uuid=${fileUUID}`, '0136', '0137', { |
|
||||||
jobHostUUID: 'all', |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
response.status(204).send(); |
|
||||||
}) |
|
||||||
.get('/', getFile) |
.get('/', getFile) |
||||||
.get('/:fileUUID', getFileDetail) |
.get('/:fileUUID', getFileDetail) |
||||||
.post('/', uploadSharedFiles.single('file'), ({ file, body }, response) => { |
.post('/', uploadSharedFiles.single('file'), createFile) |
||||||
console.log('Receiving shared file.'); |
.put('/:fileUUID', updateFile); |
||||||
|
|
||||||
if (file) { |
|
||||||
console.log(`file: ${JSON.stringify(file, null, 2)}`); |
|
||||||
console.log(`body: ${JSON.stringify(body, null, 2)}`); |
|
||||||
|
|
||||||
dbJobAnvilSyncShared( |
|
||||||
'move_incoming', |
|
||||||
`file=${file.path}`, |
|
||||||
'0132', |
|
||||||
'0133', |
|
||||||
); |
|
||||||
|
|
||||||
response.status(200).send(); |
|
||||||
} |
|
||||||
}) |
|
||||||
.put('/:fileUUID', (request, response) => { |
|
||||||
console.log('Begin edit single file.'); |
|
||||||
console.dir(request.body); |
|
||||||
|
|
||||||
const { fileUUID } = request.params; |
|
||||||
const { fileName, fileLocations, fileType } = request.body; |
|
||||||
const anvilSyncSharedFunctions = []; |
|
||||||
|
|
||||||
let query = ''; |
|
||||||
|
|
||||||
if (fileName) { |
|
||||||
const [[oldFileName]] = dbQuery( |
|
||||||
`SELECT file_name FROM files WHERE file_uuid = '${fileUUID}';`, |
|
||||||
).stdout; |
|
||||||
console.log(`oldFileName=[${oldFileName}],newFileName=[${fileName}]`); |
|
||||||
|
|
||||||
if (fileName !== oldFileName) { |
|
||||||
query += ` |
|
||||||
UPDATE files |
|
||||||
SET |
|
||||||
file_name = '${fileName}', |
|
||||||
modified_date = '${dbSubRefreshTimestamp()}' |
|
||||||
WHERE file_uuid = '${fileUUID}';`;
|
|
||||||
|
|
||||||
anvilSyncSharedFunctions.push(() => |
|
||||||
dbJobAnvilSyncShared( |
|
||||||
'rename', |
|
||||||
`file_uuid=${fileUUID}\nold_name=${oldFileName}\nnew_name=${fileName}`, |
|
||||||
'0138', |
|
||||||
'0139', |
|
||||||
{ jobHostUUID: 'all' }, |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (fileType) { |
|
||||||
query += ` |
|
||||||
UPDATE files |
|
||||||
SET |
|
||||||
file_type = '${fileType}', |
|
||||||
modified_date = '${dbSubRefreshTimestamp()}' |
|
||||||
WHERE file_uuid = '${fileUUID}';`;
|
|
||||||
|
|
||||||
anvilSyncSharedFunctions.push(() => |
|
||||||
dbJobAnvilSyncShared( |
|
||||||
'check_mode', |
|
||||||
`file_uuid=${fileUUID}`, |
|
||||||
'0143', |
|
||||||
'0144', |
|
||||||
{ jobHostUUID: 'all' }, |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
if (fileLocations) { |
|
||||||
fileLocations.forEach( |
|
||||||
({ |
|
||||||
fileLocationUUID, |
|
||||||
isFileLocationActive, |
|
||||||
}: { |
|
||||||
fileLocationUUID: string; |
|
||||||
isFileLocationActive: boolean; |
|
||||||
}) => { |
|
||||||
let fileLocationActive = 0; |
|
||||||
let jobName = 'purge'; |
|
||||||
let jobTitle = '0136'; |
|
||||||
let jobDescription = '0137'; |
|
||||||
|
|
||||||
if (isFileLocationActive) { |
|
||||||
fileLocationActive = 1; |
|
||||||
jobName = 'pull_file'; |
|
||||||
jobTitle = '0132'; |
|
||||||
jobDescription = '0133'; |
|
||||||
} |
|
||||||
|
|
||||||
query += ` |
|
||||||
UPDATE file_locations |
|
||||||
SET |
|
||||||
file_location_active = '${fileLocationActive}', |
|
||||||
modified_date = '${dbSubRefreshTimestamp()}' |
|
||||||
WHERE file_location_uuid = '${fileLocationUUID}';`;
|
|
||||||
|
|
||||||
const targetHosts = dbQuery( |
|
||||||
`SELECT
|
|
||||||
anv.anvil_node1_host_uuid, |
|
||||||
anv.anvil_node2_host_uuid, |
|
||||||
anv.anvil_dr1_host_uuid |
|
||||||
FROM anvils AS anv |
|
||||||
JOIN file_locations AS fil_loc |
|
||||||
ON anv.anvil_uuid = fil_loc.file_location_anvil_uuid |
|
||||||
WHERE fil_loc.file_location_uuid = '${fileLocationUUID}';`,
|
|
||||||
).stdout; |
|
||||||
|
|
||||||
targetHosts.flat().forEach((hostUUID: string) => { |
|
||||||
if (hostUUID) { |
|
||||||
anvilSyncSharedFunctions.push(() => |
|
||||||
dbJobAnvilSyncShared( |
|
||||||
jobName, |
|
||||||
`file_uuid=${fileUUID}`, |
|
||||||
jobTitle, |
|
||||||
jobDescription, |
|
||||||
{ jobHostUUID: hostUUID }, |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
}); |
|
||||||
}, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
console.log(`Query (type=[${typeof query}]): [${query}]`); |
|
||||||
|
|
||||||
let queryStdout; |
|
||||||
|
|
||||||
try { |
|
||||||
({ stdout: queryStdout } = dbWrite(query)); |
|
||||||
} catch (queryError) { |
|
||||||
console.log(`Failed to execute query; CAUSE: ${queryError}`); |
|
||||||
|
|
||||||
response.status(500).send(); |
|
||||||
} |
|
||||||
|
|
||||||
console.log( |
|
||||||
`Query stdout (type=[${typeof queryStdout}]): ${JSON.stringify( |
|
||||||
queryStdout, |
|
||||||
null, |
|
||||||
2, |
|
||||||
)}`,
|
|
||||||
); |
|
||||||
anvilSyncSharedFunctions.forEach((fn, index) => { |
|
||||||
console.log( |
|
||||||
`Anvil sync shared [${index}] output: [${JSON.stringify( |
|
||||||
fn(), |
|
||||||
null, |
|
||||||
2, |
|
||||||
)}]`,
|
|
||||||
); |
|
||||||
}); |
|
||||||
|
|
||||||
response.status(200).send(queryStdout); |
|
||||||
}); |
|
||||||
|
|
||||||
export default router; |
export default router; |
||||||
|
@ -0,0 +1,51 @@ |
|||||||
|
import express from 'express'; |
||||||
|
import { existsSync } from 'fs'; |
||||||
|
import path from 'path'; |
||||||
|
|
||||||
|
import { SERVER_PATHS } from '../lib/consts'; |
||||||
|
|
||||||
|
import { assertAuthentication } from '../lib/assertAuthentication'; |
||||||
|
import { stdout } from '../lib/shell'; |
||||||
|
|
||||||
|
const router = express.Router(); |
||||||
|
|
||||||
|
const htmlDir = SERVER_PATHS.var.www.html.self; |
||||||
|
|
||||||
|
router.use( |
||||||
|
(...args) => { |
||||||
|
const { 0: request, 2: next } = args; |
||||||
|
const { originalUrl } = request; |
||||||
|
|
||||||
|
if (/^[/]login/.test(originalUrl)) { |
||||||
|
stdout(`Static:login requested`); |
||||||
|
|
||||||
|
return assertAuthentication({ fail: (rq, rs, nx) => nx(), succeed: '/' })( |
||||||
|
...args, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const parts = originalUrl.replace(/[/]$/, '').split('/'); |
||||||
|
const tail = parts.pop() || 'index'; |
||||||
|
const extended = /[.]html$/.test(tail) ? tail : `${tail}.html`; |
||||||
|
|
||||||
|
parts.push(extended); |
||||||
|
|
||||||
|
const htmlPath = path.posix.join(htmlDir, ...parts); |
||||||
|
const isHtmlExists = existsSync(htmlPath); |
||||||
|
|
||||||
|
if (isHtmlExists) { |
||||||
|
stdout(`Static:[${htmlPath}] requested`); |
||||||
|
|
||||||
|
return assertAuthentication({ fail: '/login', failReturnTo: true })( |
||||||
|
...args, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return next(); |
||||||
|
}, |
||||||
|
express.static(htmlDir, { |
||||||
|
extensions: ['htm', 'html'], |
||||||
|
}), |
||||||
|
); |
||||||
|
|
||||||
|
export default router; |
@ -1,9 +1,12 @@ |
|||||||
import express from 'express'; |
import express from 'express'; |
||||||
|
|
||||||
import { getUser } from '../lib/request_handlers/user'; |
import { deleteUser, getUser } from '../lib/request_handlers/user'; |
||||||
|
|
||||||
const router = express.Router(); |
const router = express.Router(); |
||||||
|
|
||||||
router.get('/', getUser); |
router |
||||||
|
.get('/', getUser) |
||||||
|
.delete('/', deleteUser) |
||||||
|
.delete('/:userUuid', deleteUser); |
||||||
|
|
||||||
export default router; |
export default router; |
||||||
|
@ -0,0 +1,201 @@ |
|||||||
|
import assert from 'assert'; |
||||||
|
import expressSession, { |
||||||
|
SessionData, |
||||||
|
Store as BaseSessionStore, |
||||||
|
} from 'express-session'; |
||||||
|
|
||||||
|
import { DELETED } from './lib/consts'; |
||||||
|
|
||||||
|
import { getLocalHostUUID, query, timestamp, write } from './lib/accessModule'; |
||||||
|
import { getSessionSecret } from './lib/getSessionSecret'; |
||||||
|
import { stderr, stdout, stdoutVar, uuid } from './lib/shell'; |
||||||
|
|
||||||
|
const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 28800000; // 8 hours
|
||||||
|
|
||||||
|
export class SessionStore extends BaseSessionStore { |
||||||
|
constructor(options = {}) { |
||||||
|
super(options); |
||||||
|
} |
||||||
|
|
||||||
|
public async destroy( |
||||||
|
sid: string, |
||||||
|
done?: ((err?: unknown) => void) | undefined, |
||||||
|
): Promise<void> { |
||||||
|
stdout(`Destroy session ${sid}`); |
||||||
|
|
||||||
|
try { |
||||||
|
const wcode = await write( |
||||||
|
`UPDATE sessions
|
||||||
|
SET session_salt = '${DELETED}', modified_date = '${timestamp()}' |
||||||
|
WHERE session_uuid = '${sid}';`,
|
||||||
|
); |
||||||
|
|
||||||
|
assert(wcode === 0, `Write exited with code ${wcode}`); |
||||||
|
} catch (error) { |
||||||
|
stderr( |
||||||
|
`Failed to complete DB write in destroy session ${sid}; CAUSE: ${error}`, |
||||||
|
); |
||||||
|
|
||||||
|
return done?.call(null, error); |
||||||
|
} |
||||||
|
|
||||||
|
return done?.call(null); |
||||||
|
} |
||||||
|
|
||||||
|
public async get( |
||||||
|
sid: string, |
||||||
|
done: (err: unknown, session?: SessionData | null | undefined) => void, |
||||||
|
): Promise<void> { |
||||||
|
stdout(`Get session ${sid}`); |
||||||
|
|
||||||
|
let rows: [ |
||||||
|
sessionUuid: string, |
||||||
|
userUuid: string, |
||||||
|
sessionModifiedDate: string, |
||||||
|
][]; |
||||||
|
|
||||||
|
try { |
||||||
|
rows = await query( |
||||||
|
`SELECT
|
||||||
|
s.session_uuid, |
||||||
|
u.user_uuid, |
||||||
|
s.modified_date |
||||||
|
FROM sessions AS s |
||||||
|
JOIN users AS u |
||||||
|
ON s.session_user_uuid = u.user_uuid |
||||||
|
WHERE s.session_salt != '${DELETED}' |
||||||
|
AND s.session_uuid = '${sid}';`,
|
||||||
|
); |
||||||
|
} catch (queryError) { |
||||||
|
return done(queryError); |
||||||
|
} |
||||||
|
|
||||||
|
if (!rows.length) { |
||||||
|
return done(null); |
||||||
|
} |
||||||
|
|
||||||
|
const { |
||||||
|
0: [, userUuid, sessionModifiedDate], |
||||||
|
} = rows; |
||||||
|
|
||||||
|
const cookieMaxAge = |
||||||
|
SessionStore.calculateCookieMaxAge(sessionModifiedDate); |
||||||
|
|
||||||
|
const data: SessionData = { |
||||||
|
cookie: { |
||||||
|
maxAge: cookieMaxAge, |
||||||
|
originalMaxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE, |
||||||
|
}, |
||||||
|
passport: { user: userUuid }, |
||||||
|
}; |
||||||
|
|
||||||
|
return done(null, data); |
||||||
|
} |
||||||
|
|
||||||
|
public async set( |
||||||
|
sid: string, |
||||||
|
session: SessionData, |
||||||
|
done?: ((err?: unknown) => void) | undefined, |
||||||
|
): Promise<void> { |
||||||
|
stdoutVar({ session }, `Set session ${sid}`); |
||||||
|
|
||||||
|
const { passport: { user: userUuid } = {} } = session; |
||||||
|
|
||||||
|
try { |
||||||
|
assert.ok(userUuid, 'Missing user identifier'); |
||||||
|
|
||||||
|
const localHostUuid = getLocalHostUUID(); |
||||||
|
const modifiedDate = timestamp(); |
||||||
|
|
||||||
|
const wcode = await write( |
||||||
|
`INSERT INTO
|
||||||
|
sessions ( |
||||||
|
session_uuid, |
||||||
|
session_host_uuid, |
||||||
|
session_user_uuid, |
||||||
|
session_salt, |
||||||
|
modified_date |
||||||
|
) |
||||||
|
VALUES |
||||||
|
( |
||||||
|
'${sid}', |
||||||
|
'${localHostUuid}', |
||||||
|
'${userUuid}', |
||||||
|
'', |
||||||
|
'${modifiedDate}' |
||||||
|
) |
||||||
|
ON CONFLICT (session_uuid) |
||||||
|
DO UPDATE SET modified_date = '${modifiedDate}';`,
|
||||||
|
); |
||||||
|
|
||||||
|
assert(wcode === 0, `Write exited with code ${wcode}`); |
||||||
|
} catch (error) { |
||||||
|
stderr( |
||||||
|
`Failed to complete DB write in set session ${sid}; CAUSE: ${error}`, |
||||||
|
); |
||||||
|
|
||||||
|
return done?.call(null, error); |
||||||
|
} |
||||||
|
|
||||||
|
return done?.call(null); |
||||||
|
} |
||||||
|
|
||||||
|
public async touch( |
||||||
|
sid: string, |
||||||
|
session: SessionData, |
||||||
|
done?: ((err?: unknown) => void) | undefined, |
||||||
|
): Promise<void> { |
||||||
|
stdoutVar({ session }, `Touch session ${sid}`); |
||||||
|
|
||||||
|
try { |
||||||
|
const wcode = await write( |
||||||
|
`UPDATE sessions
|
||||||
|
SET modified_date = '${timestamp()}' |
||||||
|
WHERE session_uuid = '${sid}';`,
|
||||||
|
); |
||||||
|
|
||||||
|
assert(wcode === 0, `Write exited with code ${wcode}`); |
||||||
|
} catch (error) { |
||||||
|
stderr( |
||||||
|
`Failed to complete DB write in touch session ${sid}; CAUSE: ${error}`, |
||||||
|
); |
||||||
|
|
||||||
|
return done?.call(null, error); |
||||||
|
} |
||||||
|
|
||||||
|
return done?.call(null); |
||||||
|
} |
||||||
|
|
||||||
|
public static calculateCookieMaxAge( |
||||||
|
sessionModifiedDate: string, |
||||||
|
cookieOriginalMaxAge: number = DEFAULT_COOKIE_ORIGINAL_MAX_AGE, |
||||||
|
) { |
||||||
|
const sessionModifiedEpoch = Date.parse(sessionModifiedDate); |
||||||
|
const sessionDeadlineEpoch = sessionModifiedEpoch + cookieOriginalMaxAge; |
||||||
|
const cookieMaxAge = sessionDeadlineEpoch - Date.now(); |
||||||
|
|
||||||
|
stdoutVar({ sessionModifiedDate, sessionDeadlineEpoch, cookieMaxAge }); |
||||||
|
|
||||||
|
return cookieMaxAge; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default (async () => |
||||||
|
expressSession({ |
||||||
|
cookie: { |
||||||
|
httpOnly: true, |
||||||
|
maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE, |
||||||
|
secure: false, |
||||||
|
}, |
||||||
|
genid: ({ path }) => { |
||||||
|
const sid = uuid(); |
||||||
|
|
||||||
|
stdout(`Generated session identifier ${sid}; request.path=${path}`); |
||||||
|
|
||||||
|
return sid; |
||||||
|
}, |
||||||
|
resave: false, |
||||||
|
saveUninitialized: false, |
||||||
|
secret: await getSessionSecret(), |
||||||
|
store: new SessionStore(), |
||||||
|
}))(); |
@ -0,0 +1,12 @@ |
|||||||
|
type AccessStartOptions = { |
||||||
|
args?: readonly string[]; |
||||||
|
} & import('child_process').SpawnOptions; |
||||||
|
|
||||||
|
type SubroutineCommonParams = { |
||||||
|
debug?: number; |
||||||
|
}; |
||||||
|
|
||||||
|
type InsertOrUpdateFunctionCommonParams = SubroutineCommonParams & { |
||||||
|
file: string; |
||||||
|
line?: number; |
||||||
|
}; |
@ -1,8 +0,0 @@ |
|||||||
type AnvilOverview = { |
|
||||||
anvilName: string; |
|
||||||
anvilUUID: string; |
|
||||||
hosts: Array<{ |
|
||||||
hostName: string; |
|
||||||
hostUUID: string; |
|
||||||
}>; |
|
||||||
}; |
|
@ -0,0 +1,3 @@ |
|||||||
|
type JobAnvilSyncSharedOptions = { |
||||||
|
jobHostUUID?: string; |
||||||
|
}; |
@ -0,0 +1,4 @@ |
|||||||
|
type AuthLoginRequestBody = { |
||||||
|
username: string; |
||||||
|
password: string; |
||||||
|
}; |
@ -0,0 +1,15 @@ |
|||||||
|
type GetHostSshRequestBody = { |
||||||
|
password: string; |
||||||
|
port?: number; |
||||||
|
ipAddress: string; |
||||||
|
}; |
||||||
|
|
||||||
|
type GetHostSshResponseBody = { |
||||||
|
badSSHKeys?: DeleteSshKeyConflictRequestBody; |
||||||
|
hostName: string; |
||||||
|
hostOS: string; |
||||||
|
hostUUID: string; |
||||||
|
isConnected: boolean; |
||||||
|
isInetConnected: boolean; |
||||||
|
isOSRegistered: boolean; |
||||||
|
}; |
@ -0,0 +1,89 @@ |
|||||||
|
type CreateHostConnectionRequestBody = { |
||||||
|
dbName?: string; |
||||||
|
ipAddress: string; |
||||||
|
isPing?: boolean; |
||||||
|
/** Host password; same as database password */ |
||||||
|
password: string; |
||||||
|
port?: number; |
||||||
|
sshPort?: number; |
||||||
|
/** Database user */ |
||||||
|
user?: string; |
||||||
|
}; |
||||||
|
|
||||||
|
type DeleteHostConnectionRequestBody = { |
||||||
|
[hostUUID: string]: string[]; |
||||||
|
}; |
||||||
|
|
||||||
|
type HostConnectionOverview = { |
||||||
|
inbound: { |
||||||
|
ipAddress: { |
||||||
|
[ipAddress: string]: { |
||||||
|
hostUUID: string; |
||||||
|
ipAddress: string; |
||||||
|
ipAddressUUID: string; |
||||||
|
networkLinkNumber: number; |
||||||
|
networkNumber: number; |
||||||
|
networkType: string; |
||||||
|
}; |
||||||
|
}; |
||||||
|
port: number; |
||||||
|
user: string; |
||||||
|
}; |
||||||
|
peer: { |
||||||
|
[ipAddress: string]: { |
||||||
|
hostUUID: string; |
||||||
|
ipAddress: string; |
||||||
|
isPing: boolean; |
||||||
|
port: number; |
||||||
|
user: string; |
||||||
|
}; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
type HostOverview = { |
||||||
|
hostName: string; |
||||||
|
hostType: string; |
||||||
|
hostUUID: string; |
||||||
|
shortHostName: string; |
||||||
|
}; |
||||||
|
|
||||||
|
type InitializeStrikerNetworkForm = { |
||||||
|
interfaces: Array<NetworkInterfaceOverview | null | undefined>; |
||||||
|
ipAddress: string; |
||||||
|
name: string; |
||||||
|
subnetMask: string; |
||||||
|
type: string; |
||||||
|
}; |
||||||
|
|
||||||
|
type InitializeStrikerForm = { |
||||||
|
adminPassword: string; |
||||||
|
domainName: string; |
||||||
|
hostName: string; |
||||||
|
hostNumber: number; |
||||||
|
networkDNS: string; |
||||||
|
networkGateway: string; |
||||||
|
networks: InitializeStrikerNetworkForm[]; |
||||||
|
organizationName: string; |
||||||
|
organizationPrefix: string; |
||||||
|
}; |
||||||
|
|
||||||
|
type PrepareHostRequestBody = { |
||||||
|
enterpriseUUID?: string; |
||||||
|
hostIPAddress: string; |
||||||
|
hostName: string; |
||||||
|
hostPassword: string; |
||||||
|
hostSSHPort?: number; |
||||||
|
hostType: string; |
||||||
|
hostUser?: string; |
||||||
|
hostUUID?: string; |
||||||
|
redhatPassword: string; |
||||||
|
redhatUser: string; |
||||||
|
}; |
||||||
|
|
||||||
|
type SetHostInstallTargetRequestBody = { |
||||||
|
isEnableInstallTarget: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
type UpdateHostParams = { |
||||||
|
hostUUID: string; |
||||||
|
}; |
@ -0,0 +1,12 @@ |
|||||||
|
type SshKeyConflict = { |
||||||
|
[stateUUID: string]: { |
||||||
|
badFile: string; |
||||||
|
badLine: number; |
||||||
|
hostName: string; |
||||||
|
hostUUID: string; |
||||||
|
ipAddress: string; |
||||||
|
stateUUID: string; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
type DeleteSshKeyConflictRequestBody = { [hostUUID: string]: string[] }; |
@ -1,11 +1,11 @@ |
|||||||
type UPSOverview = { |
type UpsOverview = { |
||||||
upsAgent: string; |
upsAgent: string; |
||||||
upsIPAddress: string; |
upsIPAddress: string; |
||||||
upsName: string; |
upsName: string; |
||||||
upsUUID: string; |
upsUUID: string; |
||||||
}; |
}; |
||||||
|
|
||||||
type UPSTemplate = { |
type UpsTemplate = { |
||||||
[upsName: string]: AnvilDataUPSHash[string] & { |
[upsName: string]: AnvilDataUPSHash[string] & { |
||||||
links: { |
links: { |
||||||
[linkId: string]: { |
[linkId: string]: { |
@ -0,0 +1,7 @@ |
|||||||
|
type DeleteUserParamsDictionary = { |
||||||
|
userUuid: string; |
||||||
|
}; |
||||||
|
|
||||||
|
type DeleteUserRequestBody = { |
||||||
|
uuids?: string[]; |
||||||
|
}; |
@ -1,11 +0,0 @@ |
|||||||
type CreateHostConnectionRequestBody = { |
|
||||||
dbName?: string; |
|
||||||
ipAddress: string; |
|
||||||
isPing?: boolean; |
|
||||||
// Host password; same as database password.
|
|
||||||
password: string; |
|
||||||
port?: number; |
|
||||||
sshPort?: number; |
|
||||||
// database user.
|
|
||||||
user?: string; |
|
||||||
}; |
|
@ -1,9 +0,0 @@ |
|||||||
type DBInsertOrUpdateFunctionCommonParams = ModuleSubroutineCommonParams & { |
|
||||||
file: string; |
|
||||||
line?: number; |
|
||||||
}; |
|
||||||
|
|
||||||
type DBInsertOrUpdateFunctionCommonOptions = Omit< |
|
||||||
ExecModuleSubroutineOptions, |
|
||||||
'subParams' | 'subModuleName' |
|
||||||
>; |
|
@ -1,3 +0,0 @@ |
|||||||
type DBJobAnvilSyncSharedOptions = { |
|
||||||
jobHostUUID?: string; |
|
||||||
}; |
|
@ -1,3 +0,0 @@ |
|||||||
type DeleteHostConnectionRequestBody = { |
|
||||||
[hostUUID: string]: string[]; |
|
||||||
}; |
|
@ -1 +0,0 @@ |
|||||||
type DeleteSSHKeyConflictRequestBody = { [hostUUID: string]: string[] }; |
|
@ -1,5 +0,0 @@ |
|||||||
type ExecModuleSubroutineOptions = { |
|
||||||
spawnSyncOptions?: import('child_process').SpawnSyncOptions; |
|
||||||
subModuleName?: string; |
|
||||||
subParams?: Record<string, unknown>; |
|
||||||
}; |
|
@ -1,25 +0,0 @@ |
|||||||
type HostConnectionOverview = { |
|
||||||
inbound: { |
|
||||||
ipAddress: { |
|
||||||
[ipAddress: string]: { |
|
||||||
hostUUID: string; |
|
||||||
ipAddress: string; |
|
||||||
ipAddressUUID: string; |
|
||||||
networkLinkNumber: number; |
|
||||||
networkNumber: number; |
|
||||||
networkType: string; |
|
||||||
}; |
|
||||||
}; |
|
||||||
port: number; |
|
||||||
user: string; |
|
||||||
}; |
|
||||||
peer: { |
|
||||||
[ipAddress: string]: { |
|
||||||
hostUUID: string; |
|
||||||
ipAddress: string; |
|
||||||
isPing: boolean; |
|
||||||
port: number; |
|
||||||
user: string; |
|
||||||
}; |
|
||||||
}; |
|
||||||
}; |
|
@ -1,6 +0,0 @@ |
|||||||
type HostOverview = { |
|
||||||
hostName: string; |
|
||||||
hostType: string; |
|
||||||
hostUUID: string; |
|
||||||
shortHostName: string; |
|
||||||
}; |
|
@ -1,19 +0,0 @@ |
|||||||
type InitializeStrikerNetworkForm = { |
|
||||||
interfaces: Array<NetworkInterfaceOverview | null | undefined>; |
|
||||||
ipAddress: string; |
|
||||||
name: string; |
|
||||||
subnetMask: string; |
|
||||||
type: string; |
|
||||||
}; |
|
||||||
|
|
||||||
type InitializeStrikerForm = { |
|
||||||
adminPassword: string; |
|
||||||
domainName: string; |
|
||||||
hostName: string; |
|
||||||
hostNumber: number; |
|
||||||
networkDNS: string; |
|
||||||
networkGateway: string; |
|
||||||
networks: InitializeStrikerNetworkForm[]; |
|
||||||
organizationName: string; |
|
||||||
organizationPrefix: string; |
|
||||||
}; |
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue