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: Add login methods list in AdminCP #585

Merged
merged 5 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
58 changes: 58 additions & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,64 @@ import { PluginsModule } from './plugins/plugins.module';
config: DATABASE_ENVS,
schemaDatabase,
},
ssoLoginMethod: [
{
name: 'Google',
code: 'google',
getUrl: ({ redirect_uri, client_id }) => {
const params = new URLSearchParams({
client_id,
redirect_uri,
response_type: 'code',
scope: 'openid profile email',
});

return {
url: `https://accounts.google.com/o/oauth2/auth?${params}`,
};
},
callback: async ({
client_id,
client_secret,
code,
redirect_uri,
}) => {
const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id,
client_secret,
code,
redirect_uri,
grant_type: 'authorization_code',
}),
});

return await res.json();
},
registerCallback: async ({ access_token }) => {
const res = await fetch(
'https://www.googleapis.com/oauth2/v1/userinfo',
{
headers: {
Authorization: `Bearer ${access_token}`,
},
},
);
const data = await res.json();

return {
email: data.email,
id: data.id,
name: data.name,
verified_email: data.verified_email,
};
},
},
],
// email: emailResend({
// api_key: process.env.EMAIL_RESEND_API_KEY,
// from: process.env.EMAIL_RESEND_FROM,
Expand Down
23 changes: 22 additions & 1 deletion apps/frontend/src/plugins/admin/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,28 @@
}
},
"methods": {
"title": "Login Methods"
"title": "Login Methods",
"name": "Name",
"enabled": "Enabled",
"create": {
"title": "Create Method",
"desc": "New login method for your website.",
"provider": "Provider",
"client_id": "Client ID",
"client_secret": "Client Secret",
"success": "Login method has been created.",
"your_callback_url": "Your callback URL"
},
"edit": {
"title": "Edit Method",
"success": "Login method has been updated."
},
"delete": {
"desc": "This action will delete the login method <name></name>.",
"warn": "If you delete this login method, users who use this method to log in will not be able to log in.",
"success": "Login method has been deleted.",
"submit": "Yes, delete login method"
}
}
},
"legal": {
Expand Down
5 changes: 4 additions & 1 deletion packages/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

import { CoreModule } from './core/core.module';
import { SSOAuthItem } from './helpers/auth/sso.service';
import { EmailSenderFunction } from './helpers/email/email-helpers.type';
import { GlobalHelpersModule } from './helpers/helpers.module';
import {
Expand Down Expand Up @@ -162,9 +163,11 @@ export class VitNodeCoreModule {
static register({
database,
email,
ssoLoginMethod,
}: {
database: DatabaseModuleArgs;
email?: EmailSenderFunction;
ssoLoginMethod?: SSOAuthItem[];
}): DynamicModule {
return {
module: VitNodeCoreModule,
Expand All @@ -181,7 +184,7 @@ export class VitNodeCoreModule {
rootPath: ABSOLUTE_PATHS.uploads.public,
serveRoot: '/public/',
}),
GlobalHelpersModule.register({ email }),
GlobalHelpersModule.register({ email, ssoLoginMethod }),
],
};
}
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/core/admin/settings/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Module } from '@nestjs/common';

import { AuthSettingsAdminController } from './auth.controller';
import { MethodsAuthSettingsAdminModule } from './methods/methods.module';
import { EditAuthSettingsAdminService } from './services/edit.service';
import { ShowAuthSettingsAdminService } from './services/show.service';

