Merge branch 'develop' of https://gitea.vvzvlad.xyz/vvzvlad/gitmost into develop
This commit is contained in:
@@ -14,6 +14,8 @@ import { SkipThrottle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import {
|
||||
AI_CHAT_THROTTLER,
|
||||
AUTH_THROTTLER,
|
||||
PAGE_TEMPLATE_THROTTLER,
|
||||
PUBLIC_SHARE_AI_THROTTLER,
|
||||
} from '../../integrations/throttle/throttler-names';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { AuthService } from './services/auth.service';
|
||||
@@ -181,7 +183,18 @@ export class AuthController {
|
||||
return this.authService.verifyUserToken(verifyUserTokenDto, workspace.id);
|
||||
}
|
||||
|
||||
@SkipThrottle({ [AUTH_THROTTLER]: true })
|
||||
// The global ThrottlerGuard applies ALL named throttlers to every route by
|
||||
// default, so each non-AUTH bucket (AI chat, page template, public-share AI)
|
||||
// is explicitly skipped here. collab-token is auth-guarded (JwtAuthGuard),
|
||||
// per-user and client-cached, so those feature buckets are irrelevant to it;
|
||||
// skipping them avoids spurious 429s when a user opens many pages in a short
|
||||
// window. The AUTH bucket is skipped too for the same per-user, cached reason.
|
||||
@SkipThrottle({
|
||||
[AUTH_THROTTLER]: true,
|
||||
[AI_CHAT_THROTTLER]: true,
|
||||
[PAGE_TEMPLATE_THROTTLER]: true,
|
||||
[PUBLIC_SHARE_AI_THROTTLER]: true,
|
||||
})
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('collab-token')
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'reflect-metadata';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { CreateWorkspaceDto } from './create-workspace.dto';
|
||||
import { UpdateWorkspaceDto } from './update-workspace.dto';
|
||||
|
||||
// API-boundary validation for the workspace `name` field. The name is:
|
||||
// - required, 1..64 chars (MinLength/MaxLength), trimmed on input;
|
||||
// - rejected by @NoUrls when it contains a URL or a bare domain name.
|
||||
// UpdateWorkspaceDto extends CreateWorkspaceDto via PartialType, so `name`
|
||||
// stays optional there but inherits the same constraints when present.
|
||||
|
||||
async function validateCreate(payload: Record<string, unknown>) {
|
||||
const dto = plainToInstance(CreateWorkspaceDto, payload);
|
||||
return validate(dto as object);
|
||||
}
|
||||
|
||||
async function validateUpdate(payload: Record<string, unknown>) {
|
||||
const dto = plainToInstance(UpdateWorkspaceDto, payload);
|
||||
return validate(dto as object);
|
||||
}
|
||||
|
||||
function hasError(errors: any[], property: string, constraint?: string) {
|
||||
const err = errors.find((e) => e.property === property);
|
||||
if (!err) return false;
|
||||
if (!constraint) return true;
|
||||
return Object.keys(err.constraints ?? {}).includes(constraint);
|
||||
}
|
||||
|
||||
describe('CreateWorkspaceDto.name validation', () => {
|
||||
it('accepts a plain workspace name', async () => {
|
||||
const errors = await validateCreate({ name: 'My Workspace' });
|
||||
expect(hasError(errors, 'name')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a name containing a URL with the noUrls error', async () => {
|
||||
const errors = await validateCreate({
|
||||
name: 'Visit https://evil.com now',
|
||||
});
|
||||
expect(hasError(errors, 'name', 'noUrls')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a name containing a bare domain with the noUrls error', async () => {
|
||||
const errors = await validateCreate({ name: 'evil.com workspace' });
|
||||
expect(hasError(errors, 'name', 'noUrls')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects an empty name with a minLength error', async () => {
|
||||
const errors = await validateCreate({ name: '' });
|
||||
expect(hasError(errors, 'name', 'minLength')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts exactly 64 characters', async () => {
|
||||
const errors = await validateCreate({ name: 'a'.repeat(64) });
|
||||
expect(hasError(errors, 'name')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects 65 characters with a maxLength error', async () => {
|
||||
const errors = await validateCreate({ name: 'a'.repeat(65) });
|
||||
expect(hasError(errors, 'name', 'maxLength')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateWorkspaceDto.name validation (inherited)', () => {
|
||||
it('accepts a plain workspace name', async () => {
|
||||
const errors = await validateUpdate({ name: 'My Workspace' });
|
||||
expect(hasError(errors, 'name')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a name containing a URL with the noUrls error', async () => {
|
||||
const errors = await validateUpdate({
|
||||
name: 'Visit https://evil.com now',
|
||||
});
|
||||
expect(hasError(errors, 'name', 'noUrls')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts an omitted name (optional via PartialType)', async () => {
|
||||
const errors = await validateUpdate({});
|
||||
expect(hasError(errors, 'name')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import { NoUrls } from '../../../common/validators/no-urls.validator';
|
||||
|
||||
export class CreateWorkspaceDto {
|
||||
@MinLength(1)
|
||||
@MaxLength(64)
|
||||
@IsString()
|
||||
@NoUrls()
|
||||
@Transform(({ value }: TransformFnParams) => value?.trim())
|
||||
name: string;
|
||||
|
||||
|
||||
@@ -86,7 +86,9 @@ export class AiSettingsController {
|
||||
) {
|
||||
this.assertAdmin(user, workspace);
|
||||
await this.aiSettingsService.reindex(workspace.id);
|
||||
// Return refreshed masked settings so the client can update the counter.
|
||||
// Indexing runs as an async background job, so these masked settings carry
|
||||
// the PRE-job counts (the indexed total has not climbed yet). The client
|
||||
// polls this endpoint's GET counterpart to watch the counter advance.
|
||||
return this.aiSettingsService.getMasked(workspace.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { ServiceUnavailableException } from '@nestjs/common';
|
||||
*/
|
||||
export class AiSttNotConfiguredException extends ServiceUnavailableException {
|
||||
constructor() {
|
||||
super('AI STT model not configured');
|
||||
// User-facing copy: the client surfaces this 503 message verbatim in the
|
||||
// dictation toast, so keep it consistent with the client's fallback copy.
|
||||
super('Voice dictation is not configured');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user