From bc87923e276031c2637935c9c23c295f553df457 Mon Sep 17 00:00:00 2001 From: Eneshjakhar <97552851+Eneshjakhar@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:19:48 -0700 Subject: [PATCH] linking complete --- packages/server/ormconfig.ts | 2 + packages/server/src/auth/auth.controller.ts | 81 +++++++++++++++++++ packages/server/src/auth/auth.service.ts | 67 +++++++++++++++ .../src/slack/slack-link-code.entity.ts | 16 ++++ 4 files changed, 166 insertions(+) create mode 100644 packages/server/src/slack/slack-link-code.entity.ts diff --git a/packages/server/ormconfig.ts b/packages/server/ormconfig.ts index b9626a5a0..2bb196d10 100644 --- a/packages/server/ormconfig.ts +++ b/packages/server/ormconfig.ts @@ -47,6 +47,7 @@ import { SuperCourseModel } from './src/course/super-course.entity'; import { OrganizationSettingsModel } from './src/organization/organization_settings.entity'; import { OrganizationRoleHistory } from './src/organization/organization_role_history.entity'; import { SentEmailModel } from './src/mail/sent-email.entity'; +import { SlackLinkCodeModel } from './src/slack/slack-link-code.entity'; // set .envs to their default values if the developer hasn't yet set them if (fs.existsSync('.env')) { config(); @@ -120,6 +121,7 @@ const typeorm: DataSourceOptions = { SentEmailModel, OrganizationSettingsModel, OrganizationRoleHistory, + SlackLinkCodeModel, ], logging: process.env.NODE_ENV !== 'production' diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index 820d6934b..ed014e2d7 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -121,6 +121,10 @@ export class AuthController { `${this.GOOGLE_AUTH_URL}?client_id=${process.env.GOOGLE_CLIENT_ID}.apps.googleusercontent.com` + `&redirect_uri=${process.env.GOOGLE_REDIRECT_URI}&response_type=code&scope=openid%20profile%20email`, }); + case 'slack': { + const startUrl = `${process.env.DOMAIN}/api/v1/auth/slack/start?oid=${organizationId}`; + return res.status(200).send({ redirectUri: startUrl }); + } default: return res .status(HttpStatus.BAD_REQUEST) @@ -473,6 +477,13 @@ export class AuthController { Number(organizationId), ); break; + case 'slack': { + return res + .status(HttpStatus.BAD_REQUEST) + .send({ + message: 'Use /auth/slack/start and /auth/slack/exchange', + }); + } default: return res .status(HttpStatus.BAD_REQUEST) @@ -549,4 +560,74 @@ export class AuthController { private isSecure(): boolean { return this.configService.get('DOMAIN').startsWith('https://'); } + + // Simple Slack linking endpoints + @Get('slack/start') + async slackStart( + @Req() req: Request, + @Res() res: Response, + @Query('state') state?: string, + @Query('redirect_uri') redirectUri?: string, + ): Promise { + const origin = `${req.protocol}://${req.get('host')}`; + const finishUrl = `${origin}/api/v1/auth/slack/finish?state=${state || ''}&redirect_uri=${redirectUri || ''}`; + + const hasAuth = !!getCookie(req, 'auth_token'); + if (!hasAuth) { + res.redirect( + HttpStatus.FOUND, + `/login?redirect=${encodeURIComponent(finishUrl)}`, + ); + return; + } + + res.redirect(HttpStatus.FOUND, finishUrl); + } + + @Get('slack/finish') + @UseGuards(JwtAuthGuard) + async slackFinish( + @Req() req: Request, + @Res() res: Response, + @Query('state') state?: string, + @Query('redirect_uri') redirectUri?: string, + ): Promise { + if (!redirectUri) { + res.status(HttpStatus.BAD_REQUEST).send('Missing redirect_uri'); + return; + } + + const userId = Number((req.user as RequestUser).userId); + const code = await this.authService.generateSlackLinkCode(userId); + + const url = new URL(redirectUri); + url.searchParams.set('state', state || ''); + url.searchParams.set('code', code); + + console.log(`[Slack] Redirecting to ${url.toString()}`); + res.redirect(HttpStatus.FOUND, url.toString()); + } + + @Post('slack/exchange') + async slackExchange( + @Body() body: { code: string }, + @Res() res: Response, + ): Promise { + try { + const { code } = body; + if (!code) { + res.status(HttpStatus.BAD_REQUEST).json({ error: 'Missing code' }); + return; + } + + const userId = await this.authService.exchangeSlackLinkCode(code); + const userData = await this.authService.getSlackUserData(userId); + + console.log(`[Slack] Successfully linked user ${userId}`); + res.status(HttpStatus.OK).json(userData); + } catch (error) { + console.log(`[Slack] Exchange error: ${error.message}`); + res.status(HttpStatus.BAD_REQUEST).json({ error: error.message }); + } + } } diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index 600cff6b3..b620e41f5 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -20,6 +20,7 @@ import { ChatTokenModel } from 'chatbot/chat-token.entity'; import { v4 } from 'uuid'; import { UserSubscriptionModel } from 'mail/user-subscriptions.entity'; import { OrganizationService } from '../organization/organization.service'; +import { SlackLinkCodeModel } from '../slack/slack-link-code.entity'; @Injectable() export class AuthService { @@ -320,4 +321,70 @@ export class AuthService { } return token; } + + // Simple Slack linking methods + async generateSlackLinkCode(userId: number): Promise { + const user = await UserModel.findOne({ where: { id: userId } }); + if (!user) throw new BadRequestException('User not found'); + + const code = v4(); + const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes + + await SlackLinkCodeModel.create({ code, userId, expiresAt }).save(); + console.log( + `[Slack] Generated code ${code.slice(0, 8)}... for user ${userId}`, + ); + + return code; + } + + async exchangeSlackLinkCode(code: string): Promise { + console.log(`[Slack] Exchanging code ${code.slice(0, 8)}...`); + + if (!code || typeof code !== 'string') { + throw new BadRequestException('Invalid code'); + } + + const record = await SlackLinkCodeModel.findOne({ where: { code } }); + if (!record) { + throw new BadRequestException('Code not found'); + } + + if (record.expiresAt < new Date()) { + await SlackLinkCodeModel.remove(record); + throw new BadRequestException('Code expired'); + } + + await SlackLinkCodeModel.remove(record); + console.log(`[Slack] Code exchanged for user ${record.userId}`); + + return record.userId; + } + + async getSlackUserData(userId: number) { + const user = await UserModel.findOne({ + where: { id: userId }, + relations: [ + 'organizationUser', + 'courses', + 'courses.course', + 'chat_token', + ], + }); + + if (!user) throw new BadRequestException('User not found'); + + return { + userId: user.id, + name: user.name, + email: user.email, + organizationId: user.organizationUser?.organizationId || null, + courses: + user.courses?.map((uc) => ({ + id: uc.courseId, + name: uc.course?.name || 'Unknown Course', + })) || [], + chatToken: user.chat_token?.token || null, + }; + } } diff --git a/packages/server/src/slack/slack-link-code.entity.ts b/packages/server/src/slack/slack-link-code.entity.ts new file mode 100644 index 000000000..0d2371ba7 --- /dev/null +++ b/packages/server/src/slack/slack-link-code.entity.ts @@ -0,0 +1,16 @@ +import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('slack_link_codes') +export class SlackLinkCodeModel extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: 'varchar', length: 200, unique: true }) + code: string; + + @Column() + userId: number; + + @Column({ type: 'timestamp' }) + expiresAt: Date; +}