@Module({
providers: [ShowAuthSettingsAdminService, EditAuthSettingsAdminService],
controllers: [AuthSettingsAdminController],
imports: [MethodsAuthSettingsAdminModule],
})
export class AuthSettingsAdminModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { AdminAuthGuard } from '@/guards/admin-auth.guard';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import {
ApiCreatedResponse,
ApiOkResponse,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import {
CreateMethodAuthSettingsAdminBody,
EditMethodAuthSettingsAdminBody,
ShowMethodAuthSettingsAdmin,
ShowMethodAuthSettingsAdminObj,
} from 'vitnode-shared/admin/settings/auth.dto';

import { CreateMethodsAuthSettingsAdminService } from './services/create.service';
import { DeleteMethodsAuthSettingsAdminService } from './services/delete.service';
import { EditMethodsAuthSettingsAdminService } from './services/edit.service';
import { ShowMethodsAuthSettingsAdminService } from './services/show.service';

@ApiTags('Admin')
@Controller('admin/settings/auth/methods')
@ApiSecurity('admin')
@UseGuards(AdminAuthGuard)
export class MethodsAuthSettingsAdminController {
constructor(
private readonly showService: ShowMethodsAuthSettingsAdminService,
private readonly createService: CreateMethodsAuthSettingsAdminService,
private readonly deleteService: DeleteMethodsAuthSettingsAdminService,
private readonly editService: EditMethodsAuthSettingsAdminService,
) {}

@Post()
@ApiCreatedResponse({
type: ShowMethodAuthSettingsAdmin,
description: 'Create new auth method',
})
async createMethod(
@Body() body: CreateMethodAuthSettingsAdminBody,
): Promise<ShowMethodAuthSettingsAdmin> {
return this.createService.create(body);
}

@Delete(':code')
@ApiOkResponse({
description: 'Delete auth method',
})
async deleteMethod(@Param('code') code: string): Promise<void> {
return this.deleteService.delete(code);
}

@Put(':code')
@ApiOkResponse({
description: 'Edit auth method',
type: ShowMethodAuthSettingsAdmin,
})
async editMethod(
@Param('code') code: string,
@Body() body: EditMethodAuthSettingsAdminBody,
): Promise<ShowMethodAuthSettingsAdmin> {
return this.editService.edit({ code, body });
}

@Get()
@ApiOkResponse({
type: ShowMethodAuthSettingsAdminObj,
description: 'Show all auth enabled methods',
})
async showMethod(): Promise<ShowMethodAuthSettingsAdminObj> {
return this.showService.show();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';

import { MethodsAuthSettingsAdminController } from './methods.controller';
import { CreateMethodsAuthSettingsAdminService } from './services/create.service';
import { DeleteMethodsAuthSettingsAdminService } from './services/delete.service';
import { EditMethodsAuthSettingsAdminService } from './services/edit.service';
import { ShowMethodsAuthSettingsAdminService } from './services/show.service';

@Module({
providers: [
ShowMethodsAuthSettingsAdminService,
CreateMethodsAuthSettingsAdminService,
DeleteMethodsAuthSettingsAdminService,
EditMethodsAuthSettingsAdminService,
],
controllers: [MethodsAuthSettingsAdminController],
})
export class MethodsAuthSettingsAdminModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { SSOAuthConfig, SSOAuthHelper } from '@/helpers/auth/sso.service';
import {
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { existsSync } from 'fs';
import { writeFile } from 'fs/promises';
import {
CreateMethodAuthSettingsAdminBody,
ShowMethodAuthSettingsAdmin,
} from 'vitnode-shared/admin/settings/auth.dto';

@Injectable()
export class CreateMethodsAuthSettingsAdminService {
constructor(private readonly ssoAuthHelper: SSOAuthHelper) {}

async create({
code,
client_id,
client_secret,
}: CreateMethodAuthSettingsAdminBody): Promise<ShowMethodAuthSettingsAdmin> {
const sso = this.ssoAuthHelper.getSSO(code);
if (!sso) {
throw new NotFoundException(`SSO method with ${code} code not found`);
}

const dataSSO: SSOAuthConfig['sso'][0] = {
client_id,
client_secret,
code,
enabled: true,
};

if (!existsSync(this.ssoAuthHelper.path)) {
const dataToSave: SSOAuthConfig = {
sso: [dataSSO],
};

await writeFile(
this.ssoAuthHelper.path,
JSON.stringify(dataToSave, null, 2),
);

return {
...dataSSO,
name: sso.name,
};
}

const ssoConfig = await this.ssoAuthHelper.getSSOConfig();
const checkIfSSOExists = ssoConfig.sso.find(item => item.code === code);
if (checkIfSSOExists) {
throw new ConflictException(
`SSO method with ${code} code already exists`,
);
}

ssoConfig.sso.push(dataSSO);
await writeFile(
this.ssoAuthHelper.path,
JSON.stringify(ssoConfig, null, 2),
);

return {
...dataSSO,
name: sso.name,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { SSOAuthHelper } from '@/helpers/auth/sso.service';
import { Injectable, NotFoundException } from '@nestjs/common';
import { rm, writeFile } from 'fs/promises';

@Injectable()
export class DeleteMethodsAuthSettingsAdminService {
constructor(private readonly ssoAuthHelper: SSOAuthHelper) {}

async delete(code: string): Promise<void> {
const sso = await this.ssoAuthHelper.getActiveSSO(code);
if (!sso) {
throw new NotFoundException(`SSO method with ${code} code not found`);
}

const ssoConfigFile = await this.ssoAuthHelper.getSSOConfig();
ssoConfigFile.sso = ssoConfigFile.sso.filter(item => item.code !== code);
if (ssoConfigFile.sso.length === 0) {
await rm(this.ssoAuthHelper.path);

return;
}

await writeFile(
this.ssoAuthHelper.path,
JSON.stringify(ssoConfigFile, null, 2),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { SSOAuthHelper } from '@/helpers/auth/sso.service';
import { Injectable, NotFoundException } from '@nestjs/common';
import { writeFile } from 'fs/promises';
import {
EditMethodAuthSettingsAdminBody,
ShowMethodAuthSettingsAdmin,
} from 'vitnode-shared/admin/settings/auth.dto';

@Injectable()
export class EditMethodsAuthSettingsAdminService {
constructor(private readonly ssoAuthHelper: SSOAuthHelper) {}

async edit({
code,
body: { client_id, client_secret, enabled },
}: {
body: EditMethodAuthSettingsAdminBody;
code: string;
}): Promise<ShowMethodAuthSettingsAdmin> {
const sso = await this.ssoAuthHelper.getActiveSSO(code);
if (!sso) {
throw new NotFoundException(`SSO method with ${code} code not found`);
}
const ssoConfigFile = await this.ssoAuthHelper.getSSOConfig();
const ssoConfig = ssoConfigFile.sso;
const ssoIndex = ssoConfig.findIndex(item => item.code === code);
if (ssoIndex === -1) {
throw new NotFoundException(`SSO method with ${code} code not found`);
}
ssoConfig[ssoIndex] = {
...ssoConfig[ssoIndex],
client_id,
client_secret,
enabled,
};

await writeFile(
this.ssoAuthHelper.path,
JSON.stringify(ssoConfigFile, null, 2),
);

return {
...ssoConfig[ssoIndex],
name: sso.name,
};
}
}
Loading
Loading