Skip to content

Commit d12a2e0

Browse files
authored
Merge pull request #2270 from IDEMSInternational/feat/user-actions
Feat: user import action
2 parents 700b46e + 6c193a1 commit d12a2e0

File tree

16 files changed

+97
-37
lines changed

16 files changed

+97
-37
lines changed

documentation/docs/developers/server-development.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ yarn workspace api test:e2e -t app_user
6363
Requires [docker desktop](https://www.docker.com/products/docker-desktop/) installed locally
6464

6565
1. Configure .env variables as per `packages\server\README.md`
66-
Ensure `API_BASE_PATH="/api/"` to allow running as part of full stack
66+
Ensure `API_BASE_PATH="/api"` to allow running as part of full stack
6767

6868
2. Build api
6969
```bash

packages/api/.env.example

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ WEBSOCKET_PORT=3000
22
API_PORT=3000
33
# If running within docker containers this should be set to reverse proxy, e.g. /api/
44
# If serving locally this should be left blank
5-
# REMEMBER TO REVERT TO /api/ WHEN BUILDING DOCKER IMAGE
6-
API_BASE_PATH="/api/"
5+
API_BASE_PATH=""
76
DB_HOST=localhost
87
DB_PORT=5432
98

packages/api/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ RUN yarn build
3232
# Add secondary set of production-only node_modules to dist (not sure why yarn berry needs re-init but seems to be the case)
3333
# NOTE - if updating to yarn 4 will require additional plugin import line
3434
RUN yarn set version 3.3.1 && \
35-
# yarn plugin import workspace-tools && \
35+
yarn plugin import workspace-tools && \
3636
yarn workspaces focus api --production
3737

3838
### STAGE 2: Serve Dashboard ###

packages/api/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "api",
3-
"version": "1.4.3",
3+
"version": "1.4.4",
44
"scripts": {
55
"prebuild": "rimraf dist",
66
"build": "nest build",

packages/api/spec-export.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"parameters": [
99
{
1010
"name": "x-deployment-db-name",
11+
"required": true,
1112
"in": "header",
1213
"description": "Name of db for deployment to populate",
1314
"schema": {
@@ -520,7 +521,7 @@
520521
"info": {
521522
"title": "IDEMS Apps API",
522523
"description": "App-Server Communication",
523-
"version": "1.3.0",
524+
"version": "1.4.4",
524525
"contact": {}
525526
},
526527
"tags": [

packages/api/src/endpoints/app_users/app_user.controller.ts

-10
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,6 @@ export class AppUsersController {
2222
return this.appUsersService.model.findAll();
2323
}
2424

25-
// @Get(":app_user_id")
26-
// findOne(@Param("app_user_id") app_user_id: string): Promise<AppUser> {
27-
// return this.appUsersService.findOne(app_user_id);
28-
// }
29-
30-
// @Delete(":id")
31-
// remove(@Param("id") id: string): Promise<void> {
32-
// return this.appUsersService.remove(id);
33-
// }
34-
3525
@Get(":app_user_id")
3626
@ApiParam({ name: "app_user_id", type: String })
3727
@ApiOperation({ summary: "Get user profile" })

packages/api/src/environment/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,16 @@ function loadEnv() {
2727
const { NODE_ENV } = process.env;
2828

2929
let envFilePath = resolve(__dirname, "../../.env");
30+
// Hack - update path if running from compiled dist folder
31+
if (envFilePath.includes("dist")) {
32+
envFilePath = resolve(envFilePath, "../../.env");
33+
}
3034
if (NODE_ENV === "test") {
3135
envFilePath = resolve(__dirname, "../../test/.test.env");
3236
}
3337
// In production env vars are passed from docker container instead of local file
3438
if (!existsSync(envFilePath)) {
39+
console.log(envFilePath);
3540
console.warn("Env file does not exist, using local env variables", Object.keys(process.env));
3641
return;
3742
}

packages/api/src/main.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ async function bootstrap() {
2020
.setDescription("App-Server Communication")
2121
.setVersion(version)
2222
.addTag("api")
23-
// .setBasePath("/api")
2423
// Fix swagger redirection issue
2524
// https://github.com/nestjs/swagger/issues/448
2625
// https://stackoverflow.com/questions/63954037/nestjs-swagger-missing-base-path
27-
.addServer(environment.API_BASE_PATH ? `/${environment.API_BASE_PATH}` : "")
26+
// Ensure api base path populated as /api (not /api/ or similar)
27+
.addServer(environment.API_BASE_PATH ? `/${environment.API_BASE_PATH.replace(/\//g, "")}` : "")
2828
.build();
2929
const document = SwaggerModule.createDocument(app, config, { ignoreGlobalPrefix: true });
3030
// add export for docs (https://github.com/nestjs/swagger/issues/158)

packages/data-models/flowTypes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ export namespace FlowTypes {
416416
"toggle_field",
417417
"track_event",
418418
"trigger_actions",
419+
"user",
419420
] as const;
420421

421422
export interface TemplateRowAction {

packages/server/README.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ You should be able to access the dashboard at http://localhost (will redirect)
5252
http://localhost/dbadmin/
5353

5454
Login details
55-
```
55+
```yml
5656
System: PostgreSQL
5757
Server: db
5858
Username: (POSTGRES_USER in .env)
@@ -79,7 +79,7 @@ http://localhost/dashboard/
7979
A new database will need to be created to allow access for metabase. This should be automatically configured in [docker/config/db/init.sh](./docker/config/db/init.sh), however if these steps fail they can be run manually (currently a bit temperamental - TODO ISSUE - will know if successful if can see a database created that matches the provided $MB_DB_DBNAME).
8080

8181
Manual SQL can be executed from the Adminer DB interface, e.g. using the example config:
82-
```
82+
```sql
8383
CREATE USER metabase WITH PASSWORD 'metabase';
8484
CREATE DATABASE metabase;
8585
GRANT ALL PRIVILEGES ON DATABASE metabase to metabase;
@@ -88,13 +88,13 @@ GRANT ALL PRIVILEGES ON DATABASE metabase to metabase;
8888
Once running complete configuration from within the dashboard app.
8989

9090
Create a user account using preferred credentials and retain securely elsewhere. As a default when running test servers in docker the following credentials are used
91-
```
91+
```yml
9292
9393
password: demo1234
9494
```
9595

9696
Configure connection to the same database created by the api:
97-
```
97+
```yml
9898
Database type: PostgreSQL
9999
Name: (any)
100100
Host: db
@@ -111,7 +111,7 @@ You should then see the main dashboard page
111111
http://localhost/analytics/
112112

113113
An initial set of configuration screens should walk through the process of setting up users and a database connection. If connection fails or additional users need to be created the database can be accessed via the same Adminer `/dbadmin` path and `root` user credentials
114-
```
114+
```yml
115115
System: MySQL
116116
Server: analytics_db
117117
Username: root
@@ -120,13 +120,13 @@ Database: (blank)
120120
```
121121
You will be asked to create a super user login and password. This information should be stored safely.
122122
As a default when running test servers in docker the following credentials are used
123-
```
123+
```yml
124124
125125
password: demo1234
126126
```
127127

128128
To enable data collection from the frontend application follow instructions in the dashboard. You may need to record the Matomo Url and site ID as seen on the initial page, e.g.
129-
```
129+
```yml
130130
Matomo URL: http://localhost/
131131
Your site ID: 1
132132
```
@@ -150,7 +150,7 @@ http://localhost/triggers/
150150
This uses the tool [N8N](https://n8n.io/) to provide access to automation and triggers based on events.
151151
Currently the tool only supports single user authentication provided by username and password provided in the `.env` file
152152

153-
```
153+
```yml
154154
Username: ($N8N_BASIC_AUTH_USER in .env)
155155
Password: ($N8N_BASIC_AUTH_PASSWORD in .env)
156156
```

packages/server/docker/docker-compose.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ services:
4646
# context: ../../api
4747
# dockerfile: Dockerfile
4848
# target: prod-env
49-
image: idems/apps-api:1.4.3
49+
image: idems/apps-api:1.4.4
5050
env_file:
5151
- ../../api/.env
5252
environment:
@@ -89,7 +89,7 @@ services:
8989
# Postgres DB admin (pgadmin)
9090
####################################################################
9191
pgadmin:
92-
image: dpage/pgadmin4:6
92+
image: dpage/pgadmin4:8
9393
container_name: plh_pg_admin
9494
restart: unless-stopped
9595
env_file:

src/app/shared/components/template/components/select-text/select-text.component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class SelectTextComponent
3737

3838
getParams() {
3939
this.placeholder = getStringParamFromTemplateRow(this._row, "placeholder", "");
40-
this.maxLength = getNumberParamFromTemplateRow(this._row, "max_length", 30);
40+
this.maxLength = getNumberParamFromTemplateRow(this._row, "max_length", -1);
4141
this.textAlign = getStringParamFromTemplateRow(this._row, "text_align", "center");
4242
this.style = getStringParamFromTemplateRow(this._row, "style", null);
4343
this.isNumberInput = getBooleanParamFromTemplateRow(this._row, "number_input", false);

src/app/shared/components/template/components/text-box/text-box.component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class TmplTextBoxComponent extends TemplateBaseComponent implements OnIni
2929

3030
getParams() {
3131
this.placeholder = getStringParamFromTemplateRow(this._row, "placeholder", "");
32-
this.maxLength = getNumberParamFromTemplateRow(this._row, "max_length", 30);
32+
this.maxLength = getNumberParamFromTemplateRow(this._row, "max_length", -1);
3333
this.textAlign = getStringParamFromTemplateRow(this._row, "text_align", "center");
3434
this.style = getStringParamFromTemplateRow(this._row, "style", null);
3535
this.isNumberInput = getBooleanParamFromTemplateRow(this._row, "number_input", false);

src/app/shared/components/template/services/template-field.service.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,31 @@ import { booleanStringToBoolean } from "src/app/shared/utils";
77
import { TemplateTranslateService } from "./template-translate.service";
88
import { AsyncServiceBase } from "src/app/shared/services/asyncService.base";
99
import { TemplateActionRegistry } from "./instance/template-action.registry";
10+
import { AppConfigService } from "src/app/shared/services/app-config/app-config.service";
1011

1112
@Injectable({ providedIn: "root" })
1213
export class TemplateFieldService extends AsyncServiceBase {
1314
globals: { [name: string]: FlowTypes.GlobalRow } = {};
1415

16+
/** App config prefix used */
17+
public prefix: string;
18+
1519
constructor(
1620
private localStorageService: LocalStorageService,
1721
private dbService: DbService,
1822
private translateService: TemplateTranslateService,
19-
private templateActionRegistry: TemplateActionRegistry
23+
private templateActionRegistry: TemplateActionRegistry,
24+
private appConfigService: AppConfigService
2025
) {
2126
super("TemplateField");
2227
this.registerInitFunction(this.initialise);
2328
this.registerTemplateActionHandlers();
29+
this.prefix = appConfigService.APP_CONFIG.FIELD_PREFIX;
2430
}
2531

2632
private async initialise() {
2733
await this.ensureAsyncServicesReady([this.dbService, this.translateService]);
28-
this.ensureSyncServicesReady([this.localStorageService]);
34+
this.ensureSyncServicesReady([this.localStorageService, this.appConfigService]);
2935
}
3036

3137
private registerTemplateActionHandlers() {
@@ -49,7 +55,7 @@ export class TemplateFieldService extends AsyncServiceBase {
4955
* TODO - ideally showWarnings should be linked to some sort of debug mode
5056
*/
5157
public getField(key: string, showWarnings = true) {
52-
let val: any = this.localStorageService.getString("rp-contact-field." + key);
58+
let val: any = this.localStorageService.getString(`${this.prefix}.${key}`);
5359
// provide a fallback if the target variable does not exist in local storage
5460
if (val === null && showWarnings) {
5561
// console.warn("field value not found for key:", key);
@@ -80,7 +86,7 @@ export class TemplateFieldService extends AsyncServiceBase {
8086
}
8187
}
8288
// write to local storage - this will cast to string
83-
this.localStorageService.setString("rp-contact-field." + key, value);
89+
this.localStorageService.setString(`${this.prefix}.${key}`, value);
8490

8591
// write to db - note this can handle more data formats but only string/number will be available to queries
8692
if (typeof value === "boolean") value = "value";

src/app/shared/services/server/interceptors.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ let { db_name, endpoint: API_ENDPOINT } = environment.deploymentConfig.api;
1414

1515
// Override development credentials when running locally
1616
if (!environment.production) {
17+
// Docker endpoint. Replace :3000 with /api if running standalone api
1718
API_ENDPOINT = "http://localhost:3000";
19+
db_name = "dev";
1820
}
1921

2022
/** Handle updating urls intended for api server */

src/app/shared/services/userMeta/userMeta.service.ts

+59-3
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
1+
import { HttpClient } from "@angular/common/http";
12
import { Injectable } from "@angular/core";
23
import { Device } from "@capacitor/device";
4+
import { firstValueFrom } from "rxjs";
5+
36
import { AsyncServiceBase } from "../asyncService.base";
47
import { DbService } from "../db/db.service";
8+
import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry";
9+
import { TemplateFieldService } from "../../components/template/services/template-field.service";
510

611
@Injectable({ providedIn: "root" })
712
export class UserMetaService extends AsyncServiceBase {
813
/** keep an in-memory copy of user to provide synchronously */
914
public userMeta: IUserMeta;
10-
constructor(private dbService: DbService) {
11-
super("UsesrMetaService");
15+
constructor(
16+
private dbService: DbService,
17+
private templateActionRegistry: TemplateActionRegistry,
18+
private http: HttpClient,
19+
private fieldService: TemplateFieldService
20+
) {
21+
super("UserMetaService");
1222
this.registerInitFunction(this.initialise);
1323
}
1424

1525
/** When first initialising ensure a default profile created and any newer defaults are merged with older user profiles */
1626
private async initialise() {
17-
await this.ensureAsyncServicesReady([this.dbService]);
27+
await this.ensureAsyncServicesReady([this.dbService, this.fieldService]);
28+
this.registerUserActions();
1829
const userMetaValues = await this.dbService.table<IUserMetaEntry>("user_meta").toArray();
1930
const userMeta: IUserMeta = USER_DEFAULTS;
2031
userMetaValues.forEach((v) => {
@@ -27,6 +38,8 @@ export class UserMetaService extends AsyncServiceBase {
2738
}
2839
userMeta.uuid = uuid;
2940
this.userMeta = userMeta;
41+
// populate user id contact field
42+
this.fieldService.setField("_app_user_id", uuid);
3043
}
3144

3245
getUserMeta(key: keyof IUserMeta) {
@@ -38,6 +51,49 @@ export class UserMetaService extends AsyncServiceBase {
3851
await this.dbService.table<IUserMetaEntry>("user_meta").bulkPut(entries as any);
3952
this.userMeta = { ...this.userMeta, ...meta };
4053
}
54+
55+
/** Import existing user contact fields and replace current user */
56+
private async importUserFields(id: string) {
57+
try {
58+
// TODO - get type-safe return types using openapi http client
59+
const profile = await firstValueFrom(
60+
this.http.get(`/app_users/${id}`, { responseType: "json" })
61+
);
62+
if (!profile) {
63+
console.error("[User Import] not found:" + id);
64+
return;
65+
}
66+
const { contact_fields } = profile as any;
67+
for (const [key, value] of Object.entries(contact_fields)) {
68+
const fieldName = key.replace(`${this.fieldService.prefix}.`, "");
69+
// TODO - handle special contact fields as required (e.g. _app_skin, _app_theme)
70+
if (!fieldName.startsWith("_")) {
71+
await this.fieldService.setField(fieldName, value as string);
72+
}
73+
}
74+
} catch (error) {
75+
console.error("[User Import] failed", error);
76+
}
77+
}
78+
79+
private registerUserActions() {
80+
const childActions = {
81+
import: this.importUserFields.bind(this),
82+
};
83+
const childActionNames = Object.keys(childActions).join(",");
84+
this.templateActionRegistry.register({
85+
user: async ({ args }) => {
86+
const [actionId, ...childArgs] = args;
87+
if (!childActions[actionId]) {
88+
console.error(
89+
`[${actionId}] user action not defined. Available actions:\n${childActionNames}`
90+
);
91+
return;
92+
}
93+
return childActions[actionId](childArgs);
94+
},
95+
});
96+
}
4197
}
4298

4399
interface IUserMetaEntry {

0 commit comments

Comments
 (0)