Skip to content

Commit 42ae2a1

Browse files
Secrets frontend (#1459)
* frontend secrets v0 * code splitting * remove viewing secrets * date picker ui fix * properly mask passwords * fix tests * fix tests * tests * fix tests --------- Co-authored-by: Lyubov Voloshko <[email protected]>
1 parent 0a43cd4 commit 42ae2a1

File tree

46 files changed

+4069
-169
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+4069
-169
lines changed

backend/src/entities/user-secret/application/data-structures/found-secret.ds.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export class FoundSecretDS {
22
id: string;
33
slug: string;
4-
value?: string;
54
companyId: string;
65
createdAt: Date;
76
updatedAt: Date;
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export class GetSecretDS {
22
userId: string;
33
slug: string;
4-
masterPassword?: string;
54
}

backend/src/entities/user-secret/application/dto/found-secret.dto.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,6 @@ export class FoundSecretDto {
1515
})
1616
slug: string;
1717

18-
@ApiProperty({
19-
type: String,
20-
required: false,
21-
description: 'Decrypted secret value (only included when retrieving a specific secret)',
22-
example: 'my-secret-value-123',
23-
})
24-
value?: string;
25-
2618
@ApiProperty({
2719
type: String,
2820
description: 'Company ID that owns this secret',
Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { ForbiddenException, GoneException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
1+
import { GoneException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
22
import AbstractUseCase from '../../../common/abstract-use.case.js';
33
import { BaseType } from '../../../common/data-injection.tokens.js';
44
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
55
import { GetSecretDS } from '../application/data-structures/get-secret.ds.js';
66
import { FoundSecretDS } from '../application/data-structures/found-secret.ds.js';
77
import { IGetSecretBySlug } from './user-secret-use-cases.interface.js';
88
import { buildFoundSecretDS } from '../utils/build-found-secret.ds.js';
9-
import { Encryptor } from '../../../helpers/encryption/encryptor.js';
10-
import { SecretActionEnum } from '../../secret-access-log/secret-access-log.entity.js';
119
import { Messages } from '../../../exceptions/text/messages.js';
1210

1311
@Injectable({ scope: Scope.REQUEST })
@@ -20,7 +18,7 @@ export class GetSecretBySlugUseCase extends AbstractUseCase<GetSecretDS, FoundSe
2018
}
2119

2220
protected async implementation(inputData: GetSecretDS): Promise<FoundSecretDS> {
23-
const { userId, slug, masterPassword } = inputData;
21+
const { userId, slug } = inputData;
2422

2523
const user = await this._dbContext.userRepository.findOne({
2624
where: { id: userId },
@@ -41,35 +39,6 @@ export class GetSecretBySlugUseCase extends AbstractUseCase<GetSecretDS, FoundSe
4139
throw new GoneException(Messages.SECRET_EXPIRED);
4240
}
4341

44-
if (secret.masterEncryption && !masterPassword) {
45-
throw new ForbiddenException(Messages.SECRET_MASTER_PASSWORD_REQUIRED);
46-
}
47-
48-
if (secret.masterEncryption && masterPassword) {
49-
const isValid = await Encryptor.verifyUserPassword(masterPassword, secret.masterHash);
50-
if (!isValid) {
51-
await this._dbContext.secretAccessLogRepository.createAccessLog(
52-
secret.id,
53-
userId,
54-
SecretActionEnum.VIEW,
55-
false,
56-
Messages.SECRET_MASTER_PASSWORD_INVALID,
57-
);
58-
throw new ForbiddenException(Messages.SECRET_MASTER_PASSWORD_INVALID);
59-
}
60-
}
61-
62-
secret.lastAccessedAt = new Date();
63-
await this._dbContext.userSecretRepository.save(secret);
64-
65-
await this._dbContext.secretAccessLogRepository.createAccessLog(secret.id, userId, SecretActionEnum.VIEW);
66-
67-
let decryptedValue = Encryptor.decryptData(secret.encryptedValue);
68-
69-
if (secret.masterEncryption && masterPassword) {
70-
decryptedValue = Encryptor.decryptDataMasterPwd(decryptedValue, masterPassword);
71-
}
72-
73-
return buildFoundSecretDS(secret, decryptedValue);
42+
return buildFoundSecretDS(secret);
7443
}
7544
}

backend/src/entities/user-secret/user-secret.controller.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,9 @@ export class UserSecretController {
128128
@ApiOperation({ summary: 'Get secret by slug' })
129129
@ApiResponse({
130130
status: 200,
131-
description: 'Returns secret details with decrypted value.',
131+
description: 'Returns secret metadata (value is never exposed).',
132132
type: FoundSecretDto,
133133
})
134-
@ApiResponse({
135-
status: 403,
136-
description: 'Master password required or incorrect.',
137-
})
138134
@ApiResponse({
139135
status: 404,
140136
description: 'Secret or user not found.',
@@ -146,15 +142,10 @@ export class UserSecretController {
146142
@ApiParam({ name: 'slug', type: String, description: 'Unique secret identifier', example: 'database-password' })
147143
@UseGuards(CompanyUserGuard)
148144
@Get('/secrets/:slug')
149-
async getSecretBySlug(
150-
@UserId() userId: string,
151-
@Param('slug') slug: string,
152-
@MasterPassword() masterPassword?: string,
153-
): Promise<FoundSecretDto> {
145+
async getSecretBySlug(@UserId() userId: string, @Param('slug') slug: string): Promise<FoundSecretDto> {
154146
const foundSecret = await this.getSecretBySlugUseCase.execute({
155147
userId,
156148
slug,
157-
masterPassword,
158149
});
159150
return buildFoundSecretDto(foundSecret);
160151
}

backend/src/entities/user-secret/utils/build-created-secret.dto.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export function buildCreatedSecretDto(ds: CreatedSecretDS): FoundSecretDto {
55
return {
66
id: ds.id,
77
slug: ds.slug,
8-
value: undefined,
98
companyId: ds.companyId,
109
createdAt: ds.createdAt,
1110
updatedAt: ds.updatedAt,

backend/src/entities/user-secret/utils/build-found-secret.ds.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { FoundSecretDS } from '../application/data-structures/found-secret.ds.js';
22
import { UserSecretEntity } from '../user-secret.entity.js';
33

4-
export function buildFoundSecretDS(secret: UserSecretEntity, decryptedValue?: string): FoundSecretDS {
4+
export function buildFoundSecretDS(secret: UserSecretEntity): FoundSecretDS {
55
return {
66
id: secret.id,
77
slug: secret.slug,
8-
value: decryptedValue,
98
companyId: secret.companyId,
109
createdAt: secret.createdAt,
1110
updatedAt: secret.updatedAt,

backend/src/entities/user-secret/utils/build-found-secret.dto.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export function buildFoundSecretDto(ds: FoundSecretDS): FoundSecretDto {
55
return {
66
id: ds.id,
77
slug: ds.slug,
8-
value: ds.value,
98
companyId: ds.companyId,
109
createdAt: ds.createdAt,
1110
updatedAt: ds.updatedAt,

backend/src/entities/user-secret/utils/build-updated-secret.dto.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export function buildUpdatedSecretDto(ds: UpdatedSecretDS): FoundSecretDto {
55
return {
66
id: ds.id,
77
slug: ds.slug,
8-
value: undefined,
98
companyId: ds.companyId,
109
createdAt: ds.createdAt,
1110
updatedAt: ds.updatedAt,

backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ test.serial(`${currentTest}?search=test - should filter secrets by slug`, async
197197
});
198198

199199
currentTest = 'GET /secrets/:slug';
200-
test.serial(`${currentTest} - should return secret with value`, async (t) => {
200+
test.serial(`${currentTest} - should return secret metadata without value`, async (t) => {
201201
// Create user with a secret to retrieve
202202
const { token } = await setupUserWithSecrets([{ slug: 'get-test-api-key', value: 'sk-get-test-value' }]);
203203

@@ -210,8 +210,7 @@ test.serial(`${currentTest} - should return secret with value`, async (t) => {
210210
t.is(response.status, 200, response.text);
211211
const responseBody = JSON.parse(response.text);
212212
t.is(responseBody.slug, 'get-test-api-key');
213-
t.truthy(responseBody.value);
214-
t.truthy(responseBody.lastAccessedAt);
213+
t.falsy(responseBody.value);
215214
});
216215

217216
test.serial(`${currentTest} - should return 404 for non-existent secret`, async (t) => {
@@ -252,39 +251,43 @@ test.serial(`${currentTest} - should create secret with master password`, async
252251
});
253252

254253
currentTest = 'GET /secrets/:slug';
255-
test.serial(`${currentTest} - should require master password for protected secret`, async (t) => {
254+
test.serial(`${currentTest} - should return protected secret metadata without master password`, async (t) => {
256255
// Create user with a protected secret
257256
const { token } = await setupUserWithSecrets([
258-
{ slug: 'protected-secret-403', value: 'sensitive-data', masterEncryption: true, masterPassword: 'SecretPass123!' },
257+
{ slug: 'protected-secret-200', value: 'sensitive-data', masterEncryption: true, masterPassword: 'SecretPass123!' },
259258
]);
260259

261-
// Try to access without master password
260+
// Access without master password should return metadata (no value is returned anyway)
262261
const response = await request(app.getHttpServer())
263-
.get('/secrets/protected-secret-403')
262+
.get('/secrets/protected-secret-200')
264263
.set('Cookie', token)
265264
.set('Content-Type', 'application/json')
266265
.set('Accept', 'application/json');
267266

268-
t.is(response.status, 403, response.text);
267+
t.is(response.status, 200, response.text);
268+
const responseBody = JSON.parse(response.text);
269+
t.is(responseBody.slug, 'protected-secret-200');
270+
t.falsy(responseBody.value);
271+
t.is(responseBody.masterEncryption, true);
269272
});
270273

271-
test.serial(`${currentTest} - should return protected secret with correct master password`, async (t) => {
274+
test.serial(`${currentTest} - should return protected secret metadata with master password (no value returned)`, async (t) => {
272275
// Create user with a protected secret
273276
const { token } = await setupUserWithSecrets([
274-
{ slug: 'protected-secret-200', value: 'sensitive-data', masterEncryption: true, masterPassword: 'MasterPass123!' },
277+
{ slug: 'protected-secret-with-pwd', value: 'sensitive-data', masterEncryption: true, masterPassword: 'MasterPass123!' },
275278
]);
276279

277280
const response = await request(app.getHttpServer())
278-
.get('/secrets/protected-secret-200')
281+
.get('/secrets/protected-secret-with-pwd')
279282
.set('Cookie', token)
280283
.set('masterpwd', 'MasterPass123!')
281284
.set('Content-Type', 'application/json')
282285
.set('Accept', 'application/json');
283286

284287
t.is(response.status, 200, response.text);
285288
const responseBody = JSON.parse(response.text);
286-
t.is(responseBody.slug, 'protected-secret-200');
287-
t.truthy(responseBody.value);
289+
t.is(responseBody.slug, 'protected-secret-with-pwd');
290+
t.falsy(responseBody.value);
288291
});
289292

290293
currentTest = 'PUT /secrets/:slug';

0 commit comments

Comments
 (0)