[core] Scopes for Api Key (read/send/etc) - fix #2035

This commit is contained in:
devlikepro committed 2026-05-07 14:14:19 +07:00
1 parent 2fe7e307f0
commit 21daf501be
33 files changed
+456 -219

No files matched your search

+9 -86
View File
@@ -3,11 +3,9 @@ import {
Controller,
Delete,
Get,
NotFoundException,
Param,
Post,
Put,
UnprocessableEntityException,
UseGuards,
UsePipes,
} from '@nestjs/common';
@@ -17,10 +15,9 @@ import { Action } from '@waha/core/auth/casl.types';
import { CanServer } from '@waha/core/auth/policies';
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
import { ApiKey, CheckInvariant } from '@waha/core/storage/IApiKeyRepository';
import { ApiKeyService } from '@waha/core/services/ApiKeyService';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { ApiKeyDTO, ApiKeyRequest } from '@waha/structures/apikeys.dto';
import { generatePrefixedId, generateSecret } from '@waha/utils/ids';
@ApiSecurity('api_key')
@Controller('api/keys')
@@ -30,57 +27,21 @@ import { generatePrefixedId, generateSecret } from '@waha/utils/ids';
export class ApiKeysController {
constructor(private manager: SessionManager) {}
private get service(): ApiKeyService {
return new ApiKeyService(this.manager);
}
@Post('/')
@ApiOperation({ summary: 'Create a new API key' })
@UsePipes(new WAHAValidationPipe())
async create(@Body() body: ApiKeyRequest): Promise<ApiKeyDTO> {
CheckInvariant(body);
if (body.session) {
const exists = await this.manager.exists(body.session);
if (!exists) {
throw new UnprocessableEntityException(
`Session "${body.session}" does not exist`,
);
}
}
let apikey: ApiKey | null = null;
// Try 5 times to check there's no conflict on id and key
for (let i = 0; i < 5; i++) {
apikey = {
id: generatePrefixedId('key_id'),
key: `key_${generateSecret(32)}`,
isActive: body.isActive,
isAdmin: body.isAdmin,
session: body.session,
rules: null,
};
const idExists = await this.manager.apiKeyRepository.getById(apikey.id);
if (idExists) {
continue;
}
const keyExists = await this.manager.apiKeyRepository.getByKey(
apikey.key,
);
if (keyExists) {
continue;
}
break;
}
if (!apikey) {
throw new UnprocessableEntityException(
`Failed to generate API key, try again`,
);
}
CheckInvariant(apikey);
await this.manager.apiKeyRepository.upsert(apikey);
return ApiKeyToDTO(apikey);
return this.service.create(body);
}
@Get('/')
@ApiOperation({ summary: 'Get all API keys' })
async list(): Promise<ApiKeyDTO[]> {
const keys = await this.manager.apiKeyRepository.list();
return keys.map((key) => ApiKeyToDTO(key));
return this.service.list();
}
@Put('/:id')
@@ -90,50 +51,12 @@ export class ApiKeysController {
@Param('id') id: string,
@Body() body: ApiKeyRequest,
): Promise<ApiKeyDTO> {
const existing = await this.manager.apiKeyRepository.getById(id);
if (!existing) {
throw new NotFoundException('API key not found');
}
const apikey: ApiKey = {
...existing,
isActive: body.isActive,
isAdmin: body.isAdmin,
session: body.session,
};
CheckInvariant(apikey);
if (apikey.session) {
const exists = await this.manager.exists(apikey.session);
if (!exists) {
throw new UnprocessableEntityException(
`Session "${apikey.session}" does not exist`,
);
}
}
CheckInvariant(apikey);
await this.manager.apiKeyRepository.upsert(apikey);
return ApiKeyToDTO(apikey);
return this.service.update(id, body);
}
@Delete('/:id')
@ApiOperation({ summary: 'Delete an API key' })
async delete(@Param('id') id: string): Promise<{ result: true }> {
const existing = await this.manager.apiKeyRepository.getById(id);
if (!existing) {
throw new NotFoundException('API key not found');
}
await this.manager.apiKeyRepository.deleteById(id);
return { result: true };
return this.service.delete(id);
}
}
function ApiKeyToDTO(apikey: ApiKey): ApiKeyDTO {
return {
id: apikey.id,
key: apikey.key,
isActive: apikey.isActive,
isAdmin: apikey.isAdmin,
session: apikey.session,
};
}
+1 -1
View File
@@ -35,7 +35,7 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/auth')
@ApiTags('📱 Pairing')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Control, FromParam('session')))
class AuthController {
constructor(private manager: SessionManager) {}
+1 -1
View File
@@ -25,7 +25,7 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/calls')
@ApiTags('📞 Calls')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
export class CallsController {
constructor(private manager: SessionManager) {}
+14 -1
View File
@@ -50,7 +50,6 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/channels')
@ApiTags('📢 Channels')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
export class ChannelsController {
constructor(
private manager: SessionManager,
@@ -59,6 +58,7 @@ export class ChannelsController {
@Get('')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get list of know channels' })
async list(
@WorkingSessionParam session: WhatsappSession,
@@ -69,6 +69,7 @@ export class ChannelsController {
@Post('')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Create a new channel.' })
create(
@WorkingSessionParam session: WhatsappSession,
@@ -80,6 +81,7 @@ export class ChannelsController {
@Delete(':id')
@SessionApiParam
@NewsletterIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Delete the channel.' })
delete(
@WorkingSessionParam session: WhatsappSession,
@@ -91,6 +93,7 @@ export class ChannelsController {
@Get(':id')
@SessionApiParam
@NewsletterIdOrInviteCodeApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({
summary: 'Get the channel info',
description:
@@ -111,6 +114,7 @@ export class ChannelsController {
@Get(':id/messages/preview')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
@ApiParam({
name: 'id',
@@ -146,6 +150,7 @@ export class ChannelsController {
@Post(':id/follow')
@SessionApiParam
@NewsletterIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Follow the channel.' })
follow(
@WorkingSessionParam session: WhatsappSession,
@@ -157,6 +162,7 @@ export class ChannelsController {
@Post(':id/unfollow')
@SessionApiParam
@NewsletterIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Unfollow the channel.' })
unfollow(
@WorkingSessionParam session: WhatsappSession,
@@ -168,6 +174,7 @@ export class ChannelsController {
@Post(':id/mute')
@SessionApiParam
@NewsletterIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Mute the channel.' })
mute(
@WorkingSessionParam session: WhatsappSession,
@@ -179,6 +186,7 @@ export class ChannelsController {
@Post(':id/unmute')
@SessionApiParam
@NewsletterIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Unmute the channel.' })
unmute(
@WorkingSessionParam session: WhatsappSession,
@@ -190,6 +198,7 @@ export class ChannelsController {
@Post('/search/by-view')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Search for channels (by view)' })
async searchByView(
@@ -202,6 +211,7 @@ export class ChannelsController {
@Post('/search/by-text')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Search for channels (by text)' })
async searchByText(
@@ -213,6 +223,7 @@ export class ChannelsController {
@Get('/search/views')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get list of views for channel search' })
getSearchViews(): Promise<ChannelView[]> {
return this.channelsInfoService.getViews();
@@ -220,6 +231,7 @@ export class ChannelsController {
@Get('/search/countries')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get list of countries for channel search' })
getSearchCountries(): Promise<ChannelCountry[]> {
return this.channelsInfoService.getCountries();
@@ -227,6 +239,7 @@ export class ChannelsController {
@Get('/search/categories')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get list of categories for channel search' })
getSearchCategories(): Promise<ChannelCategory[]> {
return this.channelsInfoService.getCategories();
+16 -1
View File
@@ -52,12 +52,12 @@ import { Action } from '@waha/core/auth/casl.types';
@ApiTags('💬 Chats')
@UsePipes(new ValidationPipe({ transform: true }))
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
class ChatsController {
constructor(private manager: SessionManager) {}
@Get('')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get chats' })
getChats(
@WorkingSessionParam session: WhatsappSession,
@@ -68,6 +68,7 @@ class ChatsController {
@Get('overview')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({
summary:
'Get chats overview. Includes all necessary things to build UI "your chats overview" page - chat id, name, picture, last message. Sorting by last message timestamp',
@@ -83,6 +84,7 @@ class ChatsController {
@Post('overview')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({
summary:
'Get chats overview. Use POST if you have too many "ids" params - GET can limit it',
@@ -97,6 +99,7 @@ class ChatsController {
@Delete(':chatId')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Deletes the chat' })
@ChatIdApiParam
deleteChat(
@@ -108,6 +111,7 @@ class ChatsController {
@Get(':chatId/picture')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Gets chat picture' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getChatPicture(
@@ -121,6 +125,7 @@ class ChatsController {
@Get(':chatId/messages')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Gets messages in the chat' })
@ChatIdApiParam
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@@ -141,6 +146,7 @@ class ChatsController {
@Post(':chatId/messages/read')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Read unread messages in the chat' })
@ChatIdApiParam
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@@ -154,6 +160,7 @@ class ChatsController {
@Get(':chatId/messages/:messageId')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Gets message by id' })
@ChatIdApiParam
async getChatMessage(
@@ -171,6 +178,7 @@ class ChatsController {
@Post(':chatId/messages/:messageId/pin')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Pins a message in the chat' })
@ChatIdApiParam
async pinMessage(
@@ -185,6 +193,7 @@ class ChatsController {
@Post(':chatId/messages/:messageId/unpin')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Unpins a message in the chat' })
@ChatIdApiParam
async unpinMessage(
@@ -198,6 +207,7 @@ class ChatsController {
@Delete(':chatId/messages')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Clears all messages from the chat' })
@ChatIdApiParam
clearMessages(
@@ -209,6 +219,7 @@ class ChatsController {
@Delete(':chatId/messages/:messageId')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ChatIdApiParam
@MessageIdApiParam
@ApiOperation({ summary: 'Deletes a message from the chat' })
@@ -222,6 +233,7 @@ class ChatsController {
@Put(':chatId/messages/:messageId')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ChatIdApiParam
@MessageIdApiParam
@ApiOperation({ summary: 'Edits a message in the chat' })
@@ -236,6 +248,7 @@ class ChatsController {
@Post(':chatId/archive')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ChatIdApiParam
@ApiOperation({ summary: 'Archive the chat' })
archiveChat(
@@ -247,6 +260,7 @@ class ChatsController {
@Post(':chatId/unarchive')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ChatIdApiParam
@ApiOperation({ summary: 'Unarchive the chat' })
unarchiveChat(
@@ -258,6 +272,7 @@ class ChatsController {
@Post(':chatId/unread')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ChatIdApiParam
@ApiOperation({ summary: 'Unread the chat' })
unreadChat(
+25 -25
View File
@@ -75,7 +75,7 @@ export class ChattingController {
@Post('/sendText')
@ApiOperation({ summary: 'Send a text message' })
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendText(@Body() request: MessageTextRequest): Promise<WAMessage> {
const whatsapp = await this.manager.getWorkingSession(request.session);
if (mentionsAll(request)) {
@@ -91,7 +91,7 @@ export class ChattingController {
description:
'Either from an URL or base64 data - look at the request schemas for details.',
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendImage(@Body() request: MessageImageRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
if (mentionsAll(request)) {
@@ -107,7 +107,7 @@ export class ChattingController {
description:
'Either from an URL or base64 data - look at the request schemas for details.',
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendFile(@Body() request: MessageFileRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
if (mentionsAll(request)) {
@@ -123,7 +123,7 @@ export class ChattingController {
description:
'Either from an URL or base64 data - look at the request schemas for details.',
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendVoice(@Body() request: MessageVoiceRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendVoice(request);
@@ -135,7 +135,7 @@ export class ChattingController {
description:
'Either from an URL or base64 data - look at the request schemas for details.',
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendVideo(@Body() request: MessageVideoRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
if (mentionsAll(request)) {
@@ -151,7 +151,7 @@ export class ChattingController {
description:
'You can use regular /api/sendText if you wanna send auto-generated link preview.',
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
@UsePipes(new WAHAValidationPipe())
async sendLinkCustomPreview(
@Body() request: MessageLinkCustomPreviewRequest,
@@ -171,7 +171,7 @@ export class ChattingController {
description: 'Send Buttons',
deprecated: true,
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
@UsePipes(new WAHAValidationPipe())
async sendButtons(@Body() request: SendButtonsRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
@@ -183,7 +183,7 @@ export class ChattingController {
summary: 'Send a list message (interactive)',
description: 'Send a List message with sections and rows',
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
@UsePipes(new WAHAValidationPipe())
async sendList(@Body() request: SendListRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
@@ -191,7 +191,7 @@ export class ChattingController {
}
@Post('/forwardMessage')
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async forwardMessage(
@Body() request: MessageForwardRequest,
): Promise<WAMessage> {
@@ -200,7 +200,7 @@ export class ChattingController {
}
@Post('/sendSeen')
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendSeen(@Body() chat: SendSeenRequest) {
const hasMessageId = chat.messageIds?.length > 0 || Boolean(chat.messageId);
if (!hasMessageId) {
@@ -216,7 +216,7 @@ export class ChattingController {
}
@Post('/startTyping')
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async startTyping(@Body() chat: ChatRequest) {
// It's infinitive action
const whatsapp = await this.manager.getWorkingSession(chat.session);
@@ -225,7 +225,7 @@ export class ChattingController {
}
@Post('/stopTyping')
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async stopTyping(@Body() chat: ChatRequest) {
const whatsapp = await this.manager.getWorkingSession(chat.session);
await whatsapp.stopTyping(chat);
@@ -234,7 +234,7 @@ export class ChattingController {
@Put('/reaction')
@ApiOperation({ summary: 'React to a message with an emoji' })
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async setReaction(@Body() request: MessageReactionRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.setReaction(request);
@@ -242,7 +242,7 @@ export class ChattingController {
@Put('/star')
@ApiOperation({ summary: 'Star or unstar a message' })
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async setStar(@Body() request: MessageStarRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
await whatsapp.setStar(request);
@@ -254,7 +254,7 @@ export class ChattingController {
summary: 'Send a poll with options',
description: 'You can use it as buttons or list replacement',
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendPoll(@Body() request: MessagePollRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendPoll(request);
@@ -265,7 +265,7 @@ export class ChattingController {
summary: 'Vote on a poll',
description: 'Cast vote(s) on an existing poll message',
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
@UsePipes(new WAHAValidationPipe())
async sendPollVote(@Body() request: MessagePollVoteRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
@@ -273,14 +273,14 @@ export class ChattingController {
}
@Post('/sendLocation')
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendLocation(@Body() request: MessageLocationRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendLocation(request);
}
@Post('/sendContactVcard')
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendContactVCard(request);
@@ -290,7 +290,7 @@ export class ChattingController {
@ApiOperation({
summary: 'Reply on a button message',
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async sendButtonsReply(@Body() request: MessageButtonReply) {
const whatsapp = await this.manager.getWorkingSession(request.session);
@@ -299,7 +299,7 @@ export class ChattingController {
@Get('/sendText')
@ApiOperation({ summary: 'Send a text message', deprecated: true })
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.Send, FromQuery('session')))
async sendTextGet(@Query() query: MessageTextQuery) {
const whatsapp = await this.manager.getWorkingSession(query.session);
const msg = new MessageTextRequest();
@@ -314,7 +314,7 @@ export class ChattingController {
description: 'DEPRECATED. Use "GET /api/chats/{id}/messages" instead',
deprecated: true,
})
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.Read, FromQuery('session')))
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getMessages(
@Query() query: GetMessageQuery,
@@ -331,7 +331,7 @@ export class ChattingController {
description: 'DEPRECATED. Use "POST /contacts/check-exists" instead',
deprecated: true,
})
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.Read, FromQuery('session')))
async DEPRECATED_checkNumberStatus(
@Query() request: CheckNumberStatusQuery,
): Promise<WANumberExistResult> {
@@ -345,7 +345,7 @@ export class ChattingController {
'DEPRECATED - you can set "reply_to" field when sending text, image, etc',
deprecated: true,
})
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async reply(@Body() request: MessageReplyRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.reply(request);
@@ -353,7 +353,7 @@ export class ChattingController {
@Post('/sendLinkPreview')
@ApiOperation({ deprecated: true })
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async sendLinkPreview_DEPRECATED(@Body() request: MessageLinkPreviewRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendLinkPreview(request);
@@ -362,7 +362,7 @@ export class ChattingController {
@Get('/:session/new-message-id')
@SessionApiParam
@ApiOperation({ summary: 'Generate a new message ID' })
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
async getNewMessageId(
@WorkingSessionParam session: WhatsappSession,
): Promise<NewMessageIDResponse> {
+7 -7
View File
@@ -37,7 +37,7 @@ export class ContactsController {
@Get('/all')
@ApiOperation({ summary: 'Get all contacts' })
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.Read, FromQuery('session')))
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getAll(
@Query() query: SessionQuery,
@@ -53,7 +53,7 @@ export class ContactsController {
description:
'The method always return result, even if the phone number is not registered in WhatsApp. For that - use /contacts/check-exists endpoint below.',
})
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.Read, FromQuery('session')))
async get(@Query() query: ContactQuery) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getContact(query);
@@ -61,7 +61,7 @@ export class ContactsController {
@Get('/check-exists')
@ApiOperation({ summary: 'Check phone number is registered in WhatsApp.' })
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.Read, FromQuery('session')))
async checkExists(
@Query() request: CheckNumberStatusQuery,
): Promise<WANumberExistResult> {
@@ -75,7 +75,7 @@ export class ContactsController {
description:
'Returns null if you do not have permission to read their status.',
})
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.Read, FromQuery('session')))
async getAbout(@Query() query: ContactQuery) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getContactAbout(query);
@@ -87,7 +87,7 @@ export class ContactsController {
description:
'If privacy settings do not allow to get the picture, the method will return null.',
})
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.Read, FromQuery('session')))
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getProfilePicture(@Query() query: ContactProfilePictureQuery) {
const whatsapp = await this.manager.getWorkingSession(query.session);
@@ -100,7 +100,7 @@ export class ContactsController {
@Post('/block')
@ApiOperation({ summary: 'Block contact' })
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async block(@Body() request: ContactRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.blockContact(request);
@@ -108,7 +108,7 @@ export class ContactsController {
@Post('/unblock')
@ApiOperation({ summary: 'Unblock contact' })
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.Send, FromBody('session')))
async unblock(@Body() request: ContactRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.unblockContact(request);
+2 -1
View File
@@ -28,12 +28,12 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/contacts')
@ApiTags('👤 Contacts')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
export class ContactsSessionController {
constructor(private manager: SessionManager) {}
@Get('/:id')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiParam({
name: 'id',
required: true,
@@ -56,6 +56,7 @@ export class ContactsSessionController {
@Put('/:chatId')
@SessionApiParam
@ChatIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Create or update contact',
description:
+1 -1
View File
@@ -30,7 +30,7 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/events')
@ApiTags('📅 Events')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
export class EventsController {
constructor(private manager: SessionManager) {}
+26 -1
View File
@@ -51,12 +51,12 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/groups')
@ApiTags('👥 Groups')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
export class GroupsController {
constructor(private manager: SessionManager) {}
@Post('')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Create a new group.' })
createGroup(
@WorkingSessionParam session: WhatsappSession,
@@ -67,6 +67,7 @@ export class GroupsController {
@Get('join-info')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get info about the group before joining.' })
async joinInfoGroup(
@WorkingSessionParam session: WhatsappSession,
@@ -79,6 +80,7 @@ export class GroupsController {
@Post('join')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Join group via code' })
async joinGroup(
@WorkingSessionParam session: WhatsappSession,
@@ -91,6 +93,7 @@ export class GroupsController {
@Get('')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get all groups.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getGroups(
@@ -105,6 +108,7 @@ export class GroupsController {
@Get('/count')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get the number of groups.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getGroupsCount(
@@ -120,6 +124,7 @@ export class GroupsController {
@Post('refresh')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Refresh groups from the server.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async refreshGroups(@WorkingSessionParam session: WhatsappSession) {
@@ -129,6 +134,7 @@ export class GroupsController {
@Get(':id')
@GroupIdApiParam
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get the group.' })
getGroup(
@WorkingSessionParam session: WhatsappSession,
@@ -140,6 +146,7 @@ export class GroupsController {
@Delete(':id')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Delete the group.' })
deleteGroup(
@WorkingSessionParam session: WhatsappSession,
@@ -152,6 +159,7 @@ export class GroupsController {
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Leave the group.' })
leaveGroup(
@WorkingSessionParam session: WhatsappSession,
@@ -163,6 +171,7 @@ export class GroupsController {
@Get(':id/picture')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get group picture' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getChatPicture(
@@ -177,6 +186,7 @@ export class GroupsController {
@Put(':id/picture')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Set group picture' })
async setPicture(
@Param('id') id: string,
@@ -190,6 +200,7 @@ export class GroupsController {
@Delete(':id/picture')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Delete group picture' })
async deletePicture(
@Param('id') id: string,
@@ -207,6 +218,7 @@ export class GroupsController {
})
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
setDescription(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
@@ -218,6 +230,7 @@ export class GroupsController {
@Put(':id/subject')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Updates the group subject',
description:
@@ -234,6 +247,7 @@ export class GroupsController {
@Put(':id/settings/security/info-admin-only')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Updates the group "info admin only" settings.',
description:
@@ -250,6 +264,7 @@ export class GroupsController {
@Get(':id/settings/security/info-admin-only')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({
summary: "Get the group's 'info admin only' settings.",
description:
@@ -265,6 +280,7 @@ export class GroupsController {
@Put(':id/settings/security/messages-admin-only')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Update settings - who can send messages',
description:
@@ -281,6 +297,7 @@ export class GroupsController {
@Get(':id/settings/security/messages-admin-only')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({
summary: 'Get settings - who can send messages',
description: 'The group settings to only allow admins to send messages.',
@@ -295,6 +312,7 @@ export class GroupsController {
@Get(':id/invite-code')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Gets the invite code for the group.' })
getInviteCode(
@WorkingSessionParam session: WhatsappSession,
@@ -307,6 +325,7 @@ export class GroupsController {
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary:
'Invalidates the current group invite code and generates a new one.',
@@ -321,6 +340,7 @@ export class GroupsController {
@Get(':id/participants/')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get participants' })
getParticipants(
@WorkingSessionParam session: WhatsappSession,
@@ -332,6 +352,7 @@ export class GroupsController {
@Get(':id/participants/v2')
@GroupIdApiParam
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get group participants.' })
getGroupParticipants(
@WorkingSessionParam session: WhatsappSession,
@@ -344,6 +365,7 @@ export class GroupsController {
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Add participants' })
addParticipants(
@WorkingSessionParam session: WhatsappSession,
@@ -357,6 +379,7 @@ export class GroupsController {
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Remove participants',
})
@@ -372,6 +395,7 @@ export class GroupsController {
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Promote participants to admin users.' })
promoteToAdmin(
@WorkingSessionParam session: WhatsappSession,
@@ -385,6 +409,7 @@ export class GroupsController {
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Demotes participants to regular users.' })
demoteToAdmin(
@WorkingSessionParam session: WhatsappSession,
+7 -1
View File
@@ -37,12 +37,12 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/labels')
@ApiTags('🏷️ Labels')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
export class LabelsController {
constructor(private manager: SessionManager) {}
@Get('/')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get all labels' })
getAll(@WorkingSessionParam session: WhatsappSession): Promise<Label[]> {
return session.getLabels();
@@ -50,6 +50,7 @@ export class LabelsController {
@Post('/')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Create a new label' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async create(
@@ -75,6 +76,7 @@ export class LabelsController {
@Put('/:labelId')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Update a label' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async update(
@@ -106,6 +108,7 @@ export class LabelsController {
@Delete('/:labelId')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Delete a label' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async delete(
@@ -123,6 +126,7 @@ export class LabelsController {
@Get('/chats/:chatId')
@SessionApiParam
@ChatIdApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get labels for the chat' })
getChatLabels(
@WorkingSessionParam session: WhatsappSession,
@@ -134,6 +138,7 @@ export class LabelsController {
@Put('/chats/:chatId')
@SessionApiParam
@ChatIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Save labels for the chat' })
putChatLabels(
@WorkingSessionParam session: WhatsappSession,
@@ -145,6 +150,7 @@ export class LabelsController {
@Get('/:labelId/chats')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get chats by label' })
getChatsByLabel(
@WorkingSessionParam session: WhatsappSession,
+1 -1
View File
@@ -33,7 +33,7 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/lids')
@ApiTags('👤 Contacts')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
export class LidsController {
constructor(private manager: SessionManager) {}
+1 -1
View File
@@ -32,7 +32,7 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/media')
@ApiTags('🖼️ Media')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
class MediaController {
constructor(private manager: SessionManager) {}
+4 -1
View File
@@ -31,12 +31,12 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/presence')
@ApiTags('✅ Presence')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
export class PresenceController {
constructor(private manager: SessionManager) {}
@Post('')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Set session presence' })
setPresence(
@WorkingSessionParam session: WhatsappSession,
@@ -67,6 +67,7 @@ export class PresenceController {
@Get('')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get all subscribed presence information.' })
getPresenceAll(
@WorkingSessionParam session: WhatsappSession,
@@ -77,6 +78,7 @@ export class PresenceController {
@Get(':chatId')
@SessionApiParam
@ChatIdApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({
summary:
"Get the presence for the chat id. If it hasn't been subscribed - it also subscribes to it.",
@@ -91,6 +93,7 @@ export class PresenceController {
@Post(':chatId/subscribe')
@SessionApiParam
@ChatIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Subscribe to presence events for the chat.',
})
+5 -1
View File
@@ -33,12 +33,12 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/profile')
@ApiTags('🆔 Profile')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
export class ProfileController {
constructor(private manager: SessionManager) {}
@Get('')
@SessionApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({ summary: 'Get my profile' })
async getMyProfile(
@WorkingSessionParam session: WhatsappSession,
@@ -57,6 +57,7 @@ export class ProfileController {
@Put('/name')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Set my profile name' })
async setProfileName(
@@ -69,6 +70,7 @@ export class ProfileController {
@Put('/status')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Set profile status (About)' })
async setProfileStatus(
@@ -81,6 +83,7 @@ export class ProfileController {
@Put('/picture')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Set profile picture' })
async setProfilePicture(
@WorkingSessionParam session: WhatsappSession,
@@ -92,6 +95,7 @@ export class ProfileController {
@Delete('/picture')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Delete profile picture' })
async deleteProfilePicture(
@WorkingSessionParam session: WhatsappSession,
+1 -1
View File
@@ -24,7 +24,7 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api')
@ApiTags('📱 Pairing')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.Control, FromQuery('session')))
export class ScreenshotController {
constructor(private manager: SessionManager) {}
+2 -2
View File
@@ -42,7 +42,7 @@ export class ServerController {
@Get('version')
@ApiOperation({ summary: 'Get the version of the server' })
@CheckPolicies(CanServer(Action.Read))
@CheckPolicies(CanServer(Action.Retrieve))
get(): WAHAEnvironment {
return VERSION;
}
@@ -76,7 +76,7 @@ export class ServerController {
@Get('status')
@ApiOperation({ summary: 'Get the server status' })
@CheckPolicies(CanServer(Action.Read))
@CheckPolicies(CanServer(Action.Retrieve))
async status(): Promise<ServerStatusResponse> {
const now = Date.now();
const uptime = Math.floor(process.uptime() * 1000);
+11 -11
View File
@@ -66,7 +66,7 @@ class SessionsController {
): Promise<SessionInfo[]> {
let sessions = await this.sessionService.getSessions(query.all);
if (!req.user?.isAdmin) {
sessions = FilterSessions(req.ability, Action.Read, sessions);
sessions = FilterSessions(req.ability, Action.Retrieve, sessions);
}
if (query.expand?.includes(SessionExpand.apps)) {
await this.sessionService.expandSessionApps(sessions);
@@ -77,7 +77,7 @@ class SessionsController {
@Get('/:session')
@ApiOperation({ summary: 'Get session information' })
@SessionApiParam
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
async get(
@Param('session') name: string,
@@ -93,7 +93,7 @@ class SessionsController {
@Get(':session/me')
@SessionApiParam
@ApiOperation({ summary: 'Get information about the authenticated account' })
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
getMe(@SessionParam session: WhatsappSession): MeInfo | null {
return this.sessionService.getSessionMe(session);
}
@@ -115,7 +115,7 @@ class SessionsController {
@ApiOperation({ summary: 'Update a session' })
@SessionApiParam
@ApiBody({ type: SessionUpdateRequest, examples: SessionExamples })
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Setting, FromParam('session')))
@UsePipes(new WAHAValidationPipe({ forbidNonWhitelisted: false }))
async update(
@Param('session') name: string,
@@ -144,7 +144,7 @@ class SessionsController {
description:
'Start the session with the given name. The session must exist. Idempotent operation.',
})
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Control, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
async start(@Param('session') name: string): Promise<SessionDTO> {
return this.sessionService.startSession(name);
@@ -156,7 +156,7 @@ class SessionsController {
summary: 'Stop the session',
description: 'Stop the session with the given name. Idempotent operation.',
})
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Control, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
async stop(@Param('session') name: string): Promise<SessionDTO> {
return this.sessionService.stopSession(name);
@@ -168,7 +168,7 @@ class SessionsController {
summary: 'Logout from the session',
description: 'Logout the session, restart a session if it was not STOPPED',
})
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Control, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
async logout(@Param('session') name: string): Promise<SessionDTO> {
return this.sessionService.logoutSession(name);
@@ -180,7 +180,7 @@ class SessionsController {
summary: 'Restart the session',
description: 'Restart the session with the given name.',
})
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
@CheckPolicies(CanSession(Action.Control, FromParam('session')))
@UsePipes(new WAHAValidationPipe())
async restart(@Param('session') name: string): Promise<SessionDTO> {
return this.sessionService.restartSession(name);
@@ -193,7 +193,7 @@ class SessionsController {
'Create session (if not exists) or update a config (if exists) and start it.',
deprecated: true,
})
@CheckPolicies(CanSession(Action.Use, FromBody('name')))
@CheckPolicies(CanSession(Action.Control, FromBody('name')))
async DEPRACATED_start(
@Body() request: SessionStartDeprecatedRequest,
): Promise<SessionDTO> {
@@ -217,7 +217,7 @@ class SessionsController {
description: 'Stop session and Logout by default.',
deprecated: true,
})
@CheckPolicies(CanSession(Action.Use, FromBody('name')))
@CheckPolicies(CanSession(Action.Control, FromBody('name')))
async DEPRECATED_stop(
@Body() request: SessionStopDeprecatedRequest,
): Promise<void> {
@@ -237,7 +237,7 @@ class SessionsController {
description: 'Stop, Logout and Delete session.',
deprecated: true,
})
@CheckPolicies(CanSession(Action.Use, FromBody('name')))
@CheckPolicies(CanSession(Action.Control, FromBody('name')))
async DEPRECATED_logout(
@Body() request: SessionLogoutDeprecatedRequest,
): Promise<void> {
+6 -1
View File
@@ -25,12 +25,12 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/:session/status')
@ApiTags('🟢 Status')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
class StatusController {
constructor(private manager: SessionManager) {}
@Post('text')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Send text status' })
sendTextStatus(
@WorkingSessionParam session: WhatsappSession,
@@ -41,6 +41,7 @@ class StatusController {
@Post('image')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Send image status' })
sendImageStatus(
@WorkingSessionParam session: WhatsappSession,
@@ -51,6 +52,7 @@ class StatusController {
@Post('voice')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Send voice status' })
sendVoiceStatus(
@WorkingSessionParam session: WhatsappSession,
@@ -61,6 +63,7 @@ class StatusController {
@Post('video')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'Send video status' })
sendVideoStatus(
@WorkingSessionParam session: WhatsappSession,
@@ -71,6 +74,7 @@ class StatusController {
@Post('delete')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({ summary: 'DELETE sent status' })
deleteStatus(
@WorkingSessionParam session: WhatsappSession,
@@ -81,6 +85,7 @@ class StatusController {
@Get('new-message-id')
@SessionApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Generate message ID you can use to batch contacts',
})
+1 -1
View File
@@ -13,7 +13,7 @@ import { Action } from '@waha/core/auth/casl.types';
@Controller('api/version')
@ApiTags('🔍 Observability')
@UseGuards(PoliciesGuard)
@CheckPolicies(CanServer(Action.Read))
@CheckPolicies(CanServer(Action.Retrieve))
export class VersionController {
@Get('')
@ApiOperation({
+2 -2
View File
@@ -86,12 +86,12 @@ export class WebsocketGatewayCore
const params = this.getParams(request);
let session: string = params.session;
const ability = this.casl.createForUser(user);
if (session == '*' && !ability.can(Action.Use, 'all')) {
if (session == '*' && !ability.can(Action.Manage, 'all')) {
// Limit user to listen only the session events
session = user.session;
}
if (!ability.can(Action.Use, new SessionName(session))) {
if (!ability.can(Action.Read, new SessionName(session))) {
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Forbidden');
return;
}
+9 -9
View File
@@ -47,7 +47,7 @@ export class AppsController {
@Get('/')
@ApiOperation({ summary: 'List all apps for a session' })
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
@CheckPolicies(CanSession(Action.App, FromQuery('session')))
@UsePipes(new WAHAValidationPipe())
async list(
@Query(new WAHAValidationPipe()) query: ListAppsQuery,
@@ -57,7 +57,7 @@ export class AppsController {
@Post('/')
@ApiOperation({ summary: 'Create a new app' })
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
@CheckPolicies(CanSession(Action.App, FromBody('session')))
@UsePipes(new WAHAValidationPipe())
async create(@Body() app: App): Promise<App> {
const result = await this.appsService.create(this.manager, app);
@@ -70,14 +70,14 @@ export class AppsController {
@Get('/:id')
@ApiOperation({ summary: 'Get app by ID' })
@CheckPolicies(CanServer(Action.Read))
@CheckPolicies(CanServer(Action.Retrieve))
@UsePipes(new WAHAValidationPipe())
async get(@Param('id') id: string, @Req() req: any): Promise<App> {
const app = await this.appsService.get(this.manager, id);
if (!app) {
throw new NotFoundException(`App '${id}' not found`);
}
if (!req.ability?.can(Action.Use, new SessionName(app.session))) {
if (!req.ability?.can(Action.App, new SessionName(app.session))) {
throw new ForbiddenException();
}
return app;
@@ -85,7 +85,7 @@ export class AppsController {
@Put('/:id')
@ApiOperation({ summary: 'Update an existing app' })
@CheckPolicies(CanServer(Action.Read))
@CheckPolicies(CanServer(Action.Retrieve))
@UsePipes(new WAHAValidationPipe())
async update(
@Param('id') id: string,
@@ -94,11 +94,11 @@ export class AppsController {
): Promise<App> {
const existing = await this.appsService.get(this.manager, id);
if (existing) {
if (!req.ability?.can(Action.Use, new SessionName(existing.session))) {
if (!req.ability?.can(Action.App, new SessionName(existing.session))) {
throw new ForbiddenException();
}
} else {
if (!req.ability?.can(Action.Use, new SessionName(app.session))) {
if (!req.ability?.can(Action.App, new SessionName(app.session))) {
throw new ForbiddenException();
}
}
@@ -121,14 +121,14 @@ export class AppsController {
@Delete('/:id')
@ApiOperation({ summary: 'Delete an app' })
@CheckPolicies(CanServer(Action.Read))
@CheckPolicies(CanServer(Action.Retrieve))
@UsePipes(new WAHAValidationPipe())
async delete(@Param('id') id: string, @Req() req: any): Promise<void> {
const existing = await this.appsService.get(this.manager, id);
if (!existing) {
throw new NotFoundException(`App '${id}' not found`);
}
if (!req.ability?.can(Action.Use, new SessionName(existing.session))) {
if (!req.ability?.can(Action.App, new SessionName(existing.session))) {
throw new ForbiddenException();
}
const app = await this.appsService.delete(this.manager, id);
+2 -2
View File
@@ -68,7 +68,7 @@ import { WAHAHealthCheckServiceCore } from './health/WAHAHealthCheckServiceCore'
import { SessionManagerCore } from './manager.core';
import { CaslAbilityFactory } from '@waha/core/auth/casl.ability';
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
import { ApiKeyService } from '@waha/core/auth/ApiKeyService';
import { ApiKeyAuthService } from './auth/ApiKeyAuthService';
import { SessionService } from '@waha/core/services/SessionService';
export const IMPORTS_CORE = [
@@ -186,7 +186,7 @@ export const PROVIDERS_BASE: Provider[] = [
MediaLocalStorageConfig,
WebSocketAuth,
ApiKeyStrategy,
ApiKeyService,
ApiKeyAuthService,
CaslAbilityFactory,
PoliciesGuard,
SessionService,
@@ -3,7 +3,7 @@ import { User } from '@waha/core/auth/apiKey.strategy';
import { SessionManager } from '@waha/core/abc/manager.abc';
@Injectable()
export class ApiKeyService {
export class ApiKeyAuthService {
constructor(private manager: SessionManager) {}
async get(apikey: string): Promise<User | null> {
@@ -17,6 +17,7 @@ export class ApiKeyService {
return {
isAdmin: key.isAdmin,
session: key.session,
actions: key.actions,
};
}
}
+4 -2
View File
@@ -1,12 +1,14 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { IApiKeyAuth } from '@waha/core/auth/auth';
import { SessionActions } from '@waha/core/auth/casl.types';
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
import { ApiKeyService } from '@waha/core/auth/ApiKeyService';
import { ApiKeyAuthService } from './ApiKeyAuthService';
export interface User {
isAdmin: boolean;
session?: string;
actions?: SessionActions | null;
}
function AdminUser(): User {
@@ -20,7 +22,7 @@ function AdminUser(): User {
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
constructor(
private auth: IApiKeyAuth,
private apiKeyService: ApiKeyService,
private apiKeyService: ApiKeyAuthService,
) {
// @ts-ignore
super({ header: 'X-Api-Key', prefix: '' }, true, (apikey, done) => {
+3 -1
View File
@@ -17,7 +17,9 @@ export class CaslAbilityFactory {
return createMongoAbility(AdminRules());
}
if (user.session) {
return createMongoAbility(SessionRules(user.session));
return createMongoAbility(
SessionRules(user.session, user.actions ?? null),
);
}
return createMongoAbility([]);
}
+60 -21
View File
@@ -1,5 +1,5 @@
import { RawRuleOf } from '@casl/ability';
import { Action, AppAbility } from './casl.types';
import { Action, AppAbility, SessionActions } from './casl.types';
export function AdminRules(): RawRuleOf<AppAbility>[] {
return [
@@ -10,36 +10,75 @@ export function AdminRules(): RawRuleOf<AppAbility>[] {
];
}
export function SessionRules(name: string): RawRuleOf<AppAbility>[] {
return [
//
// Server
//
const DefaultSessionActions: SessionActions = {
delete: false,
setting: true,
control: true,
app: true,
read: true,
send: true,
};
export function SessionRules(
name: string,
rules: SessionActions | null = null,
): RawRuleOf<AppAbility>[] {
const actions = rules ?? DefaultSessionActions;
const result: RawRuleOf<AppAbility>[] = [
{
action: 'read',
action: 'retrieve',
subject: 'server',
},
//
// Session
//
{
action: Action.List,
subject: 'session',
// "conditions" is not required here, we filter session list dynamically later
},
{
{ action: Action.Retrieve, subject: 'session', conditions: { name: name } },
];
if (actions.read) {
result.push({
action: Action.Read,
subject: 'session',
conditions: { name: name },
},
// {
// action: Action.Delete,
// subject: 'session',
// conditions: { name: name },
// },
{
action: Action.Use,
});
}
if (actions.send) {
result.push({
action: Action.Send,
subject: 'session',
conditions: { name: name },
},
];
});
}
if (actions.control) {
result.push({
action: Action.Control,
subject: 'session',
conditions: { name: name },
});
}
if (actions.setting) {
result.push({
action: Action.Setting,
subject: 'session',
conditions: { name: name },
});
}
if (actions.app) {
result.push({
action: Action.App,
subject: 'session',
conditions: { name: name },
});
}
if (actions.delete) {
result.push({
action: Action.Delete,
subject: 'session',
conditions: { name: name },
});
}
return result;
}
+25 -3
View File
@@ -12,15 +12,37 @@ export enum Action {
//
Manage = 'manage', // it's a special action in casl, meaning "any actions"
//
// Session
// Session Management
//
List = 'list', // list sessions
Read = 'read', // read session info (not messages)
Retrieve = 'retrieve', // retrieve session info (not messages)
Create = 'create', // create a new session
Delete = 'delete', // delete a session
Use = 'use', // all actions - send a message, retrieve, manage session status
Setting = 'setting', // session config: update session settings
Control = 'control', // session lifecycle: start, stop, restart, logout, authenticate
App = 'app', // manage apps
//
// Session Operations
//
Read = 'read', // read session data (messages, contacts, chats, groups, etc.)
Send = 'send', // send messages + manage session entities (groups, labels, channels, contacts, profile)
}
export type SessionActions = Partial<
Record<
Extract<
Action,
| Action.Delete
| Action.Setting
| Action.Control
| Action.App
| Action.Read
| Action.Send
>,
boolean
>
>;
type Subjects =
| InferSubjects<typeof session | typeof server | 'session' | 'server'>
| 'all';
+8
View File
@@ -25,3 +25,11 @@ export class AvailableInPlusVersion extends UnprocessableEntityException {
);
}
}
export class AvailableInPlusVersionAll extends UnprocessableEntityException {
constructor(feature: string = 'The feature') {
super(
`${feature} is available only in Plus version. Check this out: ${DOCS_URL}`,
);
}
}
+130
View File
@@ -0,0 +1,130 @@
import {
NotFoundException,
UnprocessableEntityException,
} from '@nestjs/common';
import { SessionManager } from '@waha/core/abc/manager.abc';
import {
ApiKey,
IApiKeyRepository,
} from '@waha/core/storage/IApiKeyRepository';
import { ApiKeyDTO, ApiKeyRequest } from '@waha/structures/apikeys.dto';
import { generatePrefixedId, generateSecret } from '@waha/utils/ids';
function CheckInvariant(
apiKey: Pick<ApiKey, 'isAdmin' | 'session' | 'actions'>,
): void {
if (apiKey.isAdmin && apiKey.session) {
throw new UnprocessableEntityException(
'Session is not allowed for admin keys',
);
}
if (!apiKey.isAdmin && !apiKey.session) {
throw new UnprocessableEntityException(
'Either isAdmin must be true or session must be provided',
);
}
if (apiKey.actions && !apiKey.session) {
throw new UnprocessableEntityException(
'Actions are only allowed for session-scoped keys',
);
}
}
export class ApiKeyService {
private readonly repository: IApiKeyRepository;
constructor(private readonly manager: SessionManager) {
this.repository = manager.apiKeyRepository;
}
async create(body: ApiKeyRequest): Promise<ApiKeyDTO> {
CheckInvariant(body);
if (body.session) {
const exists = await this.manager.exists(body.session);
if (!exists) {
throw new UnprocessableEntityException(
`Session "${body.session}" does not exist`,
);
}
}
let apikey: ApiKey | null = null;
for (let i = 0; i < 5; i++) {
apikey = {
id: generatePrefixedId('key_id'),
key: `key_${generateSecret(32)}`,
isActive: body.isActive,
isAdmin: body.isAdmin,
session: body.session,
actions: body.actions ?? null,
};
const idExists = await this.repository.getById(apikey.id);
if (idExists) {
continue;
}
const keyExists = await this.repository.getByKey(apikey.key);
if (keyExists) {
continue;
}
break;
}
if (!apikey) {
throw new UnprocessableEntityException(
`Failed to generate API key, try again`,
);
}
CheckInvariant(apikey);
await this.repository.upsert(apikey);
return this.toDTO(apikey);
}
async list(): Promise<ApiKeyDTO[]> {
const keys = await this.repository.list();
return keys.map((key) => this.toDTO(key));
}
async update(id: string, body: ApiKeyRequest): Promise<ApiKeyDTO> {
const existing = await this.repository.getById(id);
if (!existing) {
throw new NotFoundException('API key not found');
}
const apikey: ApiKey = {
...existing,
isActive: body.isActive,
isAdmin: body.isAdmin,
session: body.session,
actions: body.actions ?? null,
};
CheckInvariant(apikey);
if (apikey.session) {
const exists = await this.manager.exists(apikey.session);
if (!exists) {
throw new UnprocessableEntityException(
`Session "${apikey.session}" does not exist`,
);
}
}
CheckInvariant(apikey);
await this.repository.upsert(apikey);
return this.toDTO(apikey);
}
async delete(id: string): Promise<{ result: true }> {
const existing = await this.repository.getById(id);
if (!existing) {
throw new NotFoundException('API key not found');
}
await this.repository.deleteById(id);
return { result: true };
}
private toDTO(apikey: ApiKey): ApiKeyDTO {
return {
id: apikey.id,
key: apikey.key,
isActive: apikey.isActive,
isAdmin: apikey.isAdmin,
session: apikey.session,
actions: apikey.actions,
};
}
}
+4 -11
View File
@@ -1,5 +1,4 @@
import { UnprocessableEntityException } from '@nestjs/common';
import { DOCS_URL } from '@waha/core/exceptions';
import { AvailableInPlusVersionAll } from '@waha/core/exceptions';
import {
ApiKey,
IApiKeyRepository,
@@ -31,22 +30,16 @@ export class CoreApiKeyRepository implements IApiKeyRepository {
async upsert(key: ApiKey): Promise<ApiKey> {
void key;
throw new UnprocessableEntityException(
`API key management is not available in this edition. See ${DOCS_URL}`,
);
throw new AvailableInPlusVersionAll('API key management');
}
async deleteById(id: string): Promise<void> {
void id;
throw new UnprocessableEntityException(
`API key management is not available in this edition. See ${DOCS_URL}`,
);
throw new AvailableInPlusVersionAll('API key management');
}
async deleteBySession(session: string): Promise<void> {
void session;
throw new UnprocessableEntityException(
`API key management is not available in this edition. See ${DOCS_URL}`,
);
throw new AvailableInPlusVersionAll('API key management');
}
}
+2 -19
View File
@@ -1,6 +1,4 @@
import { RawRuleOf } from '@casl/ability';
import { UnprocessableEntityException } from '@nestjs/common';
import { AppAbility } from '../auth/casl.types';
import { SessionActions } from '../auth/casl.types';
export interface ApiKey {
id: string;
@@ -8,7 +6,7 @@ export interface ApiKey {
isActive: boolean;
isAdmin: boolean;
session: string | null;
rules: RawRuleOf<AppAbility>[] | null;
actions: SessionActions | null;
}
export interface IApiKeyRepository {
@@ -28,18 +26,3 @@ export interface IApiKeyRepository {
deleteBySession(session: string): Promise<void>;
}
export function CheckInvariant(
apiKey: Pick<ApiKey, 'isAdmin' | 'session'>,
): void {
if (apiKey.isAdmin && apiKey.session) {
throw new UnprocessableEntityException(
'Session is not allowed for admin keys',
);
}
if (!apiKey.isAdmin && !apiKey.session) {
throw new UnprocessableEntityException(
'Either isAdmin must be true or session must be provided',
);
}
}
+64 -2
View File
@@ -1,8 +1,61 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsNotEmpty, IsOptional, ValidateIf } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import {
IsBoolean,
IsNotEmpty,
IsOptional,
ValidateIf,
ValidateNested,
} from 'class-validator';
import { SessionActions } from '@waha/core/auth/casl.types';
import { SessionName } from '@waha/structures/sessions.dto';
export class SessionActionsDTO implements SessionActions {
@ApiProperty({
required: false,
description: 'Read session data (messages, contacts, chats, groups, etc.)',
})
@IsBoolean()
@IsOptional()
read?: boolean;
@ApiProperty({
required: false,
description:
'Send messages and manage session entities (groups, labels, channels, contacts, profile)',
})
@IsBoolean()
@IsOptional()
send?: boolean;
@ApiProperty({
required: false,
description:
'Session lifecycle: start, stop, restart, logout, authenticate',
})
@IsBoolean()
@IsOptional()
control?: boolean;
@ApiProperty({
required: false,
description: 'Session config: update session settings',
})
@IsBoolean()
@IsOptional()
setting?: boolean;
@ApiProperty({ required: false, description: 'Manage apps' })
@IsBoolean()
@IsOptional()
app?: boolean;
@ApiProperty({ required: false, description: 'Delete the session' })
@IsBoolean()
@IsOptional()
delete?: boolean;
}
export class ApiKeyDTO {
@ApiProperty({ example: 'key_id_00000000000000000000000000' })
id: string;
@@ -18,6 +71,9 @@ export class ApiKeyDTO {
@ApiProperty({ example: 'default', required: false, nullable: true })
session: string | null;
@ApiProperty({ type: SessionActionsDTO, required: false, nullable: true })
actions: SessionActions | null;
}
export class ApiKeyRequest {
@@ -34,4 +90,10 @@ export class ApiKeyRequest {
@IsOptional()
@IsBoolean()
isActive: boolean = true;
@ApiProperty({ type: SessionActionsDTO, required: false, nullable: true })
@IsOptional()
@ValidateNested()
@Type(() => SessionActionsDTO)
actions: SessionActionsDTO | null = null;
}