@simitgroup/simpleapp-generator 1.6.4-f-alpha → 1.6.6-a-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,19 @@
1
+ [1.6.6a-alpha]
2
+ 1. fix simpleapp generated frontend lib bugs
3
+
4
+ [1.6.5g]
5
+ 1. change simpleapp document controller use document name instead of document type
6
+ 2. allow use header x-apikey(match env X_APIKEY), x-apisecret (match env X_APISECRET), to by pass access token. which will use robotuser in user context
7
+ 3. add support of x-guest-accesstoken (submit by external user source, like parent/student app), it store into usercontext as guestinfo, since appuser = robotuser
8
+ 4. Fix document search properties to restrict (required field and sorts)
9
+
10
+
11
+ [1.6.5f]
12
+ 1. added webhook db
13
+ 2. support audit trail
14
+ 3. support upload avatar to cloudapi
15
+ 4. added html input for simpleapp
16
+
1
17
  [1.2.0]
2
18
  1. lot of house keeping
3
19
  2. frontend more stable login using keycloak
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simitgroup/simpleapp-generator",
3
- "version": "1.6.4f-alpha",
3
+ "version": "1.6.6a-alpha",
4
4
  "description": "frontend nuxtjs and backend nests code generator using jsonschema",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -56,9 +56,9 @@ import {UserContext} from '../commons/user.context'
56
56
  }
57
57
  %>
58
58
  <% let superadmindoctype = ['tenant','globaluser'] %>
59
- const doctype = '<%= it.doctype %>'.toUpperCase();
60
- @ApiTags(doctype)
61
- @Controller(doctype.toLowerCase())
59
+ const apiname = '<%= it.typename %>'.toUpperCase();
60
+ @ApiTags(apiname)
61
+ @Controller(apiname.toLowerCase())
62
62
  export class <%= it.typename %>Controller extends SimpleAppAbstractController<
63
63
  <%= it.typename %>Service,
64
64
  schemas.<%= it.typename%>,
@@ -34,7 +34,7 @@ export type {
34
34
 
35
35
  } from '../openapi'
36
36
 
37
- export class <%= it.typename%>Client extends SimpleAppClient<openapi.<%= it.typename%>,openapi.<%= it.doctype.toUpperCase()%>Api>{
37
+ export class <%= capitalizeFirstLetter(it.name)%>Client extends SimpleAppClient<openapi.<%= capitalizeFirstLetter(it.name)%>,openapi.<%= it.name.toUpperCase()%>Api>{
38
38
  public readonly schema= <%~ JSON.stringify(it.jsonschema) %> as SchemaType;
39
39
  protected documentIdentityCode='<%~ it.autocompletecode %>'
40
40
  protected documentIdentityName='<%~ it.autocompletename %>'
@@ -49,7 +49,7 @@ export class <%= it.typename%>Client extends SimpleAppClient<openapi.<%= it.type
49
49
  }
50
50
 
51
51
  //const apipath = `${useRuntimeConfig().public.API_URL}/${xorg}`
52
- //const apiobj = new <%= it.doctype.toUpperCase()%>Api(undefined,apipath,$axios)
52
+ //const apiobj = new <%= it.name.toUpperCase()%>Api(undefined,apipath,$axios)
53
53
  const apiobj = getDocumentApi('<%=it.name %>')
54
54
  super(apiobj,'<%= it.doctype %>','<%=it.name %>')
55
55
  this.event=$event
@@ -13,6 +13,8 @@ HTTP_PORT=<%=it.configs.backendPort%>
13
13
 
14
14
  BPMN_PATH=./src/simpleapp/workflows/bpmn/
15
15
 
16
+ X_APIKEY=
17
+ X_APISECRET=
16
18
 
17
19
  OAUTH2_BASEURL=<%=it.configs.oauthSetting ? it.configs.oauthSetting.oauthBaseUrl : ''%>
18
20
 
@@ -25,6 +25,7 @@ import {
25
25
  RoleGuard,
26
26
  PolicyEnforcementMode,
27
27
  } from 'nest-keycloak-connect';
28
+ import { CustomKeycloakGuard } from './simpleapp/generate/commons/customkeycloak.guard';
28
29
  import {RolesGuard} from './simpleapp/generate/commons/roles/roles.guard'
29
30
  import { ConfigModule } from '@nestjs/config';
30
31
  import { ServeStaticModule } from '@nestjs/serve-static';
@@ -92,11 +93,17 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
92
93
  ]),
93
94
  ],
94
95
  controllers: [AppController],
