UNPKG

@universis/docutracks

Version:

Implementation of document numbering services hosted by docutracks

292 lines (275 loc) 11.1 kB
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); } }