Merge branch 'develop' of https://gitea.vvzvlad.xyz/vvzvlad/gitmost into develop

This commit is contained in:
claude_code
2026-06-22 21:14:05 +03:00
45 changed files with 661 additions and 127 deletions

View File

@@ -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')

View File

@@ -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);
});
});

View File

@@ -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;