95
- providers: [AppService,AppResolver, {
96
- provide: APP_INTERCEPTOR,
97
- useClass: ResponseInterceptor,
98
- },
99
- {provide: APP_GUARD,useClass: AuthGuard,},
96
+ providers: [
97
+ AppService,
98
+ CustomKeycloakGuard,
99
+ ResourceGuard,
100
+ AppResolver,
101
+ {
102
+ provide: APP_INTERCEPTOR,
103
+ useClass: ResponseInterceptor,
104
+ },
105
+ {provide: APP_GUARD,useClass: CustomKeycloakGuard,},
106
+ // {provide: APP_GUARD,useClass: AuthGuard,},
100
107
  {provide: APP_GUARD,useClass: ResourceGuard,},
101
108
  {provide: APP_GUARD,useClass: RolesGuard,}
102
109
  ],
@@ -49,9 +49,39 @@ async function bootstrap() {
49
49
  },
50
50
  },
51
51
  'oauth2',
52
+ ).
53
+ addApiKey(
54
+ {
55
+ in: 'header',
56
+ name: 'x-apikey',
57
+ type: 'apiKey',
58
+ description: 'optional only use for specal case',
59
+ },
60
+ 'x-apikey',
61
+ ).addApiKey(
62
+ {
63
+ in: 'header',
64
+ name: 'x-apisecret',
65
+ type: 'apiKey',
66
+ description: 'optional only use for specal case',
67
+ },
68
+ 'x-apisecret',
69
+ )
70
+ .addApiKey(
71
+ {
72
+ in: 'header',
73
+ name: 'x-guest-accesstoken',
74
+ type: 'apiKey',
75
+ description: 'optional only use for specal case',
76
+ },
77
+ 'x-guest-accesstoken',
52
78
  )
79
+
53
80
  .addSecurityRequirements('x-org')
54
81
  .addSecurityRequirements('oauth2')
82
+ .addSecurityRequirements('x-apikey')
83
+ .addSecurityRequirements('x-apisecret')
84
+ .addSecurityRequirements('x-guest-accesstoken')
55
85
  .build();
56
86
  const document = SwaggerModule.createDocument(app, config);
