2021-08-22 22:11:05 +02:00
|
|
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
2023-04-28 21:02:24 +02:00
|
|
|
import { environment } from '@ghostfolio/api/environments/environment';
|
2023-04-23 12:02:01 +02:00
|
|
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
|
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
2021-12-07 20:24:15 +01:00
|
|
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
2022-04-24 16:23:03 +02:00
|
|
|
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
2022-05-24 20:55:55 +02:00
|
|
|
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
|
2023-04-02 09:44:13 +02:00
|
|
|
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
|
2021-12-07 20:24:15 +01:00
|
|
|
import {
|
|
|
|
getPermissions,
|
|
|
|
hasRole,
|
|
|
|
permissions
|
|
|
|
} from '@ghostfolio/common/permissions';
|
2023-04-02 09:44:13 +02:00
|
|
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
2021-04-13 21:53:58 +02:00
|
|
|
import { Injectable } from '@nestjs/common';
|
2022-09-10 11:38:06 +02:00
|
|
|
import { Prisma, Role, User } from '@prisma/client';
|
2023-07-30 18:49:38 +02:00
|
|
|
import { differenceInDays } from 'date-fns';
|
2022-06-11 12:56:34 +02:00
|
|
|
import { sortBy } from 'lodash';
|
2021-06-21 20:03:36 +02:00
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
const crypto = require('crypto');
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class UserService {
|
2021-09-24 21:09:48 +02:00
|
|
|
public static DEFAULT_CURRENCY = 'USD';
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2022-05-24 20:55:55 +02:00
|
|
|
private baseCurrency: string;
|
|
|
|
|
2021-04-18 19:06:54 +02:00
|
|
|
public constructor(
|
|
|
|
private readonly configurationService: ConfigurationService,
|
2021-08-22 22:11:05 +02:00
|
|
|
private readonly prismaService: PrismaService,
|
2021-12-07 20:24:15 +01:00
|
|
|
private readonly propertyService: PropertyService,
|
2022-04-24 16:23:03 +02:00
|
|
|
private readonly subscriptionService: SubscriptionService,
|
|
|
|
private readonly tagService: TagService
|
2022-05-24 20:55:55 +02:00
|
|
|
) {
|
|
|
|
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2022-04-05 21:02:07 +02:00
|
|
|
public async getUser(
|
2022-07-17 11:05:23 +02:00
|
|
|
{ Account, id, permissions, Settings, subscription }: UserWithSettings,
|
2022-04-05 21:02:07 +02:00
|
|
|
aLocale = locale
|
|
|
|
): Promise<IUser> {
|
2021-08-07 22:38:07 +02:00
|
|
|
const access = await this.prismaService.access.findMany({
|
2021-04-13 21:53:58 +02:00
|
|
|
include: {
|
|
|
|
User: true
|
|
|
|
},
|
2022-09-03 09:47:18 +02:00
|
|
|
orderBy: { alias: 'asc' },
|
2021-04-13 21:53:58 +02:00
|
|
|
where: { GranteeUser: { id } }
|
|
|
|
});
|
2022-04-24 16:23:03 +02:00
|
|
|
let tags = await this.tagService.getByUser(id);
|
|
|
|
|
|
|
|
if (
|
|
|
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
|
|
|
subscription.type === 'Basic'
|
|
|
|
) {
|
|
|
|
tags = [];
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
id,
|
2021-06-21 20:03:36 +02:00
|
|
|
permissions,
|
2021-06-02 20:15:53 +02:00
|
|
|
subscription,
|
2022-04-24 16:23:03 +02:00
|
|
|
tags,
|
2021-04-13 21:53:58 +02:00
|
|
|
access: access.map((accessItem) => {
|
|
|
|
return {
|
2022-08-27 11:29:09 +02:00
|
|
|
alias: accessItem.alias,
|
2021-04-13 21:53:58 +02:00
|
|
|
id: accessItem.id
|
|
|
|
};
|
|
|
|
}),
|
2021-04-25 21:22:35 +02:00
|
|
|
accounts: Account,
|
2021-04-13 21:53:58 +02:00
|
|
|
settings: {
|
2022-09-10 16:11:49 +02:00
|
|
|
...(<UserSettings>Settings.settings),
|
|
|
|
locale: (<UserSettings>Settings.settings)?.locale ?? aLocale
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-02-06 09:32:41 +01:00
|
|
|
public async hasAdmin() {
|
|
|
|
const usersWithAdminRole = await this.users({
|
|
|
|
where: {
|
|
|
|
role: {
|
|
|
|
equals: 'ADMIN'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return usersWithAdminRole.length > 0;
|
|
|
|
}
|
|
|
|
|
2021-08-16 21:40:29 +02:00
|
|
|
public isRestrictedView(aUser: UserWithSettings) {
|
2022-09-10 16:11:49 +02:00
|
|
|
return aUser.Settings.settings.isRestrictedView ?? false;
|
2021-08-16 21:40:29 +02:00
|
|
|
}
|
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
public async user(
|
|
|
|
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
|
|
|
): Promise<UserWithSettings | null> {
|
2022-05-14 13:53:43 +02:00
|
|
|
const {
|
|
|
|
accessToken,
|
|
|
|
Account,
|
2023-01-28 09:42:15 +01:00
|
|
|
Analytics,
|
2022-05-14 13:53:43 +02:00
|
|
|
authChallenge,
|
|
|
|
createdAt,
|
|
|
|
id,
|
|
|
|
provider,
|
|
|
|
role,
|
|
|
|
Settings,
|
|
|
|
Subscription,
|
|
|
|
thirdPartyId,
|
|
|
|
updatedAt
|
|
|
|
} = await this.prismaService.user.findUnique({
|
2023-01-28 09:42:15 +01:00
|
|
|
include: {
|
|
|
|
Account: true,
|
|
|
|
Analytics: true,
|
|
|
|
Settings: true,
|
|
|
|
Subscription: true
|
|
|
|
},
|
2021-04-13 21:53:58 +02:00
|
|
|
where: userWhereUniqueInput
|
|
|
|
});
|
|
|
|
|
2022-05-14 13:53:43 +02:00
|
|
|
const user: UserWithSettings = {
|
|
|
|
accessToken,
|
|
|
|
Account,
|
|
|
|
authChallenge,
|
|
|
|
createdAt,
|
|
|
|
id,
|
|
|
|
provider,
|
|
|
|
role,
|
2023-07-15 12:32:43 +02:00
|
|
|
Settings: Settings as UserWithSettings['Settings'],
|
2022-05-14 13:53:43 +02:00
|
|
|
thirdPartyId,
|
2023-01-28 09:42:15 +01:00
|
|
|
updatedAt,
|
|
|
|
activityCount: Analytics?.activityCount
|
2022-05-14 13:53:43 +02:00
|
|
|
};
|
2021-06-02 20:15:53 +02:00
|
|
|
|
2022-05-14 13:53:43 +02:00
|
|
|
if (user?.Settings) {
|
2022-09-10 11:38:06 +02:00
|
|
|
if (!user.Settings.settings) {
|
|
|
|
user.Settings.settings = {};
|
2022-05-14 13:53:43 +02:00
|
|
|
}
|
|
|
|
} else if (user) {
|
|
|
|
// Set default settings if needed
|
|
|
|
user.Settings = {
|
2022-09-10 11:38:06 +02:00
|
|
|
settings: {},
|
2022-05-14 13:53:43 +02:00
|
|
|
updatedAt: new Date(),
|
2022-09-10 16:11:49 +02:00
|
|
|
userId: user?.id
|
2022-05-14 13:53:43 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-09-10 11:38:06 +02:00
|
|
|
// Set default value for base currency
|
|
|
|
if (!(user.Settings.settings as UserSettings)?.baseCurrency) {
|
|
|
|
(user.Settings.settings as UserSettings).baseCurrency =
|
|
|
|
UserService.DEFAULT_CURRENCY;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set default value for date range
|
|
|
|
(user.Settings.settings as UserSettings).dateRange =
|
|
|
|
(user.Settings.settings as UserSettings).viewMode === 'ZEN'
|
|
|
|
? 'max'
|
|
|
|
: (user.Settings.settings as UserSettings)?.dateRange ?? 'max';
|
|
|
|
|
|
|
|
// Set default value for view mode
|
|
|
|
if (!(user.Settings.settings as UserSettings).viewMode) {
|
|
|
|
(user.Settings.settings as UserSettings).viewMode = 'DEFAULT';
|
|
|
|
}
|
|
|
|
|
2023-01-28 09:42:15 +01:00
|
|
|
let currentPermissions = getPermissions(user.role);
|
|
|
|
|
2022-05-14 13:53:43 +02:00
|
|
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
|
|
|
user.subscription =
|
|
|
|
this.subscriptionService.getSubscription(Subscription);
|
|
|
|
|
2023-07-30 18:49:38 +02:00
|
|
|
if (user.subscription?.type === 'Basic') {
|
|
|
|
const daysSinceRegistration = differenceInDays(
|
|
|
|
new Date(),
|
|
|
|
user.createdAt
|
|
|
|
);
|
|
|
|
let frequency = 20;
|
|
|
|
|
|
|
|
if (daysSinceRegistration > 180) {
|
|
|
|
frequency = 3;
|
|
|
|
} else if (daysSinceRegistration > 60) {
|
|
|
|
frequency = 5;
|
|
|
|
} else if (daysSinceRegistration > 30) {
|
|
|
|
frequency = 10;
|
|
|
|
} else if (daysSinceRegistration > 15) {
|
|
|
|
frequency = 15;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Analytics?.activityCount % frequency === 1) {
|
|
|
|
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
|
|
|
}
|
2023-01-28 09:42:15 +01:00
|
|
|
}
|
2021-06-21 20:03:36 +02:00
|
|
|
|
2023-01-28 09:42:15 +01:00
|
|
|
if (user.subscription?.type === 'Premium') {
|
|
|
|
currentPermissions.push(permissions.reportDataGlitch);
|
|
|
|
}
|
2022-05-14 13:53:43 +02:00
|
|
|
}
|
|
|
|
|
2021-12-07 20:24:15 +01:00
|
|
|
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {
|
|
|
|
if (hasRole(user, Role.ADMIN)) {
|
|
|
|
currentPermissions.push(permissions.toggleReadOnlyMode);
|
|
|
|
}
|
|
|
|
|
|
|
|
const isReadOnlyMode = (await this.propertyService.getByKey(
|
|
|
|
PROPERTY_IS_READ_ONLY_MODE
|
|
|
|
)) as boolean;
|
|
|
|
|
|
|
|
if (isReadOnlyMode) {
|
|
|
|
currentPermissions = currentPermissions.filter((permission) => {
|
|
|
|
return !(
|
|
|
|
permission.startsWith('create') ||
|
|
|
|
permission.startsWith('delete') ||
|
|
|
|
permission.startsWith('update')
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-28 21:02:24 +02:00
|
|
|
if (!environment.production && role === 'ADMIN') {
|
|
|
|
currentPermissions.push(permissions.impersonateAllUsers);
|
|
|
|
}
|
|
|
|
|
2022-06-11 12:56:34 +02:00
|
|
|
user.Account = sortBy(user.Account, (account) => {
|
|
|
|
return account.name;
|
|
|
|
});
|
2022-05-14 13:53:43 +02:00
|
|
|
user.permissions = currentPermissions.sort();
|
2021-06-02 20:15:53 +02:00
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
return user;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async users(params: {
|
|
|
|
skip?: number;
|
|
|
|
take?: number;
|
|
|
|
cursor?: Prisma.UserWhereUniqueInput;
|
|
|
|
where?: Prisma.UserWhereInput;
|
2021-12-05 16:52:24 +01:00
|
|
|
orderBy?: Prisma.UserOrderByWithRelationInput;
|
2021-04-13 21:53:58 +02:00
|
|
|
}): Promise<User[]> {
|
|
|
|
const { skip, take, cursor, where, orderBy } = params;
|
2021-08-07 22:38:07 +02:00
|
|
|
return this.prismaService.user.findMany({
|
2021-04-13 21:53:58 +02:00
|
|
|
skip,
|
|
|
|
take,
|
|
|
|
cursor,
|
|
|
|
where,
|
|
|
|
orderBy
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public createAccessToken(password: string, salt: string): string {
|
|
|
|
const hash = crypto.createHmac('sha512', salt);
|
|
|
|
hash.update(password);
|
|
|
|
|
|
|
|
return hash.digest('hex');
|
|
|
|
}
|
|
|
|
|
2023-02-05 18:57:12 +01:00
|
|
|
public async createUser({
|
|
|
|
data
|
2023-03-04 10:13:04 +01:00
|
|
|
}: {
|
|
|
|
data: Prisma.UserCreateInput;
|
|
|
|
}): Promise<User> {
|
2022-02-06 21:40:26 +01:00
|
|
|
if (!data?.provider) {
|
|
|
|
data.provider = 'ANONYMOUS';
|
|
|
|
}
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
let user = await this.prismaService.user.create({
|
2021-04-25 12:07:32 +02:00
|
|
|
data: {
|
|
|
|
...data,
|
|
|
|
Account: {
|
|
|
|
create: {
|
2022-05-24 20:55:55 +02:00
|
|
|
currency: this.baseCurrency,
|
2021-04-25 12:07:32 +02:00
|
|
|
isDefault: true,
|
|
|
|
name: 'Default Account'
|
|
|
|
}
|
2021-09-24 21:09:48 +02:00
|
|
|
},
|
|
|
|
Settings: {
|
|
|
|
create: {
|
2022-09-10 16:11:49 +02:00
|
|
|
settings: {
|
|
|
|
currency: this.baseCurrency
|
|
|
|
}
|
2021-09-24 21:09:48 +02:00
|
|
|
}
|
2021-04-25 12:07:32 +02:00
|
|
|
}
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
});
|
|
|
|
|
2023-02-05 18:57:12 +01:00
|
|
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
|
|
|
await this.prismaService.analytics.create({
|
|
|
|
data: {
|
|
|
|
User: { connect: { id: user.id } }
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-02-06 21:40:26 +01:00
|
|
|
if (data.provider === 'ANONYMOUS') {
|
2021-04-13 21:53:58 +02:00
|
|
|
const accessToken = this.createAccessToken(
|
|
|
|
user.id,
|
|
|
|
this.getRandomString(10)
|
|
|
|
);
|
|
|
|
|
|
|
|
const hashedAccessToken = this.createAccessToken(
|
|
|
|
accessToken,
|
|
|
|
process.env.ACCESS_TOKEN_SALT
|
|
|
|
);
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
user = await this.prismaService.user.update({
|
2021-04-13 21:53:58 +02:00
|
|
|
data: { accessToken: hashedAccessToken },
|
|
|
|
where: { id: user.id }
|
|
|
|
});
|
|
|
|
|
|
|
|
return { ...user, accessToken };
|
|
|
|
}
|
|
|
|
|
|
|
|
return user;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async updateUser(params: {
|
|
|
|
where: Prisma.UserWhereUniqueInput;
|
|
|
|
data: Prisma.UserUpdateInput;
|
|
|
|
}): Promise<User> {
|
|
|
|
const { where, data } = params;
|
2021-08-07 22:38:07 +02:00
|
|
|
return this.prismaService.user.update({
|
2021-04-13 21:53:58 +02:00
|
|
|
data,
|
|
|
|
where
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
2023-05-25 17:27:33 +02:00
|
|
|
try {
|
|
|
|
await this.prismaService.access.deleteMany({
|
|
|
|
where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] }
|
|
|
|
});
|
|
|
|
} catch {}
|
2021-05-03 21:23:00 +02:00
|
|
|
|
2023-05-25 17:27:33 +02:00
|
|
|
try {
|
|
|
|
await this.prismaService.account.deleteMany({
|
|
|
|
where: { userId: where.id }
|
|
|
|
});
|
|
|
|
} catch {}
|
2021-05-03 21:23:00 +02:00
|
|
|
|
2023-05-25 17:27:33 +02:00
|
|
|
try {
|
|
|
|
await this.prismaService.analytics.delete({
|
|
|
|
where: { userId: where.id }
|
|
|
|
});
|
|
|
|
} catch {}
|
2021-05-03 21:23:00 +02:00
|
|
|
|
2023-05-25 17:27:33 +02:00
|
|
|
try {
|
|
|
|
await this.prismaService.order.deleteMany({
|
|
|
|
where: { userId: where.id }
|
|
|
|
});
|
|
|
|
} catch {}
|
2021-05-03 21:23:00 +02:00
|
|
|
|
|
|
|
try {
|
2021-08-07 22:38:07 +02:00
|
|
|
await this.prismaService.settings.delete({
|
2021-05-03 21:23:00 +02:00
|
|
|
where: { userId: where.id }
|
|
|
|
});
|
|
|
|
} catch {}
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
return this.prismaService.user.delete({
|
2021-04-13 21:53:58 +02:00
|
|
|
where
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-16 21:40:29 +02:00
|
|
|
public async updateUserSetting({
|
|
|
|
userId,
|
|
|
|
userSettings
|
|
|
|
}: {
|
|
|
|
userId: string;
|
|
|
|
userSettings: UserSettings;
|
|
|
|
}) {
|
2022-09-10 11:38:06 +02:00
|
|
|
const settings = userSettings as unknown as Prisma.JsonObject;
|
2021-08-16 21:40:29 +02:00
|
|
|
|
|
|
|
await this.prismaService.settings.upsert({
|
|
|
|
create: {
|
|
|
|
settings,
|
|
|
|
User: {
|
|
|
|
connect: {
|
|
|
|
id: userId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
update: {
|
|
|
|
settings
|
|
|
|
},
|
|
|
|
where: {
|
|
|
|
userId: userId
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-20 21:52:01 +02:00
|
|
|
private getRandomString(length: number) {
|
|
|
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
|
|
const result = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
|
|
result.push(
|
|
|
|
characters.charAt(Math.floor(Math.random() * characters.length))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return result.join('');
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|