You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

156 lines
4.4 KiB

import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
ConflictException,
ForbiddenException,
Injectable,
InternalServerErrorException,
Logger,
NotFoundException
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import {
LinkOidcToUserParams,
ValidateOAuthLoginParams
} from './interfaces/interfaces';
@Injectable()
export class AuthService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly jwtService: JwtService,
private readonly propertyService: PropertyService,
private readonly userService: UserService
) {}
public async validateAnonymousLogin(accessToken: string): Promise<string> {
const hashedAccessToken = this.userService.createAccessToken({
password: accessToken,
salt: this.configurationService.get('ACCESS_TOKEN_SALT')
});
const [user] = await this.userService.users({
where: { accessToken: hashedAccessToken }
});
if (user) {
return this.jwtService.sign({
id: user.id
});
}
throw new Error();
}
public async validateOAuthLogin({
provider,
thirdPartyId
}: ValidateOAuthLoginParams): Promise<string> {
try {
// First, search by thirdPartyId only to support linked accounts
// (users with provider ANONYMOUS but with thirdPartyId set)
let [user] = await this.userService.users({
where: { thirdPartyId }
});
if (user) {
return this.jwtService.sign({
id: user.id
});
}
const isUserSignupEnabled =
await this.propertyService.isUserSignupEnabled();
if (!isUserSignupEnabled) {
throw new ForbiddenException('Sign up forbidden');
}
// Create new user if not found
user = await this.userService.createUser({
data: {
provider,
thirdPartyId
}
});
return this.jwtService.sign({
id: user.id
});
} catch (error) {
Logger.error(
`validateOAuthLogin: Error - ${error instanceof Error ? error.message : 'Unknown error'}`,
'AuthService'
);
throw new InternalServerErrorException(
'validateOAuthLogin',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
/**
* Links an OIDC provider to an existing user account.
* The user must have provider ANONYMOUS (token-based auth).
* The thirdPartyId must not be already linked to another user.
*
* @param params - Parameters for linking OIDC to user
* @param params.userId - The ID of the user to link
* @param params.thirdPartyId - The OIDC subject identifier
* @returns JWT token for the linked user
* @throws ConflictException if thirdPartyId is already linked to another user
* @throws Error if user not found or has invalid provider
*/
public async linkOidcToUser({
thirdPartyId,
userId
}: LinkOidcToUserParams): Promise<string> {
// Check if thirdPartyId is already linked to another user
const [existingUser] = await this.userService.users({
where: { thirdPartyId }
});
if (existingUser) {
if (existingUser.id === userId) {
Logger.warn(
`linkOidcToUser: User ${userId.substring(0, 8)}... is already linked to this thirdPartyId`,
'AuthService'
);
// Already linked to the same user, just return token
return this.jwtService.sign({ id: userId });
}
Logger.warn(
`linkOidcToUser: thirdPartyId already linked to another user ${existingUser.id.substring(0, 8)}...`,
'AuthService'
);
throw new ConflictException(
'This OIDC account is already linked to another user'
);
}
// Get the current user
const user = await this.userService.user({ id: userId });
if (!user) {
throw new NotFoundException('User not found');
}
if (user.provider !== 'ANONYMOUS') {
throw new ConflictException(
'Only users with token authentication can link OIDC'
);
}
// Update user with thirdPartyId and switch provider to OIDC
await this.userService.updateUser({
data: { thirdPartyId, provider: 'OIDC' },
where: { id: userId }
});
return this.jwtService.sign({ id: userId });
}
}