Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat!: Auth user db sync #2666

Merged
merged 8 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "api",
"version": "1.5.1",
"version": "1.6.0",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
Expand Down
35 changes: 34 additions & 1 deletion packages/api/spec-export.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,39 @@
]
}
},
"/auth_users/{auth_user_id}": {
"get": {
"operationId": "AuthUsersController_findOne",
"summary": "Get auth user profile",
"parameters": [
{
"name": "auth_user_id",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "x-deployment-db-name",
"in": "header",
"description": "Name of db for deployment to populate",
"schema": {
"type": "string",
"default": "plh"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Auth Users"
]
}
},
"/contact_fields": {
"get": {
"operationId": "ContactFieldController_findAll",
Expand Down Expand Up @@ -339,7 +372,7 @@
"info": {
"title": "IDEMS Apps API",
"description": "App-Server Communication",
"version": "1.4.4",
"version": "1.6.0",
"contact": {}
},
"tags": [
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { DeploymentModule } from "./modules";
}),
DefaultModule,
Endpoints.AppUsersModule,
Endpoints.AuthUsersModule,
Endpoints.ContactFieldsModule,
Endpoints.AppFeedbackModule,
Endpoints.AppNotificationInteractionModule,
Expand Down
9 changes: 5 additions & 4 deletions packages/api/src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ export class DBInstance {
database: this.dbName,
// disable verbose migration logs in test
logging: process.env.NODE_ENV === "test" ? false : true,
// Fix SSL issue
// Enable ssl mode when running on production
// https://dev.to/rodjosh/connectionerror-sequelizeconnectionerror-no-pghbaconf-entry-for-host-in-heroku-postgresql-using-sequelize-3icj
dialectOptions: {
ssl: {
// https://stackoverflow.com/a/61411969
dialectOptions: environment.production ? {
ssl: {
require: true,
rejectUnauthorized: false,
},
},
} : {},
});
await this.runMigrations(migrationClient);
await migrationClient.close();
Expand Down
13 changes: 13 additions & 0 deletions packages/api/src/db/migrations/008-auth-user-column.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DataTypes, QueryInterface } from "sequelize";
import { UmzugOptions } from "umzug";

export const up = async (options: UmzugOptions<QueryInterface>) => {
const queryInterface = options.context as QueryInterface;
await queryInterface.addColumn("app_users", "auth_user_id", {
type: DataTypes.STRING,
});
};
export const down = async (options: UmzugOptions<QueryInterface>) => {
const queryInterface = options.context as QueryInterface;
await queryInterface.removeColumn("app_users", "auth_user_id");
};
3 changes: 3 additions & 0 deletions packages/api/src/endpoints/app_users/app_user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export class AppUser extends Model<InferAttributes<AppUser>, InferCreationAttrib
@Column({ allowNull: false })
app_deployment_name: string;

@Column
auth_user_id: string;

@Column({ type: DataType.JSONB })
contact_fields: any;

Expand Down
24 changes: 24 additions & 0 deletions packages/api/src/endpoints/auth_users/auth_user.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Controller, Get, Param, } from "@nestjs/common";
import { ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger";
import { DeploymentHeaders } from "src/modules/deployment.decorators";
import { DeploymentService } from "src/modules/deployment.service";
import { AppUser } from "../app_users/app_user.model";

@ApiTags("Auth Users")
@Controller("auth_users")
/**
* The auth_users endpoint are a thin wrapper around the app_users table to enable
* user lookup by auth_user_id
*/
export class AuthUsersController {
constructor(private readonly deploymentService: DeploymentService) {}

@Get(":auth_user_id")
@ApiParam({ name: "auth_user_id", type: String })
@ApiOperation({ summary: "Get auth user profile" })
@DeploymentHeaders()
findOne(@Param("auth_user_id") auth_user_id: string): Promise<AppUser> {
const model = this.deploymentService.model(AppUser);
return model.findOne({where:{auth_user_id}})
}
}
7 changes: 7 additions & 0 deletions packages/api/src/endpoints/auth_users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AuthUsersController } from './auth_user.controller';
import { Module } from '@nestjs/common';

@Module({
controllers: [AuthUsersController],
})
export class AuthUsersModule {}
1 change: 1 addition & 0 deletions packages/api/src/endpoints/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./app_users";
export * from './auth_users'
export * from "./contact_fields";
export * from "./tables";
export * from "./app_feedback";
Expand Down
2 changes: 1 addition & 1 deletion packages/data-models/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* They are used to store store computed or exported variables and are not user overridable
*/
enum PROTECTED_FIELDS {
APP_AUTH_USER = "app_auth_user",
APP_FIRST_LAUNCH = "app_first_launch",
APP_LANGUAGE = "app_language",
APP_SKIN = "app_skin",
Expand All @@ -14,6 +13,7 @@ enum PROTECTED_FIELDS {
APP_UPDATE_DOWNLOADED = "app_update_downloaded",
APP_USER_ID = "app_user_id",
APP_VERSION = "app_version",
AUTH_USER_ID = "auth_user_id",
CONTENT_VERSION = "content_version",
DEPLOYMENT_NAME = "deployment_name",
FEEDBACK_SELECTED_TEXT = "feedback_selected_text",
Expand Down
2 changes: 1 addition & 1 deletion packages/server/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ services:
# context: ../../api
# dockerfile: Dockerfile
# target: prod-env
image: idems/apps-api:1.5.1
image: idems/apps-api:1.6.0
env_file:
- ../../api/.env
environment:
Expand Down
28 changes: 21 additions & 7 deletions src/app/shared/services/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { IAuthUser } from "./types";
import { filter, firstValueFrom, tap } from "rxjs";
import { TemplateService } from "../../components/template/services/template.service";
import { toObservable } from "@angular/core/rxjs-interop";
import { ServerService } from "../server/server.service";

@Injectable({
providedIn: "root",
Expand All @@ -22,14 +23,20 @@ export class AuthService extends AsyncServiceBase {
private localStorageService: LocalStorageService,
private deploymentService: DeploymentService,
private injector: Injector,
private templateService: TemplateService
private templateService: TemplateService,
private serverService: ServerService
) {
super("Auth");
this.provider = getAuthProvider(this.config.provider);
this.registerInitFunction(this.initialise);
effect(async () => {
const authUser = this.provider.authUser();
await this.checkProfileRestore(authUser);
this.addStorageEntry(authUser);
// perform immediate sync if user signed in to ensure data backed up
if (authUser) {
this.serverService.syncUserData();
}
});
}

Expand All @@ -46,6 +53,14 @@ export class AuthService extends AsyncServiceBase {
}
}

private async checkProfileRestore(authUser?: IAuthUser) {
if (!authUser) return;
const existingUser = this.localStorageService.getProtected("AUTH_USER_ID");
if (existingUser) return;
// no existingUser user, should check if authUser exists on server and prompt restore
// TODO - handle
}

private async enforceLogin(templateName: string) {
// If user already logged in simply return. If providers auto-login during then waiting to verify
// should be included during the provide init method
Expand Down Expand Up @@ -91,13 +106,12 @@ export class AuthService extends AsyncServiceBase {
});
}

/** Keep a subset of auth user info in contact fields for db lookup*/
private addStorageEntry(user?: IAuthUser) {
if (user) {
const { uid } = user;
this.localStorageService.setProtected("APP_AUTH_USER", JSON.stringify({ uid }));
/** Keep id of auth user info in contact fields for db lookup*/
private addStorageEntry(auth_user?: IAuthUser) {
if (auth_user) {
this.localStorageService.setProtected("AUTH_USER_ID", auth_user.uid);
} else {
this.localStorageService.removeProtected("APP_AUTH_USER");
this.localStorageService.removeProtected("AUTH_USER_ID");
}
}
}
4 changes: 4 additions & 0 deletions src/app/shared/services/server/server.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,18 @@ export class ServerService extends SyncServiceBase {
const timestamp = generateTimestamp();
contact_fields[getProtectedFieldName("SERVER_SYNC_LATEST")] = timestamp;

const auth_user_id = this.localStorageService.getProtected("AUTH_USER_ID") || null;

// TODO - get DTO from api (?)
const data = {
auth_user_id,
contact_fields,
app_version: _app_builder_version,
device_info: this.device_info,
app_deployment_name: name,
dynamic_data,
};

console.log("[SERVER] sync data", data);
return new Promise<string>((resolve, reject) => {
this.http
Expand Down
Loading