Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const ChatbotSettingsModal: React.FC<ChatbotSettingsModalProps> = ({
form.setFieldsValue({ ...sets, ...formValues })
setFormValues({ ...sets, ...formValues })
}
// eslint-disable-next-line
}, [courseSettings, form])

const areParamsDefault = useMemo(() => {
Expand Down
2 changes: 2 additions & 0 deletions packages/server/ormconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
import { ChatbotProviderModel } from './src/chatbot/chatbot-infrastructure-models/chatbot-provider.entity';
import { CourseChatbotSettingsModel } from './src/chatbot/chatbot-infrastructure-models/course-chatbot-settings.entity';
import { OrganizationChatbotSettingsModel } from './src/chatbot/chatbot-infrastructure-models/organization-chatbot-settings.entity';
Expand Down Expand Up @@ -124,6 +125,7 @@ const typeorm: DataSourceOptions = {
SentEmailModel,
OrganizationSettingsModel,
OrganizationRoleHistory,
SlackLinkCodeModel,
ChatbotProviderModel,
CourseChatbotSettingsModel,
OrganizationChatbotSettingsModel,
Expand Down
81 changes: 81 additions & 0 deletions packages/server/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -549,4 +560,74 @@ export class AuthController {
private isSecure(): boolean {
return this.configService.get<string>('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<void> {
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<void> {
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<void> {
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 });
}
}
}
67 changes: 67 additions & 0 deletions packages/server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -320,4 +321,70 @@ export class AuthService {
}
return token;
}

// Simple Slack linking methods
async generateSlackLinkCode(userId: number): Promise<string> {
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<number> {
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,
};
}
}
1 change: 0 additions & 1 deletion packages/server/src/backup/backup.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { Test, TestingModule } from '@nestjs/testing';
import { BackupService, baseBackupCommand } from './backup.service';
import * as fs from 'fs';
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/guards/organization-roles.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class OrganizationRolesGuard implements CanActivate {
}

async setupData(
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
request: any,
): Promise<{ user: OrganizationUserModel }> {
const user = await OrganizationUserModel.findOne({
Expand Down
16 changes: 16 additions & 0 deletions packages/server/src/slack/slack-link-code.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}