@universis/docutracks
Version:
Implementation of document numbering services hosted by docutracks
292 lines (275 loc) • 11.1 kB
text/typescript
import { Args, DataError, HttpError, HttpNotAcceptableError, IApplication, TraceUtils } from '@themost/common';
import { DataContext } from '@themost/data';
import {DefaultDocumentNumberService, DocumentNumberSeries, DocumentNumberSeriesItem} from '@universis/docnumbers';
import fetch, { Headers, Response } from 'node-fetch';
import { OrganizationGroup } from './DocutracksNumberService.interfaces';
import { DepartmentDocumentNumberSeriesReplacer } from './DocumentNumberSeriesReplacer';
import { DocumentReferenceKind, DocumentReference, DocumentReferenceType, ObjectReference } from './DocutracksNumberService.interfaces';
export declare interface DocutracksConfiguration {
server: string;
user: string;
password: string;
}
declare interface LoginResponse {
Success: boolean;
CookieName?: string;
}
declare interface RegisterDocumentRequest {
Document: DocumentReference
}
declare interface RegisterDocumentResponse {
Success: boolean;
DocumentReference?: number;
DocumentInfo?: {
ProtocolText: string;
ProtocolDate: string;
DocumentCopy: string;
};
ErrorInfo?: {
ErrorCode: number;
ErrorMessage: string;
ExceptionMessage?: string;
ExceptionStackTrace?: string;
}
}
declare interface UserContext {
user?: {
name: string;
authenticationType?: string;
authenticationProvider?: any;
}
}
declare interface GetUserResponse {
User: {
Id: number;
UserName: string;
FirstName: string;
LastName: string;
DisplayName: string;
Email: string;
IsAdministrator: boolean;
HasDigitalSignatureToken: boolean;
IsContentEditor: boolean;
ContainerId: number;
ReceivesUserNotifications: boolean;
ReceivesAssignNotifications: boolean;
ReceivesCPNotifications: boolean;
}
}
export class LoginError extends Error {
constructor() {
super('Failed to authenticate process against current document number service');
}
}
export class InvalidIdentifierError extends Error {
constructor() {
super('The specified document series has an invalid identifier');
}
}
function assignHeaders(source: Headers, addHeaders: Headers): Headers {
if (addHeaders) {
addHeaders.forEach((value: string, name: string) => {
source.append(name, value);
});
}
return source;
}
export class DocutracksNumberService extends DefaultDocumentNumberService {
public options: DocutracksConfiguration;
private headers: Headers;
constructor(app: IApplication) {
super(app);
this.options = app.getConfiguration().getSourceAt('settings/universis/docutracks');
// apply changes to document series model
new DepartmentDocumentNumberSeriesReplacer(app).apply();
}
/**
* Validates the content type of the given response
* @param {Response} response
*/
private validateContentType(response: Response) {
// get content type
const contentType = response.headers.get('Content-Type');
if (/^application\/json;/g.test(contentType) === false) {
throw new HttpNotAcceptableError(`Document service response is invalid. Expected a valid json but got ${contentType}`);
}
}
protected async login(): Promise<any> {
const loginResponse = await fetch(new URL('/services/authentication/login', this.options.server).toString(), {
method: 'POST',
body: JSON.stringify({
UserName: this.options.user,
Password: this.options.password
}),
headers: new Headers({
'Content-Type': 'application/json'
})
});
if (loginResponse.ok) {
// validate response
this.validateContentType(loginResponse);
// get body
const body: LoginResponse | any = await loginResponse.json();
if (body == null) {
throw new Error('The response returned by document service is invalid.');
}
if (body && body.Success === false) {
throw new LoginError();
}
// return cookie
const setCookie = loginResponse.headers.get('Set-Cookie');
return setCookie.split(',').map((v: string) => v.trim()).find((v: string) => {
return /^\.ASPXAUTH=/.test(v);
});
} else {
throw new LoginError();
}
}
private getGroupFrom(alternateName: string): number {
const matches = /^\/Groups\/(\d+)$/ig.exec(alternateName);
if (matches == null) {
throw new InvalidIdentifierError();
}
return parseInt(matches[1], 10);
}
async next(context: DataContext, documentSeries: DocumentNumberSeries, extraAttributes?: any): Promise<any> {
Args.check(documentSeries != null, 'Document series cannot be empty at this context.');
// get document series alternate name
const alternateName = await context.model('DocumentNumberSeries').where('id').equal(documentSeries.id)
.select('alternateName').silent().value();
if (alternateName == null) {
throw new DataError('E_ATTRIBUTE',
'The specified document series does not have an alternate name or it\'s not accessible.', null, 'DocumentNumberSeries', 'alternateName');
}
// try to login
const authenticationCookie = await this.login();
// the alternate name should have a format like /Groups/104
// get group identifier from the given alternate name
const value = this.getGroupFrom(alternateName);
// set group reference
const group: ObjectReference = {
Id: value
}
const title = extraAttributes && extraAttributes.description;
// create request payload
const documentRequest: RegisterDocumentRequest = {
Document: {
Title: title,
Attachments: [],
Kind: DocumentReferenceKind.Default,
Type: DocumentReferenceType.Outgoing,
CreatedByGroup: group,
CreatedForGroup: group,
DocumentCopies: [
{
CreatedByGroup: group,
OwnedByGroup: group
}
]
}
}
const userContext = context as UserContext;
if (userContext.user && userContext.user.name) {
// try to get user
const findUser = await this.getUser(userContext.user.name);
if (findUser.User != null) {
documentRequest.Document.CreatedBy = findUser.User;
}
}
const headers1 = assignHeaders(new Headers({
'Content-Type': 'application/json',
'Cookie': authenticationCookie
}), this.headers);
// register document
const response1 = await fetch(new URL('/services/document/register', this.options.server).toString(), {
method: 'POST',
body: JSON.stringify(documentRequest),
headers: headers1
});
// if response is ok
if (response1.ok) {
this.validateContentType(response1);
// get response
const documentResponse: RegisterDocumentResponse | any = await response1.json();
// if the operation has been failed
if (documentResponse.Success === false) {
TraceUtils.error('DocutracksNumberService', documentRequest, documentResponse);
// throw error
throw new DataError('E_DOC_NUMBER_ERROR', documentResponse.ErrorInfo.ExceptionMessage || documentResponse.ErrorInfo.ErrorMessage, null);
}
// otherwise return document number
return documentResponse.DocumentInfo.ProtocolText;
}
}
async getUser(name: string): Promise<GetUserResponse> {
// try to login
const authenticationCookie = await this.login();
const headers = assignHeaders(new Headers({
'Content-Type': 'application/json',
'Cookie': authenticationCookie
}), this.headers);
const response = await fetch(new URL('/services/user/get/byusername', this.options.server).toString(), {
method: 'POST',
body: JSON.stringify({
Username: name
}),
// tslint:disable-next-line: object-literal-shorthand
headers: headers
});
if (response.ok) {
this.validateContentType(response);
const res: GetUserResponse = await response.json();
return res;
}
throw new HttpError(response.status, 'An error occurred while getting user', response.statusText);
}
async getGroups(): Promise<any> {
// try to login
const authenticationCookie = await this.login();
// register document
const headers = assignHeaders(new Headers({
'Content-Type': 'application/json',
'Cookie': authenticationCookie
}), this.headers);
const response = await fetch(new URL('/services/organization/fullUsersTree', this.options.server).toString(), {
method: 'GET',
// tslint:disable-next-line: object-literal-shorthand
headers: headers
});
if (response.ok) {
this.validateContentType(response);
const body = await response.json();
const results = [];
if (Array.isArray(body)) {
function extractGroups(group: OrganizationGroup, parentPath: string): any[] {
const res = [];
const addGroup = {
id: group.Id,
name: group.DisplayName,
alternateName: `/Groups/${group.Id}`,
path: `${parentPath}/${group.DisplayName}`
};
res.push(addGroup);
if (group.SubGroups) {
group.SubGroups.forEach((item) => {
const addGroups = extractGroups(item, addGroup.path);
res.push.apply(res, addGroups);
});
}
return res;
}
body.forEach((item: any) => {
results.push.apply(results, extractGroups(item, ''));
});
}
return results;
}
throw new Error('An error occurred while getting organization groups');
}
add(context: DataContext, file: string, item: DocumentNumberSeriesItem): any {
return super.add(context, file, item);
}
replace(context: DataContext, file: string, item: DocumentNumberSeriesItem): any {
return super.replace(context, file, item);
}
}