@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 CHANGED
@@ -1,3 +1,8 @@
1
+
2
+ [2.0.2x-alpha]
3
+ 1. Add id_token_hint to Keycloak logout
4
+ 2. tighten types
5
+
1
6
  [2.0.2w-alpha]
2
7
  1. Add tenant health to user context
3
8
  2. Add Day js isBetween plugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simitgroup/simpleapp-generator",
3
- "version": "2.0.2w-alpha",
3
+ "version": "2.0.2x-alpha",
4
4
  "description": "frontend nuxtjs and backend nests code generator using jsonschema.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,19 +1,20 @@
1
- import { UserContext } from '../user-context/user.context';
1
+ import { BadRequestException, InternalServerErrorException } from '@nestjs/common';
2
2
  import { InjectModel } from '@nestjs/mongoose';
3
- import { Model, FilterQuery } from 'mongoose';
4
- import { DocNumberFormatResult, ForeignKey, IsolationType, SchemaFields } from '../../framework/schemas';
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
- async generateNextNumberFromDocument(appUser: UserContext, docType: string, data: any) {
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: SchemaFields) {
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 (appUser:UserContext, filter:any, docType:string){
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
- async documentNumberChecker(appUser:UserContext,docType:string,docFormat:DocumentNoFormat){
281
+ return filter;
282
+ }
278
283
 
279
- const docMeta = this.getDocumentMeta(docType)
280
- const collectionName = docMeta.docName.toLowerCase()
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, any>;
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?: any;
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?: object;
22
+ filter?: PipelineStage.Match['$match'];
22
23
 
23
- fields?: any[];
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
- [key: string]: any;
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 = { action: string; collection: string; id: string[]; data: any[] };
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): Promise<Observable<any>> {
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: UserContext = req['sessionuser'];
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
- error: err.options,
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
- resp.status(err?.status ?? 500);
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();
@@ -72,6 +72,7 @@ export default NuxtAuthHandler({
72
72
  token.accessToken = account.access_token;
73
73
  token.refreshToken = account.refresh_token;
74
74
  token.expiresAt = account.expires_at;
75
+ token.idToken = account.id_token;
75
76
  return token;
76
77
  }
77
78
 
@@ -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 path = `${
9
- process.env.OAUTH2_CONFIGURL
10
- }/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(
11
- process.env.AUTH_ORIGIN ?? ""
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
- return {
15
- path: 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 { getServerSession } from "#auth";
9
- import axios from "axios";
7
+
8
+ import axios from 'axios';
10
9
  import fs from "node:fs";
11
- import { pathJoin } from "~/server/utils/path";
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
- export default defineEventHandler(async (event: any) => {
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
- try {
19
- session = await getServerSession(event);
20
- } catch (error) {
21
- return sendRedirect(event, "/login", 302);
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
- const req = event.node.req;
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
- if (!accessToken || typeof accessToken !== "string") {
35
- return sendRedirect(event, "/login", 302);
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
- if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH") {
39
- forwardData = await readBody(event);
40
- } else {
41
- forwardData = getQuery(event);
42
- }
125
+ frontEndRes.statusCode = res.status;
126
+ if(res.statusText) {
127
+ frontEndRes.statusMessage = res.statusText;
128
+ }
43
129
 
44
- // if(typeof forwardData === "object" && "_branch" in forwardData) {
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
- resolve(res.data);
129
- }
130
- })
131
- .catch((error) => {
132
- if (!error?.response) {
133
- console.log("backend server no response ", error.code);
134
- reject({
135
- statusMessage: "backendServerDownMessage",
136
- statusCode: 503,
137
- });
138
- } else {
139
- if (error.response?.status && error.response.status == 401) {
140
- return sendRedirect(event, "/login", 302);
141
- }
142
- reject({
143
- statusMessage: error.response.statusText,
144
- statusCode: error.response.status,
145
- data: error.response.data,
146
- }); // resolve({ status: 'ok' })
147
- // throw createError({ statusMessage: 'Bad Requests', statusCode: 404 })
148
- }
149
- });
150
-
151
- // resolve({
152
- // status: 'ok'
153
- // })
154
- });
155
- });
156
-
157
- function base64ToBuffer(base64String: string) {
158
- // Split the base64 string into parts
159
- const parts = base64String.split(",");
160
- const contentType = parts[0].split(":")[1];
161
- const base64Data = parts[1];
162
-
163
- // Decode the base64 data
164
- const binaryString = atob(base64Data);
165
- const buffer = new Buffer.from(binaryString, "binary");
166
-
167
- return buffer;
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
+ }