Skip to content

Commit

Permalink
feat: public user profiles, caching, discover doctors, organizations
Browse files Browse the repository at this point in the history
  • Loading branch information
Ruslan Garifullin committed Apr 22, 2023
1 parent 0c32983 commit 8e385cd
Show file tree
Hide file tree
Showing 37 changed files with 2,124 additions and 82 deletions.
2 changes: 1 addition & 1 deletion misc/env/dev-local.env
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ MINIO_SECRET_KEY=iegi3aiNgie5meiQu3du2Peicho2vae8Eoke
MINIO_ROOT_PASSWORD=iegi3aiNgie5meiQu3du2Peicho2vae8Eoke
WEB3_JSON_RPC_URL=https://polygon-mumbai.g.alchemy.com/v2/WDJFOaajRdjZqM82RoLrLLcq0Hhivrlh
WEB3_PRIVATE_KEY=c31e3902ce32b1f25629c8d4d5e6118a63815515cb23d95d0a0c1888f5a842d0
WEB3_ACCOUNT_MANAGER_ADDRESS=0xa27b0471E0332a6E5FD0ae3414E6cb1E6096725C
WEB3_ACCOUNT_MANAGER_ADDRESS=0x59e03D69F98D0Fa7Ba9e025C933b6CC0aB306177
2 changes: 1 addition & 1 deletion misc/env/dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ MINIO_SECRET_KEY=iegi3aiNgie5meiQu3du2Peicho2vae8Eoke
MINIO_ROOT_PASSWORD=iegi3aiNgie5meiQu3du2Peicho2vae8Eoke
WEB3_JSON_RPC_URL=https://polygon-mumbai.g.alchemy.com/v2/WDJFOaajRdjZqM82RoLrLLcq0Hhivrlh
WEB3_PRIVATE_KEY=c31e3902ce32b1f25629c8d4d5e6118a63815515cb23d95d0a0c1888f5a842d0
WEB3_ACCOUNT_MANAGER_ADDRESS=0xa27b0471E0332a6E5FD0ae3414E6cb1E6096725C
WEB3_ACCOUNT_MANAGER_ADDRESS=0x59e03D69F98D0Fa7Ba9e025C933b6CC0aB306177
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"multer": "^1.4.5-lts.1",
"node-machine-id": "^1.1.12",
"passport": "^0.6.0",
"passport-dapp-web3": "^1.0.4",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.10.0",
Expand Down
14 changes: 13 additions & 1 deletion packages/js/shared/db-common/src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { ApiProperty } from "@nestjs/swagger";
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { LocalDateTransformer } from "../transformers/local-date.transformer";
import { FileEntity } from "./file.entity";
import type { UserOrganizationDetailsDto, UserPublicProfileDto } from "@hdapp/shared/web2-common/dto/user";
import type { EmailAddress, Web3Address } from "@hdapp/shared/web2-common/types";

@Entity()
export class UserEntity {
@PrimaryGeneratedColumn()
Expand Down Expand Up @@ -46,6 +46,18 @@ export class UserEntity {
@ApiProperty({ description: "True if user is a administrator" })
hasAdministratorCapabilities: boolean;

@Column({ default: false })
@ApiProperty({ description: "True if user is an organization" })
hasOrganizationCapabilities: boolean;

@Column({ type: "json" })
@ApiProperty({ description: "Organization details" })
organizationDetails: UserOrganizationDetailsDto | null;

@Column({ type: "json" })
@ApiProperty({ description: "Public profile provided by the user" })
publicProfile: UserPublicProfileDto | null;

@Column({ default: false })
@ApiProperty({ description: "True if user has verified their email" })
hasVerifiedEmail: boolean;
Expand Down
13 changes: 9 additions & 4 deletions packages/js/shared/web2-common/src/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,29 @@ export namespace endpoints {
revoke_jwt: "/api/auth/revoke",
refresh_jwt: "/api/auth/refresh",
verify_email: "/api/auth/verify/:verifyToken"
};
} as const;

export const file = {
download: "/api/media/download/:id/:name",
upload: "/api/media/upload"
};
} as const;