57
87
  SwaggerModule.setup('api', app, document, {
@@ -7,16 +7,50 @@
7
7
 
8
8
  import { ApiProperty } from '@nestjs/swagger';
9
9
  export class ApiKeyValuePair {
10
- @ApiProperty({"type":Object,"required":false,"examples":['{"field1":"1"}'],"default":""} )
11
- field1: any
10
+ @ApiProperty({
11
+ type: Object,
12
+ required: false,
13
+ examples: ['{"field1":"1"}'],
14
+ default: '',
15
+ })
16
+ field1: any;
17
+ }
18
+ export class SortItem {
19
+
20
+ }
21
+
22
+
23
+ export class ApiSearchBody {
24
+ @ApiProperty({
25
+ type: Object,
26
+ required: false,
27
+ examples: ['{"field1":"1"}'],
28
+ default: { field1: 'ok', field2: true },
29
+ })
30
+ filter?: Object;
31
+ @ApiProperty({
32
+ type: [String],
33
+ required: false,
34
+ examples: ['["field1","field2"]'],
35
+ default: ['field1', 'field2'],
36
+ })
37
+ fields?: string[];
38
+ @ApiProperty({
39
+ type: 'array',
40
+ required: false,
41
+ items:{
42
+ type: 'array',
43
+ items:{
44
+ type:'string'
45
+ }
46
+ },
47
+ examples: ['[[ "field1", "asc" ]]'],
48
+ default: [['field1', 'asc']],
49
+ })
50
+
51
+ sorts?: string[][];
52
+
53
+
54
+ @ApiProperty({ type: () => Object, required: false })
55
+ lookup: Object;
12
56
  }
13
- export class ApiSearchBody{
14
- @ApiProperty({ "type":Object, "required":false, "examples":['{"field1":"1"}'] ,"default":{"field1":"ok","field2":true,}} )
15
- filter: Object;
16
- @ApiProperty({"type":[Object],"required":true,"examples":['["field1","field2"]'],"default":["field1","field2"]} )
17
- fields: [Object];
18
- @ApiProperty({"type":()=>[Object],"required":true,"examples":['[[ "field1", "asc" ]]'],"default":[[ 'field1', 'asc' ]]} )
19
- sorts: [Object];
20
- @ApiProperty({type: () => Object,required: false,})
21
- lookup: Object
22
- }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * This file was automatically generated by simpleapp generator. Every
3
+ * MODIFICATION OVERRIDE BY GENERATEOR
4
+ * last change 2024-04-17
5
+ * Author: Ks Tan
6
+ */
7
+ import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
8
+ import { Reflector } from '@nestjs/core';
9
+ import { ResourceGuard } from 'nest-keycloak-connect';
10
+
11
+ @Injectable()
12
+ export class CustomKeycloakGuard implements CanActivate {
13
+ constructor(private reflector: Reflector, private resourceGuard: ResourceGuard) {}
14
+
15
+ async canActivate(context: ExecutionContext): Promise<boolean> {
16
+ const request = context.switchToHttp().getRequest();
17
+
18
+
19
+ //graphql no http request, exclude from capability of x-apikey
20
+ if(request?.headers){
21
+ const apiKey = request.headers['x-apikey'];
22
+ const apiSecret = request.headers['x-apisecret'];
23
+ // validate apikey and apisecret at middleware level, reach here mean approved as robot
24
+ if (apiKey && apiSecret ) {
25
+ return true;
26
+ }
27
+ }
28
+
29
+ // If API key is not present, fall back to Keycloak authentication
30
+ try {
31
+ const canActivate = await this.resourceGuard.canActivate(context);
32
+ return canActivate as boolean;
33
+ } catch (error) {
34
+ throw new UnauthorizedException('Invalid API key or Keycloak token');
35
+ }
36
+ }
37
+
38
+ private validateApiKey(apiKey: string, apiSecret: string): boolean {
39
+ // Implement your API key and secret validation logic here
40
+ // This is just a simple example
41
+ return apiKey === 'your-valid-api-key' && apiSecret === 'your-valid-api-secret';
42
+ }
43
+ }
@@ -56,9 +56,9 @@ export class TenantMiddleware implements NestMiddleware {
56
56
 
57
57
  if (req.baseUrl == '/graphql') {
58
58
  let tokenstr: string = req.headers['authorization'] ?? '';
59
- tokenstr = tokenstr.replace('Bearer ', '');
60
- const xorg = req.headers['x-org'] ?? this.defaultxorg;
61
- if (tokenstr) {
59
+ tokenstr = tokenstr.replace('Bearer ', '');
60
+ const xorg = req.headers['x-org'] ?? this.defaultxorg;
61
+ if (tokenstr) {
62
62
  await u.setCurrentUserInfo(tokenstr, xorg);
63
63
  }
64
64
  req['sessionuser'] = u;
@@ -66,6 +66,33 @@ export class TenantMiddleware implements NestMiddleware {
66
66
  return;
67
67
  }
68
68
  this.logger.debug(`running TenantMiddleware for ${req.baseUrl}`);
69
+
70
+
71
+ //if APIKEY defined, and there is api key and secret supplied. use robot user
72
+ if(process.env.X_APIKEY &&req.headers['x-apikey'] && req.headers['x-apisecret']){
73
+
74
+ if(req.headers['x-apikey']==process.env.X_APIKEY &&
75
+ req.headers['x-apisecret']==process.env.X_APISECRET){
76
+
77
+ u.setAsStaticUser(
78
+ '00000000-0000-0000-0000-000000000000',
79
+ 'robot',
80
+ 'Robot',
81
+ 'robot@a.org',
82
+ req.headers['x-org']?? this.defaultxorg
83
+ );
84
+ if(req.headers['x-guest-accesstoken']) {
85
+ u.setGuestToken(<string>req.headers['x-guest-accesstoken']);
86
+ }
87
+ req['sessionuser'] = u;
88
+ next();
89
+ return;
90
+ }else{
91
+ this.logger.log('invalid api key/scret');
92
+ throw new BadRequestException('invalid api key/scret');
93
+ }
94
+ }
95
+
69
96
  if (!req.headers['authorization']) {
70
97
  this.logger.log('undefine bearer token');
71
98
  return res.status(401).send('Undefine bearer token');
@@ -84,8 +111,7 @@ export class TenantMiddleware implements NestMiddleware {
84
111
  let tokenstr: string = req.headers['authorization'];
85
112
  tokenstr = tokenstr.replace('Bearer ', '');
86
113
 
87
- const xorg = req.headers['x-org'] ?? this.defaultxorg;
88
-
114
+ const xorg = req.headers['x-org'] ?? this.defaultxorg;
89
115
  await u.setCurrentUserInfo(tokenstr, xorg);
90
116
  if (u.getId() == '' && this.requireXorg(req.baseUrl)) {
91
117
  this.logger.log('access deny of no user:', req.baseUrl);
@@ -11,12 +11,12 @@ import { Model } from 'mongoose';
11
11
  import { UserContext } from 'src/simpleapp/generate/commons/user.context';
12
12
  import { User } from 'src/simpleapp/services/user.service';
13
13
  import { Permission } from 'src/simpleapp/services/perm.service';
14
-
14
+ const Base64URL = require('@darkwolf/base64url');
15
15
  @Injectable()
16
16
  export class SimpleAppRobotUserService {
17
17
  private systemAccessToken: string;
18
- private setToken = (token:string) => this.systemAccessToken = token
19
- private getToken = ()=>this.systemAccessToken
18
+ private setToken = (token: string) => (this.systemAccessToken = token);
19
+ private getToken = () => this.systemAccessToken;
20
20
  private expired: string;
21
21
  logger = new Logger();
22
22
  @InjectModel('User') private readonly usermodel: Model<User>;
@@ -28,7 +28,7 @@ export class SimpleAppRobotUserService {
28
28
  }
29
29
 
30
30
  async init() {
31
- await this.refreshSystemToken();
31
+ // await this.refreshSystemToken();
32
32
  }
33
33
  async refreshSystemToken() {
34
34
  enum GrantType {
@@ -76,25 +76,27 @@ export class SimpleAppRobotUserService {
76
76
  return data;
77
77
  });
78
78
 
79
- this.setToken(tokens.access_token)
80
- // console.log("access token ",this.getToken())
79
+ this.setToken(tokens.access_token);
80
+ console.log("access token ",this.getToken())
81
81
  const nextrefresh = tokens.expires_in * 0.8;
82
82
  const appuser = this.prepareAppUser(undefined);
83
-
84
- if(tokens.access_token){
85
- setTimeout(async () => {
86
- await this.refreshSystemToken();
87
- }, nextrefresh * 1000);
88
- }
83
+
84
+ // if (tokens.access_token) {
85
+ // setTimeout(async () => {
86
+ // await this.refreshSystemToken();
87
+ // }, nextrefresh * 1000);
88
+ // }
89
89
  }
90
90
 
91
91
  prepareAppUser(data: any) {
92
+ console.log("prepareAppUserprepareAppUser");
92
93
  const appuser = new UserContext(this.usermodel, this.permmodel);
93
94
  appuser.setAsStaticUser(
94
95
  '00000000-0000-0000-0000-000000000000',
95
96
  'robot',
96
97
  'Robot',
97
98
  'robot@a.org',
99
+ Base64URL.encodeText('0-0-0')
98
100
  );
99
101
 
100
102
  const tenantId = data?.tenantId ?? 0;
@@ -103,11 +105,7 @@ export class SimpleAppRobotUserService {
103
105
 
104
106
  appuser.setXorg(appuser.generateXorg(tenantId, orgId, branchId));
105
107
  appuser.setUserToken(this.getToken());
106
- // console.log(
107
- // 'return user ' + appuser.getUname(),
108
- // appuser.getXorg(),
109
- // appuser.getUserToken().length,
110
- // );
108
+
111
109
  return appuser;
112
110
  }
113
111
  }
@@ -58,6 +58,9 @@ export class UserContext {
58
58
  protected ssoACL: any = {};
59
59
  protected token: string = '';
60
60
  protected refreshtoken: string = '';
61
+ //guest access token obtain from header 'x-guest-accesstoken', during use x-apikey/x-apisecret
62
+ protected guestToken?: string='';
63
+ protected guestInfo:{uid:string,uname:string,fullname:string,email:string} = {uid:'',uname:'',fullname:'',email:''}
61
64
  protected groups: string[] = [];
62
65
  protected branchCode: string = '';
63
66
  protected branchName: string = '';
@@ -110,6 +113,7 @@ export class UserContext {
110
113
  getOffsetMinute = () => this.offsetMinute;
111
114
  getGroups = () => this.groups;
112
115
  getCurrency = () => this.currency;
116
+ getGuestInfo = () => this.guestInfo;
113
117
  getMoreProps = () => this.moreProps;
114
118
  getRoles = () => this.roles;
115
119
  getModifieds = () => this.modifiedRecords;
@@ -282,10 +286,11 @@ export class UserContext {
282
286
  this.uid = tokeninfo?.sub ?? '';
283
287
  this.email = tokeninfo?.email ?? '';
284
288
  this.uname = tokeninfo?.preferred_username ?? '';
285
- this.fullname = tokeninfo?.name ?? [];
289
+ this.fullname = tokeninfo?.name ?? '';
286
290
  this.ssoACL = tokeninfo?.resource_access ?? [];
287
291
  this.logger.verbose(`set token ${this.uid}`);
288
292
  //read current user from db
293
+ // console.log("Set User token")
289
294
  // console.log("await this.obtainProfileFromDb()")
290
295
  const userinfo = await this.obtainProfileFromDb();
291
296
  this.logger.verbose(userinfo, 'obtainProfileFromDb result');
@@ -696,6 +701,7 @@ export class UserContext {
696
701
  uname: string,
697
702
  name: string,
698
703
  email: string,
704
+ xorg:string
699
705
  ) => {
700
706
  //define token info
701
707
  this.token = '';
@@ -704,9 +710,18 @@ export class UserContext {
704
710
  this.uname = uname;
705
711
  this.fullname = name;
706
712
  this.ssoACL = '';
707
- this.roles = [Role.Everyone, Role.User];
713
+ this.roles = [Role.Everyone, Role.User,Role.SuperUser];
714
+ this.setXorg(xorg);
708
715
  };
709
716
 
717
+ setGuestToken(tokenstr:string){
718
+ const tokeninfo = jwt.decode(tokenstr);
719
+ this.guestInfo.uid = tokeninfo?.sub ?? '';
720
+ this.guestInfo.email = tokeninfo?.email ?? '';
721
+ this.guestInfo.uname = tokeninfo?.preferred_username ?? '';
722
+ this.guestInfo.fullname = tokeninfo?.name ?? '';
723
+
724
+ }
710
725
  /**
711
726
  * define additional properties from user into moreProps
712
727
  */
@@ -628,7 +628,7 @@ export class SimpleAppService<T extends { _id?: string; __v?: number }> {
628
628
  // }
629
629
  // this.logger.debug('warn2');
630
630
  if (this.hooks.beforeUpdate)
631
- await this.hooks.beforeUpdate(appuser, id, existingdata,data);
631
+ await this.hooks.beforeUpdate(appuser, id, existingdata, data);
632
632
 
633
633
  const dbsession = appuser.getDBSession();
634
634
  if (dbsession && !dbsession.inTransaction()) {
@@ -691,7 +691,7 @@ export class SimpleAppService<T extends { _id?: string; __v?: number }> {
691
691
  data.__v = existingdata.__v + 1;
692
692
 
693
693
  if (this.hooks.beforeUpdate)
694
- await this.hooks.beforeUpdate(appuser, id,existingdata, data);
694
+ await this.hooks.beforeUpdate(appuser, id, existingdata, data);
695
695
 
696
696
  const dbsession = appuser.getDBSession();
697
697
  if (dbsession && !dbsession.inTransaction()) {
@@ -1058,7 +1058,8 @@ export class SimpleAppService<T extends { _id?: string; __v?: number }> {
1058
1058
  collections.forEach((tokey: string) => {
1059
1059
  const toarr = tokey.split('.');
1060
1060
  const to = toarr[0];
1061
- const foreignField = toarr[1] ?? '_id';
1061
+ toarr.splice(0,1)
1062
+ const foreignField = toarr.join('.') ?? '_id';
1062
1063
  pipelines.push({
1063
1064
  $lookup: {
1064
1065
  from: to,
@@ -1083,7 +1084,7 @@ export class SimpleAppService<T extends { _id?: string; __v?: number }> {
1083
1084
  });
1084
1085
  pipelines.push({ $sort: sortobj });
1085
1086
  }
1086
- // console.log('pipelinespipelinespipelines', pipelines);
1087
+ this.logger.warn( pipelines,'pipelinespipelinespipelines',);
1087
1088
 
1088
1089
  return pipelines;
1089
1090
  }
@@ -39,7 +39,7 @@ export const getDocumentApi = (documentName: string): any => {
39
39
  const config = getAxiosConfig()
40
40
  const docsOpenapi: any = {
41
41
  <% for(let i=0;i<it.modules.length; i++){ %>
42
- '<%=it.modules[i].docname.toLowerCase()%>': new o.<%=it.modules[i].doctype.toUpperCase()%>Api(config),
42
+ '<%=it.modules[i].docname.toLowerCase()%>': new o.<%=it.modules[i].docname.toUpperCase()%>Api(config),
43
43
  <%}%>
44
44
  };
45
45
 
@@ -24,7 +24,7 @@ type crudType = {
24
24
  runDelete: Function;
25
25
  runSearch: Function;
26
26
  runDefault:Function;
27
- runFullTextSearch:Function;
27
+ runFullTextSearch?:Function;
28
28
  };
29
29
  export class SimpleAppClient<
30
30
  TData extends { _id?: string,created?:string },