@simitgroup/simpleapp-generator 2.0.2-w-alpha → 2.0.2-x-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ReleaseNote.md +5 -0
- package/package.json +1 -1
- package/templates/nest/src/simple-app/_core/features/document-no-format/document-no-format.service.ts.eta +68 -62
- package/templates/nest/src/simple-app/_core/features/profile/profile.controller.ts.eta +1 -1
- package/templates/nest/src/simple-app/_core/features/profile/profile.schema.ts.eta +2 -2
- package/templates/nest/src/simple-app/_core/framework/schemas/simple-app.schema.ts.eta +31 -23
- package/templates/nest/src/simple-app/_core/framework/simple-app.interceptor.ts.eta +23 -20
- package/templates/nuxt/server/api/auth/[...].ts.eta +1 -0
- package/templates/nuxt/server/api/auth/logout.ts.eta +20 -7
- package/templates/nuxt/server/api/profile/[...].ts.eta +161 -153
package/ReleaseNote.md
CHANGED
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BadRequestException, InternalServerErrorException } from '@nestjs/common';
|
|
2
2
|
import { InjectModel } from '@nestjs/mongoose';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { Branch } from '@resources/branch/branch.schema';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import { AnyObject, FilterQuery, Model } from 'mongoose';
|
|
6
|
+
import { DocNumberFormatResult, ForeignKey, IsolationType } from '../../framework/schemas';
|
|
7
|
+
import { UserContext } from '../user-context/user.context';
|
|
5
8
|
import { DocumentNoFormat, DocumentNoFormatPreview } from './document-no-format.schema';
|
|
6
|
-
import { Injectable, InternalServerErrorException, BadRequestException } from '@nestjs/common';
|
|
7
9
|
import { alldocuments } from './document.dict';
|
|
8
|
-
import dayjs from 'dayjs';
|
|
9
10
|
|
|
10
11
|
export class SimpleAppDocumentNoFormatService {
|
|
11
12
|
constructor(@InjectModel('DocumentNoFormat') private docformat: Model<DocumentNoFormat>) {}
|
|
12
|
-
getDocumentMeta = (docType:string)=> alldocuments.find(x => x.docType === docType)
|
|
13
|
+
getDocumentMeta = (docType: string) => alldocuments.find((x) => x.docType === docType);
|
|
13
14
|
|
|
14
15
|
async runListDocFormats(appUser: UserContext, doctype: string) {
|
|
15
16
|
doctype = doctype.toUpperCase();
|
|
16
|
-
const searchresult = await this.docformat.aggregate([
|
|
17
|
+
const searchresult = await this.docformat.aggregate<DocumentNoFormat>([
|
|
17
18
|
{
|
|
18
19
|
$match: {
|
|
19
20
|
docNoType: doctype,
|
|
@@ -40,7 +41,15 @@ export class SimpleAppDocumentNoFormatService {
|
|
|
40
41
|
}
|
|
41
42
|
return data;
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
|
|
45
|
+
async generateNextNumberFromDocument(
|
|
46
|
+
appUser: UserContext,
|
|
47
|
+
docType: string,
|
|
48
|
+
// ! Not sure what this type is. Where does "DocumentNoFormat" come from??? Why this field is PascalCase???
|
|
49
|
+
data: object & {
|
|
50
|
+
DocumentNoFormat?: ForeignKey;
|
|
51
|
+
},
|
|
52
|
+
) {
|
|
44
53
|
let formatId = '';
|
|
45
54
|
if (data.DocumentNoFormat && data.DocumentNoFormat._id) {
|
|
46
55
|
formatId = data.DocumentNoFormat._id;
|
|
@@ -51,6 +60,7 @@ export class SimpleAppDocumentNoFormatService {
|
|
|
51
60
|
code: docnoobj.formatName,
|
|
52
61
|
label: docnoobj.formatName,
|
|
53
62
|
};
|
|
63
|
+
|
|
54
64
|
data.DocumentNoFormat = result;
|
|
55
65
|
return docnoobj.result;
|
|
56
66
|
}
|
|
@@ -70,27 +80,27 @@ export class SimpleAppDocumentNoFormatService {
|
|
|
70
80
|
const result = await this.docformat.find(filter).session(session);
|
|
71
81
|
if (result && result.length > 0) {
|
|
72
82
|
const d: DocumentNoFormat = result[0];
|
|
73
|
-
const initNumber = d.nextNumber
|
|
83
|
+
const initNumber = d.nextNumber;
|
|
74
84
|
const recordId = d._id;
|
|
75
85
|
// const newdocno = SimpleAppDocumentNoFormatService.previewDocNo(d);
|
|
76
86
|
|
|
77
|
-
const checker = await this.documentNumberChecker(appUser,doctype,d)
|
|
87
|
+
const checker = await this.documentNumberChecker(appUser, doctype, d);
|
|
78
88
|
|
|
79
|
-
if(!checker.accept){
|
|
80
|
-
d.nextNumber = initNumber
|
|
81
|
-
const errdocno = SimpleAppDocumentNoFormatService.previewDocNo(d);
|
|
89
|
+
if (!checker.accept) {
|
|
90
|
+
d.nextNumber = initNumber;
|
|
91
|
+
const errdocno = SimpleAppDocumentNoFormatService.previewDocNo(d);
|
|
82
92
|
throw new InternalServerErrorException(`Unique Key ${errdocno} conflict`);
|
|
83
93
|
}
|
|
84
94
|
|
|
85
|
-
const newdocno = checker.newDocNo
|
|
95
|
+
const newdocno = checker.newDocNo;
|
|
86
96
|
const newnextnumber = initNumber + checker.offset;
|
|
87
97
|
const updatedata = { nextNumber: newnextnumber } as DocumentNoFormat;
|
|
88
98
|
|
|
89
99
|
const options = process.env.BlockGenerateDocNumberWithSession ? {} : { session: session };
|
|
90
100
|
const updateresult = await this.docformat.findByIdAndUpdate(recordId, updatedata, options);
|
|
91
|
-
result[0].nextNumber = initNumber
|
|
101
|
+
result[0].nextNumber = initNumber;
|
|
92
102
|
// console.log('add transaction step for update document no',[result[0]])
|
|
93
|
-
appUser.addTransactionStep('update','documentnoformat',[recordId],[result[0]])
|
|
103
|
+
appUser.addTransactionStep('update', 'documentnoformat', [recordId], [result[0]]);
|
|
94
104
|
if (updateresult) {
|
|
95
105
|
const result: DocNumberFormatResult = {
|
|
96
106
|
formatId: d._id,
|
|
@@ -107,7 +117,7 @@ export class SimpleAppDocumentNoFormatService {
|
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
async generateBranchDefaultDocNumbers(appUser: UserContext, data:
|
|
120
|
+
async generateBranchDefaultDocNumbers(appUser: UserContext, data: Branch) {
|
|
111
121
|
const branchName = data.branchName;
|
|
112
122
|
const branchCode = data.branchCode;
|
|
113
123
|
const recordId = data._id;
|
|
@@ -126,9 +136,8 @@ export class SimpleAppDocumentNoFormatService {
|
|
|
126
136
|
const docformats = alldocuments.filter((item) => item.docNumber);
|
|
127
137
|
const allFormats: DocumentNoFormat[] = [];
|
|
128
138
|
for (let i = 0; i < docformats.length; i++) {
|
|
129
|
-
|
|
130
139
|
const doc = docformats[i];
|
|
131
|
-
if(['tenantinvoice'].includes(doc.docType)) continue;
|
|
140
|
+
if (['tenantinvoice'].includes(doc.docType)) continue;
|
|
132
141
|
const pattern = doc.docNoPattern.replace('@BranchCode', branchCode);
|
|
133
142
|
const formatdata: DocumentNoFormat = {
|
|
134
143
|
_id: crypto.randomUUID(),
|
|
@@ -154,10 +163,10 @@ export class SimpleAppDocumentNoFormatService {
|
|
|
154
163
|
try {
|
|
155
164
|
//rollback when not able to create branch
|
|
156
165
|
const result = await this.docformat.insertMany(allFormats, { session: appUser.getDBSession() });
|
|
157
|
-
const allRecordIds = result.map(item=>item._id)
|
|
158
|
-
const allLogData = result.map(item=>null)
|
|
159
|
-
|
|
160
|
-
appUser.addTransactionStep('create','documentnoformat',allRecordIds,allLogData)
|
|
166
|
+
const allRecordIds = result.map((item) => item._id);
|
|
167
|
+
const allLogData = result.map((item) => null);
|
|
168
|
+
|
|
169
|
+
appUser.addTransactionStep('create', 'documentnoformat', allRecordIds, allLogData);
|
|
161
170
|
if (!result) {
|
|
162
171
|
throw new InternalServerErrorException(`Generate ${allFormats.length} document formats for "${branchCode}" failed.`, 'generateDefaultDocNumbers');
|
|
163
172
|
}
|
|
@@ -203,7 +212,7 @@ export class SimpleAppDocumentNoFormatService {
|
|
|
203
212
|
}
|
|
204
213
|
};
|
|
205
214
|
|
|
206
|
-
async search(appUser, filter: FilterQuery<DocumentNoFormat>) {
|
|
215
|
+
async search(appUser: UserContext, filter: FilterQuery<DocumentNoFormat>) {
|
|
207
216
|
const isolationFilter: FilterQuery<DocumentNoFormat> = {
|
|
208
217
|
tenantId: appUser.tenantId,
|
|
209
218
|
orgId: appUser.orgId,
|
|
@@ -228,77 +237,74 @@ export class SimpleAppDocumentNoFormatService {
|
|
|
228
237
|
|
|
229
238
|
if (result && result.length > 0) {
|
|
230
239
|
const d: DocumentNoFormat = result[0];
|
|
231
|
-
const initNumber = d.nextNumber
|
|
232
|
-
const checker = await this.documentNumberChecker(appUser,doctype,d)
|
|
240
|
+
const initNumber = d.nextNumber;
|
|
241
|
+
const checker = await this.documentNumberChecker(appUser, doctype, d);
|
|
233
242
|
|
|
234
|
-
if(!checker.accept){
|
|
235
|
-
d.nextNumber = initNumber
|
|
236
|
-
const errdocno = SimpleAppDocumentNoFormatService.previewDocNo(d);
|
|
243
|
+
if (!checker.accept) {
|
|
244
|
+
d.nextNumber = initNumber;
|
|
245
|
+
const errdocno = SimpleAppDocumentNoFormatService.previewDocNo(d);
|
|
237
246
|
throw new InternalServerErrorException(`Unique Key ${errdocno} conflict during request-multiple-number`);
|
|
238
247
|
}
|
|
239
248
|
|
|
240
|
-
|
|
241
|
-
d.nextNumber=initNumber + checker.offset - 1 //offset will return >=1, we want start from 0
|
|
249
|
+
d.nextNumber = initNumber + checker.offset - 1; //offset will return >=1, we want start from 0
|
|
242
250
|
const recordId = d._id;
|
|
243
|
-
for (let i = 0; i < quantity; i++) {
|
|
251
|
+
for (let i = 0; i < quantity; i++) {
|
|
244
252
|
const newdocno = SimpleAppDocumentNoFormatService.previewDocNo(d);
|
|
245
253
|
documentNumbers.push(newdocno);
|
|
246
254
|
d.nextNumber++;
|
|
247
255
|
}
|
|
248
256
|
const updatedata = { nextNumber: d.nextNumber } as DocumentNoFormat;
|
|
249
|
-
const options = process.env.BlockGenerateDocNumberWithSession ? {} : { session: appUser.getDBSession()
|
|
257
|
+
const options = process.env.BlockGenerateDocNumberWithSession ? {} : { session: appUser.getDBSession() };
|
|
250
258
|
const updateresult = await this.docformat.findByIdAndUpdate(recordId, updatedata, options);
|
|
251
|
-
appUser.addTransactionStep('update','documentnoformat',[recordId],[result[0]])
|
|
259
|
+
appUser.addTransactionStep('update', 'documentnoformat', [recordId], [result[0]]);
|
|
252
260
|
return documentNumbers;
|
|
253
261
|
} else {
|
|
254
262
|
throw new BadRequestException(`No active document number found for ${doctype}. Please update in Settings > Document Numbering Format`);
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
|
|
258
|
-
applyIsolationFilter
|
|
259
|
-
const docMeta = this.getDocumentMeta(docType)
|
|
260
|
-
if(docMeta.isolationType===IsolationType.tenant) {
|
|
261
|
-
filter['tenantId'] = appUser.tenantId
|
|
266
|
+
applyIsolationFilter(appUser: UserContext, filter: AnyObject, docType: string) {
|
|
267
|
+
const docMeta = this.getDocumentMeta(docType);
|
|
268
|
+
if ((docMeta.isolationType as IsolationType) === IsolationType.tenant) {
|
|
269
|
+
filter['tenantId'] = appUser.tenantId;
|
|
262
270
|
}
|
|
263
|
-
if(docMeta.isolationType===IsolationType.org) {
|
|
264
|
-
filter['tenantId'] = appUser.tenantId
|
|
265
|
-
filter['orgId'] = appUser.orgId
|
|
271
|
+
if ((docMeta.isolationType as IsolationType) === IsolationType.org) {
|
|
272
|
+
filter['tenantId'] = appUser.tenantId;
|
|
273
|
+
filter['orgId'] = appUser.orgId;
|
|
266
274
|
}
|
|
267
|
-
if(docMeta.isolationType===IsolationType.branch) {
|
|
268
|
-
filter['tenantId'] = appUser.tenantId
|
|
269
|
-
filter['orgId'] = appUser.orgId
|
|
270
|
-
filter['branchId'] = appUser.branchId
|
|
275
|
+
if ((docMeta.isolationType as IsolationType) === IsolationType.branch) {
|
|
276
|
+
filter['tenantId'] = appUser.tenantId;
|
|
277
|
+
filter['orgId'] = appUser.orgId;
|
|
278
|
+
filter['branchId'] = appUser.branchId;
|
|
271
279
|
}
|
|
272
|
-
|
|
273
|
-
return filter
|
|
274
|
-
}
|
|
275
|
-
|
|
276
280
|
|
|
277
|
-
|
|
281
|
+
return filter;
|
|
282
|
+
}
|
|
278
283
|
|
|
279
|
-
|
|
280
|
-
const
|
|
284
|
+
async documentNumberChecker(appUser: UserContext, docType: string, docFormat: DocumentNoFormat) {
|
|
285
|
+
const docMeta = this.getDocumentMeta(docType);
|
|
286
|
+
const collectionName = docMeta.docName.toLowerCase();
|
|
281
287
|
const collection = this.docformat.db.collection(collectionName);
|
|
282
|
-
const codeField = docMeta.uniqueKey
|
|
283
|
-
let newdocno = ''
|
|
288
|
+
const codeField = docMeta.uniqueKey;
|
|
289
|
+
let newdocno = '';
|
|
284
290
|
let attempt = 0;
|
|
285
291
|
let accept = false;
|
|
286
292
|
// console.log("run documentNumberChecker")
|
|
287
|
-
for (attempt = 0; attempt < 10; attempt++) {
|
|
288
|
-
newdocno = SimpleAppDocumentNoFormatService.previewDocNo(docFormat);
|
|
289
|
-
const tmpFilter = { [codeField]: newdocno}
|
|
290
|
-
const filter = this.applyIsolationFilter(appUser, tmpFilter,docType)
|
|
293
|
+
for (attempt = 0; attempt < 10; attempt++) {
|
|
294
|
+
newdocno = SimpleAppDocumentNoFormatService.previewDocNo(docFormat);
|
|
295
|
+
const tmpFilter = { [codeField]: newdocno };
|
|
296
|
+
const filter = this.applyIsolationFilter(appUser, tmpFilter, docType);
|
|
291
297
|
// console.log("filter",filter)
|
|
292
298
|
const existDoc = await collection.findOne(filter);
|
|
293
|
-
docFormat.nextNumber += 1
|
|
299
|
+
docFormat.nextNumber += 1;
|
|
294
300
|
if (existDoc) {
|
|
295
301
|
continue;
|
|
296
302
|
}
|
|
297
|
-
accept=true;
|
|
303
|
+
accept = true;
|
|
298
304
|
// console.log('accepted at attempt=',attempt)
|
|
299
|
-
break;
|
|
305
|
+
break;
|
|
300
306
|
}
|
|
301
307
|
// console.log('finally attempt',attempt+1)
|
|
302
|
-
return Promise.resolve({accept:accept,offset:attempt+1, newDocNo:newdocno})
|
|
308
|
+
return Promise.resolve({ accept: accept, offset: attempt + 1, newDocNo: newdocno });
|
|
303
309
|
}
|
|
304
310
|
}
|
|
@@ -33,7 +33,7 @@ export class ProfileController {
|
|
|
33
33
|
@ApiResponse({ status: 401, type: Object, description: 'Undefine profile' })
|
|
34
34
|
async getProfile(@AppUser() appUser: UserContext) {
|
|
35
35
|
this.logger.debug(`access getProfile API by ${appUser.getUid()},(${appUser.getId()})`);
|
|
36
|
-
const result = await this.profileservice.getProfile(appUser);
|
|
36
|
+
const result = (await this.profileservice.getProfile(appUser)) as UserContextInfo;
|
|
37
37
|
// this.logger.debug('getProfile result is:');
|
|
38
38
|
// this.logger.debug(result);
|
|
39
39
|
if (result) {
|
|
@@ -96,7 +96,7 @@ export class UserContextInfo {
|
|
|
96
96
|
branchInfo: Branch;
|
|
97
97
|
|
|
98
98
|
@ApiProperty({ type: () => Object, description: 'Store all the rest of useful fields regarding user or branch' })
|
|
99
|
-
moreProps?: Record<string,
|
|
99
|
+
moreProps?: Record<string, unknown>;
|
|
100
100
|
|
|
101
101
|
@ApiProperty({ type: () => TenantHealth, required: false })
|
|
102
102
|
tenantHealth?: TenantHealth;
|
|
@@ -139,7 +139,7 @@ export class ProfileUserBranch {
|
|
|
139
139
|
@ApiProperty({ type: String })
|
|
140
140
|
xOrg: string;
|
|
141
141
|
@ApiProperty({ type: Object, required: false })
|
|
142
|
-
organization?:
|
|
142
|
+
organization?: Record<string, unknown>;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
export class TenantPermission {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Field, ObjectType } from '@nestjs/graphql';
|
|
2
2
|
import { ApiProperty } from '@nestjs/swagger';
|
|
3
3
|
import { JSONSchema7 } from 'json-schema';
|
|
4
|
+
import { PipelineStage } from 'mongoose';
|
|
4
5
|
import { Permission } from 'src/simple-app/_core/resources/permission/permission.schema';
|
|
5
6
|
|
|
6
7
|
export class ModifiedCollection {
|
|
@@ -18,12 +19,14 @@ export class DocNumberFormatResult {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export class SearchBody {
|
|
21
|
-
filter?:
|
|
22
|
+
filter?: PipelineStage.Match['$match'];
|
|
22
23
|
|
|
23
|
-
fields?:
|
|
24
|
+
fields?: PipelineStage.Project['$project'];
|
|
25
|
+
|
|
26
|
+
sorts?: PipelineStage.Sort['$sort'];
|
|
27
|
+
|
|
28
|
+
lookup?: PipelineStage.Lookup['$lookup'];
|
|
24
29
|
|
|
25
|
-
sorts?: any[];
|
|
26
|
-
lookup?: object;
|
|
27
30
|
pagination?: {
|
|
28
31
|
pageSize?: number;
|
|
29
32
|
pageNo?: number;
|
|
@@ -39,22 +42,6 @@ export enum IsolationType {
|
|
|
39
42
|
export class MoreProjectionType {
|
|
40
43
|
[key: string]: string;
|
|
41
44
|
}
|
|
42
|
-
export class ApiEvent {
|
|
43
|
-
_id: string;
|
|
44
|
-
created: string;
|
|
45
|
-
updated?: string;
|
|
46
|
-
duration: number;
|
|
47
|
-
createdBy: string;
|
|
48
|
-
path: string;
|
|
49
|
-
ip: string;
|
|
50
|
-
method: string;
|
|
51
|
-
headers: any;
|
|
52
|
-
data?: any;
|
|
53
|
-
statusCode: number;
|
|
54
|
-
status: string;
|
|
55
|
-
errMsg?: string;
|
|
56
|
-
errData?: any;
|
|
57
|
-
}
|
|
58
45
|
|
|
59
46
|
export class DeleteResultType<T> {
|
|
60
47
|
data: T;
|
|
@@ -70,6 +57,7 @@ export class DocumentStatus {
|
|
|
70
57
|
readOnly: boolean;
|
|
71
58
|
actions: string[]; //api name ['confirm','revert','close','void' and etc]
|
|
72
59
|
}
|
|
60
|
+
|
|
73
61
|
export enum RESTMethods {
|
|
74
62
|
'post' = 'post',
|
|
75
63
|
'get' = 'get',
|
|
@@ -126,14 +114,21 @@ export class ForeignKey {
|
|
|
126
114
|
@Field()
|
|
127
115
|
@ApiProperty({ type: () => String })
|
|
128
116
|
_id?: string;
|
|
117
|
+
|
|
129
118
|
@Field()
|
|
130
119
|
@ApiProperty({ type: () => String })
|
|
131
120
|
code?: string;
|
|
121
|
+
|
|
132
122
|
@Field()
|
|
133
123
|
@ApiProperty({ type: () => String })
|
|
134
124
|
label?: string;
|
|
135
|
-
|
|
125
|
+
|
|
126
|
+
// ! DO NOT use an index signature on your own schemas. ForeignKey is intentionally strict — only
|
|
127
|
+
// ! `_id`, `code`, and `label` are allowed. For fields with extra dynamic properties, extend this
|
|
128
|
+
// ! class or use the appropriate XxxAutoComplete type instead.
|
|
129
|
+
// [key: string]: any;
|
|
136
130
|
}
|
|
131
|
+
|
|
137
132
|
export class MyForeignKey {
|
|
138
133
|
[collectionname: string]: string[];
|
|
139
134
|
}
|
|
@@ -141,6 +136,9 @@ export class MyForeignKey {
|
|
|
141
136
|
export class NoParam {}
|
|
142
137
|
|
|
143
138
|
export class DynamicParam {
|
|
139
|
+
// ! `any` is intentional here. DynamicParam represents a fully open key-value map where both
|
|
140
|
+
// ! the keys and value types are unknown at compile time. Do not use this class for typed fields.
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
142
|
[key: string]: any;
|
|
145
143
|
}
|
|
146
144
|
|
|
@@ -163,7 +161,6 @@ export class SchemaFields {
|
|
|
163
161
|
updatedBy?: string;
|
|
164
162
|
__v?: number;
|
|
165
163
|
documentStatus?: string;
|
|
166
|
-
[key: string]: any; //SimpleAppJSONSchema7 | SimpleAppJSONSchema7[] | undefined;
|
|
167
164
|
}
|
|
168
165
|
|
|
169
166
|
export class SimpleAppJSONSchema7 implements JSONSchema7 {
|
|
@@ -177,7 +174,18 @@ export class BranchPermission extends Permission {
|
|
|
177
174
|
branch: ForeignKey;
|
|
178
175
|
}
|
|
179
176
|
|
|
180
|
-
export type StepData = {
|
|
177
|
+
export type StepData = {
|
|
178
|
+
action: string;
|
|
179
|
+
|
|
180
|
+
collection: string;
|
|
181
|
+
|
|
182
|
+
id: string[];
|
|
183
|
+
|
|
184
|
+
// ! `any[]` is intentional here. Each step in a SAGA transaction can carry data from any
|
|
185
|
+
// ! MongoDB collection, so the shape is not known until runtime.
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
187
|
+
data: any[];
|
|
188
|
+
};
|
|
181
189
|
|
|
182
190
|
export type DocumentDictEntry = {
|
|
183
191
|
docName: string;
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* last change 2023-03-17
|
|
5
5
|
* Author: Ks Tan
|
|
6
6
|
*/
|
|
7
|
+
import { ApiEvent } from '@core-features/log/schemas';
|
|
7
8
|
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
|
8
9
|
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
|
|
10
|
+
import type { Response as ExpressResponse } from 'express';
|
|
9
11
|
import { Connection, Model } from 'mongoose';
|
|
10
|
-
import { Observable } from 'rxjs';
|
|
11
12
|
import { catchError, map, tap } from 'rxjs/operators';
|
|
12
13
|
import { UserContext } from '../features/user-context/user.context';
|
|
13
|
-
import { ApiEvent } from './schemas';
|
|
14
14
|
import { SimpleAppDbRevertService } from './simple-app-db-revert.service';
|
|
15
15
|
@Injectable()
|
|
16
16
|
export class SimpleAppInterceptor implements NestInterceptor {
|
|
@@ -20,40 +20,32 @@ export class SimpleAppInterceptor implements NestInterceptor {
|
|
|
20
20
|
private simpleAppDbRevertService: SimpleAppDbRevertService,
|
|
21
21
|
) {}
|
|
22
22
|
|
|
23
|
-
async intercept(context: ExecutionContext, next: CallHandler)
|
|
23
|
+
async intercept(context: ExecutionContext, next: CallHandler) {
|
|
24
24
|
//not http request then exclude such as graphql
|
|
25
25
|
if (context.getType() != 'http') {
|
|
26
26
|
// obtain usersession here
|
|
27
|
-
return next.handle().pipe(
|
|
28
|
-
tap(async () => {
|
|
29
|
-
//console.log("none http, do nothing at interceptor")
|
|
30
|
-
}),
|
|
31
|
-
);
|
|
27
|
+
return next.handle().pipe(tap(() => {}));
|
|
32
28
|
}
|
|
33
|
-
const req = context.switchToHttp().getRequest();
|
|
34
|
-
const resp = context.switchToHttp().getResponse();
|
|
29
|
+
const req = context.switchToHttp().getRequest<Request>();
|
|
30
|
+
const resp = context.switchToHttp().getResponse<ExpressResponse>();
|
|
35
31
|
//console.log("want to get user session:", Object.keys(req))
|
|
36
32
|
|
|
37
33
|
if (req.url == '/health') {
|
|
38
|
-
return next.handle().pipe(
|
|
39
|
-
tap(async () => {
|
|
40
|
-
//console.log("none http, do nothing at interceptor")
|
|
41
|
-
}),
|
|
42
|
-
);
|
|
34
|
+
return next.handle().pipe(tap(() => {}));
|
|
43
35
|
}
|
|
44
36
|
|
|
45
|
-
const usersession
|
|
37
|
+
const usersession = req['sessionuser'] as UserContext;
|
|
46
38
|
//console.log("after read user session:",usersession)
|
|
47
39
|
const method = req['method'];
|
|
48
40
|
const headers = { ...req['headers'] };
|
|
49
|
-
const ip = req['ip'];
|
|
41
|
+
const ip = req['ip'] as string;
|
|
50
42
|
const url = req['url'];
|
|
51
43
|
// let { url, method, headers, body }
|
|
52
44
|
const session = await this.connection.startSession();
|
|
53
45
|
if (!session['runCount']) {
|
|
54
46
|
session['runCount'] = 0;
|
|
55
47
|
} else {
|
|
56
|
-
session['runCount'] = session['runCount'] + 1;
|
|
48
|
+
session['runCount'] = (session['runCount'] as number) + 1;
|
|
57
49
|
}
|
|
58
50
|
usersession.setDBSession(session);
|
|
59
51
|
// const session: ClientSession = usersession.getDBSession();
|
|
@@ -83,8 +75,10 @@ export class SimpleAppInterceptor implements NestInterceptor {
|
|
|
83
75
|
return next.handle().pipe(
|
|
84
76
|
map((data) => {
|
|
85
77
|
if (data && typeof data === 'object' && 'pagination' in data && 'items' in data) {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
86
79
|
return data;
|
|
87
80
|
}
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
88
82
|
return data;
|
|
89
83
|
}),
|
|
90
84
|
catchError(async (err, caught) => {
|
|
@@ -97,20 +91,29 @@ export class SimpleAppInterceptor implements NestInterceptor {
|
|
|
97
91
|
}
|
|
98
92
|
|
|
99
93
|
const responseBody = {
|
|
94
|
+
// eslint-disable-next-line
|
|
100
95
|
message: err.message,
|
|
101
96
|
timestamp: new Date().toISOString(),
|
|
102
97
|
path: url,
|
|
103
|
-
|
|
98
|
+
// eslint-disable-next-line
|
|
99
|
+
error: err.options as object,
|
|
104
100
|
};
|
|
105
101
|
|
|
102
|
+
// eslint-disable-next-line
|
|
106
103
|
eventObj.statusCode = err.status;
|
|
104
|
+
// eslint-disable-next-line
|
|
107
105
|
eventObj.errMsg = responseBody.message;
|
|
108
106
|
eventObj.data = req.body;
|
|
109
107
|
eventObj.status = 'NG';
|
|
110
108
|
eventObj.errData = responseBody.error;
|
|
111
|
-
|
|
109
|
+
|
|
110
|
+
resp.status(
|
|
111
|
+
// eslint-disable-next-line
|
|
112
|
+
err?.status ?? 500,
|
|
113
|
+
);
|
|
112
114
|
return responseBody;
|
|
113
115
|
}),
|
|
116
|
+
// eslint-disable-next-line
|
|
114
117
|
tap(async () => {
|
|
115
118
|
// console.log("============interceptor tap",method,url)
|
|
116
119
|
const endtime = new Date();
|
|
@@ -4,14 +4,27 @@
|
|
|
4
4
|
* last change 2024-02-23
|
|
5
5
|
* Author: Ks Tan
|
|
6
6
|
*/
|
|
7
|
+
import { getToken } from '#auth';
|
|
8
|
+
|
|
7
9
|
export default defineEventHandler(async (event) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
10
|
+
const token = await getToken({ event });
|
|
11
|
+
const idToken = token?.idToken as string | undefined;
|
|
12
|
+
|
|
13
|
+
// NOTE: AUTH_ORIGIN is encoded but intentionally without trailing slash —
|
|
14
|
+
// the logout composable appends the encoded path (e.g. %2Flogin%3F...) directly
|
|
15
|
+
// so Keycloak decodes the full absolute URL: https://origin/login?callbackUrl=...
|
|
16
|
+
// id_token_hint is required in Keycloak 18+ to skip the logout confirmation page.
|
|
17
|
+
// post_logout_redirect_uri MUST be last — the logout composable appends the
|
|
18
|
+
// encoded path directly to this string (e.g. %2Flogin%3FcallbackUrl%3D...)
|
|
19
|
+
// so it becomes part of the redirect URI value, not a new param.
|
|
20
|
+
let path = `${process.env.OAUTH2_CONFIGURL}/protocol/openid-connect/logout`
|
|
21
|
+
+ `?client_id=${encodeURIComponent(process.env.OAUTH2_CLIENTID ?? '')}`;
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
path
|
|
23
|
+
if (idToken) {
|
|
24
|
+
path += `&id_token_hint=${encodeURIComponent(idToken)}`;
|
|
16
25
|
}
|
|
26
|
+
|
|
27
|
+
path += `&post_logout_redirect_uri=${encodeURIComponent(process.env.AUTH_ORIGIN ?? '')}`;
|
|
28
|
+
|
|
29
|
+
return { path };
|
|
17
30
|
});
|
|
@@ -4,165 +4,173 @@
|
|
|
4
4
|
* last change 2024-02-23
|
|
5
5
|
* Author: Ks Tan
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
import axios from "axios";
|
|
7
|
+
|
|
8
|
+
import axios from 'axios';
|
|
10
9
|
import fs from "node:fs";
|
|
11
|
-
import {
|
|
10
|
+
import { getServerSession } from '#auth'
|
|
11
|
+
import type { Session } from 'next-auth';
|
|
12
|
+
import { pathJoin } from '~/server/utils/path';
|
|
13
|
+
|
|
14
|
+
export default defineEventHandler(async (event:any) => {
|
|
15
|
+
type additionalprops = {accessToken?:string}
|
|
16
|
+
let session:any=null
|
|
17
|
+
// console.log("profile api-------------------------")
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
session = await getServerSession(event)
|
|
21
|
+
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return sendRedirect(event, '/login', 401)
|
|
24
|
+
}
|
|
25
|
+
if(!session) {
|
|
26
|
+
return sendRedirect(event, "/login", 302);
|
|
27
|
+
}
|
|
28
|
+
return new Promise<any>(async (resolve, reject) => {
|
|
29
|
+
|
|
30
|
+
const seperateSymbol = '.';
|
|
31
|
+
const documentLink = event.context.params?._ ?? ''
|
|
32
|
+
const accessToken = session?.accessToken;
|
|
33
|
+
let forwardData: any = {};
|
|
34
|
+
|
|
35
|
+
const req = event.node.req;
|
|
36
|
+
|
|
37
|
+
if (!accessToken || typeof accessToken !== "string") {
|
|
38
|
+
return sendRedirect(event, "/login", 302);
|
|
39
|
+
}
|
|
12
40
|
|
|
13
|
-
|
|
14
|
-
type additionalprops = { accessToken?: string };
|
|
15
|
-
let session: any = null;
|
|
16
|
-
// console.log("profile api-------------------------")
|
|
41
|
+
if(req.method == 'POST' || req.method == 'PUT' || req.method == 'PATCH') {
|
|
17
42
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
if (!session) {
|
|
24
|
-
return sendRedirect(event, "/login", 302);
|
|
25
|
-
}
|
|
26
|
-
return new Promise<any>(async (resolve, reject) => {
|
|
27
|
-
const seperateSymbol = ".";
|
|
28
|
-
const documentLink = event.context.params?._ ?? "";
|
|
29
|
-
const accessToken = session?.accessToken;
|
|
30
|
-
let forwardData: any = {};
|
|
43
|
+
forwardData = await readBody(event);
|
|
44
|
+
} else {
|
|
45
|
+
forwardData = getQuery(event);
|
|
46
|
+
}
|
|
31
47
|
|
|
32
|
-
|
|
48
|
+
// if(typeof forwardData === "object" && "_branch" in forwardData) {
|
|
49
|
+
// xOrg = xOrg + forwardData._branch;
|
|
50
|
+
// delete forwardData._branch;
|
|
51
|
+
// }
|
|
52
|
+
|
|
53
|
+
const frontEndRes = event.node.res;
|
|
54
|
+
|
|
55
|
+
const url = process.env.SIMPLEAPP_BACKEND_URL + `/profile/${documentLink}`;
|
|
56
|
+
// console.warn('backend server-----',req.method,url,forwardData)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
const axiosConfig: any = {
|
|
60
|
+
method: req.method,
|
|
61
|
+
url: url,
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: `Bearer ${accessToken}`,
|
|
64
|
+
},
|
|
65
|
+
data: forwardData,
|
|
66
|
+
params: forwardData,
|
|
67
|
+
}
|
|
33
68
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
69
|
+
if(documentLink=='avatar' ){
|
|
70
|
+
axiosConfig.url = pathJoin([useRuntimeConfig().public.imageUrl, documentLink]);
|
|
71
|
+
delete axiosConfig.params
|
|
72
|
+
}
|
|
73
|
+
// console.log("axiosConfig",axiosConfig)
|
|
74
|
+
// if(key === 'system') {
|
|
75
|
+
// axiosConfig.headers["X-Global"] = true;
|
|
76
|
+
// delete axiosConfig.headers["X-Org"];
|
|
77
|
+
// }
|
|
78
|
+
|
|
79
|
+
// if(otherLink.includes('avatar')) {
|
|
80
|
+
// axiosConfig.responseType = 'arraybuffer';
|
|
81
|
+
// // axiosConfig.headers['Acceptable'] = 'text/html,image/avif,image/webp,image/apng';
|
|
82
|
+
// }
|
|
83
|
+
|
|
84
|
+
axios(axiosConfig).then((res) => {
|
|
85
|
+
// console.log("RRes",res)
|
|
86
|
+
if (res.headers['content-type'] === 'image/png') {
|
|
87
|
+
// Set the response headers for the image
|
|
88
|
+
frontEndRes.setHeader('Content-Type', 'image/png');
|
|
89
|
+
frontEndRes.setHeader('Content-Disposition', 'inline');
|
|
90
|
+
|
|
91
|
+
// Send the image data as the response body
|
|
92
|
+
frontEndRes.end(Buffer.from(res.data, 'binary'));
|
|
93
|
+
}else if(documentLink.includes('images/')){
|
|
94
|
+
// console.log(documentLink," Resdata of base64 photo",res.data.length)
|
|
95
|
+
let imageData
|
|
96
|
+
frontEndRes.setHeader('Content-Type', 'image/png');
|
|
97
|
+
|
|
98
|
+
// console.log("load image for",documentLink)
|
|
99
|
+
if( res.data.length){
|
|
100
|
+
// console.log("obtain base64 from server length:",res.data.length)
|
|
101
|
+
imageData = base64ToBuffer(res.data);
|
|
102
|
+
|
|
103
|
+
}else{
|
|
104
|
+
// console.log("server no image, use default image")
|
|
105
|
+
const folder = 'public/images/'
|
|
106
|
+
let filename = ''
|
|
107
|
+
if(documentLink.includes('student')) filename='student.png';
|
|
108
|
+
else if(documentLink.includes('teacher')) filename='teacher.png';
|
|
109
|
+
else if(documentLink.includes('organization')) filename='organization.png';
|
|
110
|
+
else filename='unknown.png';
|
|
111
|
+
const fullpath = folder+filename;
|
|
112
|
+
// console.log("photo path",fullpath)
|
|
113
|
+
if(fs.existsSync(fullpath)){
|
|
114
|
+
imageData = fs.readFileSync(fullpath)
|
|
115
|
+
}else{
|
|
116
|
+
console.log(fullpath,'does not exists')
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
frontEndRes.end(Buffer.from(imageData, 'binary'));
|
|
120
|
+
|
|
121
|
+
} else {
|
|
122
|
+
// For non-image responses, set the Content-Type header and send the response body
|
|
123
|
+
// setHeader(event, 'Content-type', <string>res.headers['Content-Type']);
|
|
37
124
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
125
|
+
frontEndRes.statusCode = res.status;
|
|
126
|
+
if(res.statusText) {
|
|
127
|
+
frontEndRes.statusMessage = res.statusText;
|
|
128
|
+
}
|
|
43
129
|
|
|
44
|
-
|
|
45
|
-
// xOrg = xOrg + forwardData._branch;
|
|
46
|
-
// delete forwardData._branch;
|
|
47
|
-
// }
|
|
48
|
-
|
|
49
|
-
const frontEndRes = event.node.res;
|
|
50
|
-
|
|
51
|
-
const url = process.env.SIMPLEAPP_BACKEND_URL + `/profile/${documentLink}`;
|
|
52
|
-
// console.warn('backend server-----',req.method,url,forwardData)
|
|
53
|
-
|
|
54
|
-
const axiosConfig: any = {
|
|
55
|
-
method: req.method,
|
|
56
|
-
url: url,
|
|
57
|
-
headers: {
|
|
58
|
-
Authorization: `Bearer ${accessToken}`,
|
|
59
|
-
},
|
|
60
|
-
data: forwardData,
|
|
61
|
-
params: forwardData,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
if (documentLink == "avatar") {
|
|
65
|
-
axiosConfig.url = pathJoin([
|
|
66
|
-
useRuntimeConfig().public.imageUrl,
|
|
67
|
-
documentLink,
|
|
68
|
-
]);
|
|
69
|
-
delete axiosConfig.params;
|
|
70
|
-
}
|
|
71
|
-
// console.log("axiosConfig",axiosConfig)
|
|
72
|
-
// if(key === 'system') {
|
|
73
|
-
// axiosConfig.headers["X-Global"] = true;
|
|
74
|
-
// delete axiosConfig.headers["X-Org"];
|
|
75
|
-
// }
|
|
76
|
-
|
|
77
|
-
// if(otherLink.includes('avatar')) {
|
|
78
|
-
// axiosConfig.responseType = 'arraybuffer';
|
|
79
|
-
// // axiosConfig.headers['Acceptable'] = 'text/html,image/avif,image/webp,image/apng';
|
|
80
|
-
// }
|
|
81
|
-
|
|
82
|
-
axios(axiosConfig)
|
|
83
|
-
.then((res) => {
|
|
84
|
-
// console.log("RRes",res)
|
|
85
|
-
if (res.headers["content-type"] === "image/png") {
|
|
86
|
-
// Set the response headers for the image
|
|
87
|
-
frontEndRes.setHeader("Content-Type", "image/png");
|
|
88
|
-
frontEndRes.setHeader("Content-Disposition", "inline");
|
|
89
|
-
|
|
90
|
-
// Send the image data as the response body
|
|
91
|
-
frontEndRes.end(Buffer.from(res.data, "binary"));
|
|
92
|
-
} else if (documentLink.includes("images/")) {
|
|
93
|
-
// console.log(documentLink," Resdata of base64 photo",res.data.length)
|
|
94
|
-
let imageData;
|
|
95
|
-
frontEndRes.setHeader("Content-Type", "image/png");
|
|
96
|
-
|
|
97
|
-
// console.log("load image for",documentLink)
|
|
98
|
-
if (res.data.length) {
|
|
99
|
-
// console.log("obtain base64 from server length:",res.data.length)
|
|
100
|
-
imageData = base64ToBuffer(res.data);
|
|
101
|
-
} else {
|
|
102
|
-
// console.log("server no image, use default image")
|
|
103
|
-
const folder = "public/images/";
|
|
104
|
-
let filename = "";
|
|
105
|
-
if (documentLink.includes("student")) filename = "student.png";
|
|
106
|
-
else if (documentLink.includes("teacher")) filename = "teacher.png";
|
|
107
|
-
else if (documentLink.includes("organization"))
|
|
108
|
-
filename = "organization.png";
|
|
109
|
-
else filename = "unknown.png";
|
|
110
|
-
const fullpath = folder + filename;
|
|
111
|
-
// console.log("photo path",fullpath)
|
|
112
|
-
if (fs.existsSync(fullpath)) {
|
|
113
|
-
imageData = fs.readFileSync(fullpath);
|
|
114
|
-
} else {
|
|
115
|
-
console.log(fullpath, "does not exists");
|
|
130
|
+
resolve(res.data);
|
|
116
131
|
}
|
|
117
|
-
}
|
|
118
|
-
frontEndRes.end(Buffer.from(imageData, "binary"));
|
|
119
|
-
} else {
|
|
120
|
-
// For non-image responses, set the Content-Type header and send the response body
|
|
121
|
-
// setHeader(event, 'Content-type', <string>res.headers['Content-Type']);
|
|
122
|
-
|
|
123
|
-
frontEndRes.statusCode = res.status;
|
|
124
|
-
if (res.statusText) {
|
|
125
|
-
frontEndRes.statusMessage = res.statusText;
|
|
126
|
-
}
|
|
127
132
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
133
|
+
}).catch((error) => {
|
|
134
|
+
if(!error?.response){
|
|
135
|
+
console.log("backend server no response ",error.code)
|
|
136
|
+
reject({
|
|
137
|
+
statusMessage:"backendServerDownMessage",
|
|
138
|
+
statusCode: 503,
|
|
139
|
+
});
|
|
140
|
+
}else{
|
|
141
|
+
|
|
142
|
+
if (error.response?.status && error.response.status == 401) {
|
|
143
|
+
// rejecting would bubble a 401 into the client and often renders the Nuxt error page.
|
|
144
|
+
// Instead, end the request with a redirect so the browser can enter the login flow.
|
|
145
|
+
return sendRedirect(event, "/login", 401);
|
|
146
|
+
}
|
|
147
|
+
reject({
|
|
148
|
+
statusMessage: error.response.statusText,
|
|
149
|
+
statusCode: error.response.status ,
|
|
150
|
+
data: error.response.data
|
|
151
|
+
}); // resolve({ status: 'ok' })
|
|
152
|
+
// throw createError({ statusMessage: 'Bad Requests', statusCode: 404 })
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// resolve({
|
|
157
|
+
// status: 'ok'
|
|
158
|
+
// })
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
function base64ToBuffer(base64String:string) {
|
|
166
|
+
// Split the base64 string into parts
|
|
167
|
+
const parts = base64String.split(',');
|
|
168
|
+
const contentType = parts[0].split(':')[1];
|
|
169
|
+
const base64Data = parts[1];
|
|
170
|
+
|
|
171
|
+
// Decode the base64 data
|
|
172
|
+
const binaryString = atob(base64Data);
|
|
173
|
+
const buffer = new Buffer.from(binaryString, 'binary');
|
|
174
|
+
|
|
175
|
+
return buffer
|
|
176
|
+
}
|