export const users = {
create_one: "/api/users",
find_paged: "/api/users",
find_public_profiles_paged: "/api/users/public",
get_filters: "/api/users/public/filters",
find_by_id: "/api/users/by_id/:id",
patch_by_id: "/api/users/by_id/:id",
find_by_web3_address: "/api/users/by_web3_address/:address",
find_public_profile_by_web3_address: "/api/users/by_web3_address/:address/public_profile",
get_current: "/api/users/current",
patch_current: "/api/users/current",
patch_public_profile_current: "/api/users/web3/public_profile",
verify_by_id: "/api/users/by_id/:id/verify"
};
} as const;

export const statistics = {
get: "/api/statistics"
};
} as const;
}
41 changes: 40 additions & 1 deletion packages/js/shared/web2-common/src/api/services/users.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { UpdateUserDto } from "@hdapp/shared/web2-common/dto";
import { UserFiltersDto } from "../../dto/user-filters.dto";
import { UserDto } from "../../dto/user.dto";
import { PublicUserDto, PublicUserSearchFiltersDto, UserDto } from "../../dto/user.dto";
import { UserPublicProfileDto } from "../../dto/user/public-profile.dto";
import { UpdatePublicProfileDto } from "../../dto/user/update-public-profile.dto";
import { PagedResponse } from "../../types/paged-response.type";
import { Web3Address } from "../../types/web3-address.type";
import { endpoints } from "../endpoints";
Expand Down Expand Up @@ -53,4 +55,41 @@ export const UsersService = new (class {
method: "POST"
});
}

getPublicProfileFilters(): Promise<PublicUserSearchFiltersDto> {
return http.request({
method: "GET",
url: endpoints.users.get_filters,
type: PublicUserSearchFiltersDto
});
}

findPublicProfilesPaged(filters: UserFiltersDto, options?: FindPagedOptions): Promise<PagedResponse<PublicUserDto>> {
return http.request({
url: endpoints.users.find_public_profiles_paged,
params: {
filters: btoa(JSON.stringify(filters)),
from_id: options?.from_id,
sort_by: options?.sort_by,
sort_desc: !!options?.sort_desc,
},
type: PagedResponse(PublicUserDto)
});
}

findPublicProfileByWeb3Address(address: string | Web3Address): Promise<PublicUserDto> {
return http.request({
url: endpoints.users.find_public_profile_by_web3_address
.replace(":address", address),
type: PublicUserDto
});
}

async updatePublicProfile(data: UpdatePublicProfileDto): Promise<void> {
await http.request({
data,
url: endpoints.users.patch_public_profile_current,
method: "PATCH"
});
}
});
1 change: 1 addition & 0 deletions packages/js/shared/web2-common/src/dto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "./login-user.dto";
export * from "./token.dto";
export * from "./user-filters.dto";
export * from "./user.dto";
export * as user from "./user";
3 changes: 3 additions & 0 deletions packages/js/shared/web2-common/src/dto/user-filters.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export const UserFiltersDto = partial({
is_verified_doctor: boolean,
is_banned: boolean,
has_web3_address: boolean,
areas_of_focus: string,
location: string,
organization_id: string,
});

export type UserFiltersDto = TypeOf<typeof UserFiltersDto>;
24 changes: 23 additions & 1 deletion packages/js/shared/web2-common/src/dto/user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { array, boolean, number, partial, string, type, TypeOf } from "io-ts";
import { array, boolean, number, partial, record, string, tuple, type, TypeOf } from "io-ts";
import { orNull } from "../io-ts-utils/or-null";
import { orUndefined } from "../io-ts-utils/or-undefined";
import { emailAddressType } from "../types/email-address.type";
import { web3AddressType } from "../types/web3-address.type";
import { FileDto } from "./file.dto";
import { UserOrganizationDetailsDto } from "./user/organization-details.dto";
import { UserPublicProfileDto } from "./user/public-profile.dto";

export const UserDto = type({
id: number,
Expand All @@ -16,13 +18,31 @@ export const UserDto = type({
has_doctor_capabilities: boolean,
has_moderator_capabilities: boolean,
has_administrator_capabilities: boolean,
has_organization_capabilities: boolean,
has_verified_email: boolean,
is_verified_doctor: boolean,
is_banned: boolean,
organization_details: orUndefined(UserOrganizationDetailsDto),
public_profile: orUndefined(UserPublicProfileDto),
});

export type UserDto = TypeOf<typeof UserDto>;

export const PublicUserDto = type({
web3_address: orNull(web3AddressType),
public_profile: orUndefined(UserPublicProfileDto),
});

export type PublicUserDto = TypeOf<typeof PublicUserDto>;

export const PublicUserSearchFiltersDto = type({
areas_of_focus: array(type({ value: string, count: number })),
locations: array(type({ value: string, count: number })),
organizations: array(type({ id: string, name: string, count: number })),
});

export type PublicUserSearchFiltersDto = TypeOf<typeof PublicUserSearchFiltersDto>;

export const CreateUserDto = type({
email: emailAddressType,
full_name: string,
Expand All @@ -48,6 +68,8 @@ export const UpdateUserDto = partial({
has_verified_email: boolean,
is_verified_doctor: boolean,
is_banned: boolean,
is_organization: boolean,
organization_details: UserOrganizationDetailsDto,
});

export type UpdateUserDto = TypeOf<typeof UpdateUserDto>;
Expand Down
3 changes: 3 additions & 0 deletions packages/js/shared/web2-common/src/dto/user/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./organization-details.dto";
export * from "./public-profile.dto";
export * from "./update-public-profile.dto";
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { array, partial, string, TypeOf } from "io-ts";

export const UserOrganizationDetailsDto = partial({
email_domains: array(string),
website: string,
});

export type UserOrganizationDetailsDto = TypeOf<typeof UserOrganizationDetailsDto>;
15 changes: 15 additions & 0 deletions packages/js/shared/web2-common/src/dto/user/public-profile.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { array, partial, record, string, type, TypeOf } from "io-ts";

export const UserPublicProfileDto = partial({
full_name: string,
avatar: string,
organization_id: string,
location: string,
languages: array(string),
areasOfFocus: string,
specialty: string,
timetable: array(array(string)),
socials: array(type({ name: string, value: string }))
});

export type UserPublicProfileDto = TypeOf<typeof UserPublicProfileDto>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type, string, TypeOf, any } from "io-ts";
import { web3AddressType } from "../../types/web3-address.type";
import { UserPublicProfileDto } from "./public-profile.dto";

export const UpdatePublicProfileDto = type({
address: web3AddressType,
message: any,
signed: string,
public_profile: UserPublicProfileDto
});

export type UpdatePublicProfileDto = TypeOf<typeof UpdatePublicProfileDto>;
11 changes: 10 additions & 1 deletion packages/js/web2/backend/src/adapters/user.adapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CreateUserEntity } from "@hdapp/shared/db-common/entities";
import { CreateUserDto, UpdateUserDto, UserDto } from "@hdapp/shared/web2-common/dto";
import { CreateUserDto, PublicUserDto, UpdateUserDto, UserDto } from "@hdapp/shared/web2-common/dto";
import { LocalDate } from "@js-joda/core";
import { DeepPartial } from "typeorm";
import { UserFullEntity } from "../entities/user-full.entity";
Expand All @@ -19,14 +19,23 @@ export const UserAdapter = new (class {
has_administrator_capabilities: entity.hasAdministratorCapabilities,
has_doctor_capabilities: entity.hasDoctorCapabilities,
has_moderator_capabilities: entity.hasModeratorCapabilities,
has_organization_capabilities: entity.hasOrganizationCapabilities,
has_verified_email: entity.hasVerifiedEmail,
is_banned: entity.isBanned,
is_verified_doctor: entity.isVerifiedDoctor,
organization_details: entity.organizationDetails ?? undefined,
public_profile: entity.publicProfile ?? undefined,
medical_organization_name: entity.hasDoctorCapabilities
? entity.medicalOrganizationName ?? undefined
: undefined,
};
}
transformToPublicDto(entity: UserFullEntity): PublicUserDto {
return {
web3_address: entity.web3Address,
public_profile: entity.publicProfile ?? undefined,
};
}

transformCreateDtoToEntity(entity: CreateUserDto): CreateUserEntity {
return {
Expand Down
3 changes: 2 additions & 1 deletion packages/js/web2/backend/src/api/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { AuthService } from "./auth.service";
import { JwtStrategy } from "./jwt.strategy";
import { LocalStrategy } from "./local.strategy";
import { MailService } from "./mail.service";
import { Web3Strategy } from "./web3.strategy";

@Module({
imports: [
UsersModule,
JwtModule.register({ secret: process.env.JWT_SECRET }),
PassportModule,
],
providers: [AuthService, JwtStrategy, LocalStrategy, MailService, RedisService],
providers: [AuthService, JwtStrategy, LocalStrategy, MailService, RedisService, Web3Strategy],
controllers: [AuthController],
})
export class AuthModule {}
20 changes: 14 additions & 6 deletions packages/js/web2/backend/src/api/auth/mail.service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UserEntity } from "@hdapp/shared/db-common/entities/user.entity";
import { EmailAddress, Web3Address } from "@hdapp/shared/web2-common/types";
import { ErrorClass, Logger } from "@hdapp/shared/web2-common/utils";
import { Injectable } from "@nestjs/common";
Expand All @@ -14,21 +15,28 @@ export class MailService {
) { }

async sendWalletInfo(
userEmail: EmailAddress,
user: UserEntity,
walletPublicKey: Web3Address,
walletPrivateKey: string,
walletMnemonic: string,
walletMnemonic: string
) {
try {
const userB64 = Buffer.from(
JSON.stringify({
full_name: user.fullName,
birth_date: user.birthDate
}),
"utf-8"
).toString("base64");
await this.mailer.sendMail({
to: userEmail, // list of receivers
to: user.email, // list of receivers
subject: "Your HDAPP WalletInfo", // Subject line
text: `Thanks for creating an account on HDAPP!\r\nIn order to sign in into your new account, press the following link: https://hdapp.ruslang.xyz/app?privateKey=${walletPrivateKey}\r\n\r\nYour wallet details:\r\nPublic key: ${walletPublicKey}\r\nPrivate key: ${walletPrivateKey}\r\nMnemonic: ${walletMnemonic}`, // plaintext body
text: `Hello, ${user.fullName}!\r\n\r\nThanks for creating an account on HDAPP!\r\nIn order to sign in into your new account, press the following link: https://hdapp.ruslang.xyz/app?privateKey=${walletPrivateKey}&user=${userB64}\r\n\r\nYour wallet details:\r\nPublic key: ${walletPublicKey}\r\nPrivate key: ${walletPrivateKey}\r\nMnemonic: ${walletMnemonic}`, // plaintext body
});

debug("Sent wallet info e-mail.", { email: userEmail });
debug("Sent wallet info e-mail.", { email: user.email });
} catch (err) {
throw new SendMailError({ err, email: userEmail }, "Could not send e-mail with wallet info");
throw new SendMailError({ err, email: user.email }, "Could not send e-mail with wallet info");
}
}

Expand Down
27 changes: 27 additions & 0 deletions packages/js/web2/backend/src/api/auth/web3.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { UserEntity } from "@hdapp/shared/db-common/entities";
import { getRightOrFail } from "@hdapp/shared/web2-common/io-ts-utils/get-right";
import { web3AddressType } from "@hdapp/shared/web2-common/types";
import { FriendlyError } from "@hdapp/shared/web2-common/utils";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import Strategy from "passport-dapp-web3";
import { UsersService } from "../users/users.service";

@Injectable()
export class Web3Strategy extends PassportStrategy(Strategy) {
constructor(private users: UsersService) {
super({ session: false });
}

async validate(address: string): Promise<UserEntity> {
try {
const web3Address = getRightOrFail(web3AddressType.decode(address));
return await this.users.findOneByWeb3Address(web3Address);
} catch (e) {
if (e instanceof FriendlyError)
throw new UnauthorizedException({ message: e.message });

throw e;
}
}
}
Loading

0 comments on commit 8e385cd

Please sign in to comment.