version-0.1 #1
9
src/admin/package-lock.json
generated
9
src/admin/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "mic-check-admin",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@iconify-json/bxl": "^1.2.0",
|
||||
"@iconify-json/ri": "^1.2.0",
|
||||
"@iconify/vue": "^4.1.0",
|
||||
@@ -2002,6 +2003,14 @@
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/inter": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz",
|
||||
"integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"test": "jest --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@iconify-json/bxl": "^1.2.0",
|
||||
"@iconify-json/ri": "^1.2.0",
|
||||
"@iconify/vue": "^4.1.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$font-family-custom: "Inter", sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto,
|
||||
$font-family-custom: "Inter Variable", sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto,
|
||||
"Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
/* stylelint-disable length-zero-no-unit */
|
||||
@forward "../../../base/libs/vuetify/variables" with (
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ApiKeyResponse, CreateApiKeyRequest, CreateApiKeyResponse } from '
|
||||
|
||||
export async function listApiKeys(organizationId: number): Promise<ApiKeyResponse[]> {
|
||||
const { data } = await apiClient.get<ApiKeyResponse[]>(
|
||||
`/v1/organisations/${organizationId}/api-keys`,
|
||||
`/v1/organisation/${organizationId}/api-keys`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -13,12 +13,12 @@ export async function createApiKey(
|
||||
request: CreateApiKeyRequest,
|
||||
): Promise<CreateApiKeyResponse> {
|
||||
const { data } = await apiClient.post<CreateApiKeyResponse>(
|
||||
`/v1/organisations/${organizationId}/api-keys`,
|
||||
`/v1/organisation/${organizationId}/api-keys`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteApiKey(organizationId: number, keyId: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/organisations/${organizationId}/api-keys/${keyId}`);
|
||||
await apiClient.delete(`/v1/organisation/${organizationId}/api-key/${keyId}`);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function listAuditLogsByOrganization(
|
||||
filter: AuditLogFilter = {},
|
||||
): Promise<PaginatedResponse<AuditLogResponse>> {
|
||||
const { data } = await apiClient.get<PaginatedResponse<AuditLogResponse>>(
|
||||
`/v1/organisations/${orgId}/audit-logs`,
|
||||
`/v1/organisation/${orgId}/audit-logs`,
|
||||
{ params: toParams(filter) },
|
||||
);
|
||||
return data;
|
||||
@@ -28,7 +28,7 @@ export async function listAuditLogsByProject(
|
||||
filter: AuditLogFilter = {},
|
||||
): Promise<PaginatedResponse<AuditLogResponse>> {
|
||||
const { data } = await apiClient.get<PaginatedResponse<AuditLogResponse>>(
|
||||
`/v1/projects/${projectId}/audit-logs`,
|
||||
`/v1/project/${projectId}/audit-logs`,
|
||||
{ params: toParams(filter) },
|
||||
);
|
||||
return data;
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function listEnvironments(projectId: number, page = 1, pageSize = 1
|
||||
}
|
||||
|
||||
export async function getEnvironment(apiKey: string): Promise<EnvironmentResponse> {
|
||||
const { data } = await apiClient.get<EnvironmentResponse>(`/v1/environments/${apiKey}`);
|
||||
const { data } = await apiClient.get<EnvironmentResponse>(`/v1/environment/${apiKey}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -26,17 +26,17 @@ export async function createEnvironment(request: CreateEnvironmentRequest): Prom
|
||||
}
|
||||
|
||||
export async function updateEnvironment(apiKey: string, request: UpdateEnvironmentRequest): Promise<EnvironmentResponse> {
|
||||
const { data } = await apiClient.put<EnvironmentResponse>(`/v1/environments/${apiKey}`, request);
|
||||
const { data } = await apiClient.put<EnvironmentResponse>(`/v1/environment/${apiKey}`, request);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteEnvironment(apiKey: string): Promise<void> {
|
||||
await apiClient.delete(`/v1/environments/${apiKey}`);
|
||||
await apiClient.delete(`/v1/environment/${apiKey}`);
|
||||
}
|
||||
|
||||
export async function cloneEnvironment(apiKey: string, request: CloneEnvironmentRequest): Promise<EnvironmentResponse> {
|
||||
const { data } = await apiClient.post<EnvironmentResponse>(
|
||||
`/v1/environments/${apiKey}/clone`,
|
||||
`/v1/environment/${apiKey}/clone`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function listFeatureSegments(
|
||||
featureId: number,
|
||||
): Promise<FeatureSegmentResponse[]> {
|
||||
const { data } = await apiClient.get<FeatureSegmentResponse[]>(
|
||||
`/v1/environments/${envApiKey}/features/${featureId}/segments`,
|
||||
`/v1/environment/${envApiKey}/feature/${featureId}/segments`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export async function createFeatureSegment(
|
||||
request: CreateFeatureSegmentRequest,
|
||||
): Promise<FeatureSegmentResponse> {
|
||||
const { data } = await apiClient.post<FeatureSegmentResponse>(
|
||||
`/v1/environments/${envApiKey}/features/${featureId}/segments`,
|
||||
`/v1/environment/${envApiKey}/feature/${featureId}/segments`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -35,7 +35,7 @@ export async function updateFeatureSegment(
|
||||
request: UpdateFeatureSegmentRequest,
|
||||
): Promise<FeatureSegmentResponse> {
|
||||
const { data } = await apiClient.put<FeatureSegmentResponse>(
|
||||
`/v1/environments/${envApiKey}/features/${featureId}/segments/${id}`,
|
||||
`/v1/environment/${envApiKey}/feature/${featureId}/segment/${id}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -46,7 +46,7 @@ export async function deleteFeatureSegment(
|
||||
featureId: number,
|
||||
id: number,
|
||||
): Promise<void> {
|
||||
await apiClient.delete(`/v1/environments/${envApiKey}/features/${featureId}/segments/${id}`);
|
||||
await apiClient.delete(`/v1/environment/${envApiKey}/feature/${featureId}/segment/${id}`);
|
||||
}
|
||||
|
||||
export async function listIdentitySegments(
|
||||
@@ -54,7 +54,7 @@ export async function listIdentitySegments(
|
||||
identityId: number,
|
||||
): Promise<SegmentSummaryResponse[]> {
|
||||
const { data } = await apiClient.get<SegmentSummaryResponse[]>(
|
||||
`/v1/environments/${envApiKey}/identities/${identityId}/segments`,
|
||||
`/v1/environment/${envApiKey}/identity/${identityId}/segments`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import type {
|
||||
|
||||
export async function listFeatureStates(envApiKey: string): Promise<FeatureStateResponse[]> {
|
||||
const { data } = await apiClient.get<FeatureStateResponse[]>(
|
||||
`/v1/environments/${envApiKey}/featurestates`,
|
||||
`/v1/environment/${envApiKey}/featurestates`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getFeatureState(envApiKey: string, id: number): Promise<FeatureStateResponse> {
|
||||
const { data } = await apiClient.get<FeatureStateResponse>(
|
||||
`/v1/environments/${envApiKey}/featurestates/${id}`,
|
||||
`/v1/environment/${envApiKey}/featurestate/${id}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export async function updateFeatureState(
|
||||
request: UpdateFeatureStateRequest,
|
||||
): Promise<FeatureStateResponse> {
|
||||
const { data } = await apiClient.put<FeatureStateResponse>(
|
||||
`/v1/environments/${envApiKey}/featurestates/${id}`,
|
||||
`/v1/environment/${envApiKey}/featurestate/${id}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -37,7 +37,7 @@ export async function patchFeatureState(
|
||||
request: PatchFeatureStateRequest,
|
||||
): Promise<FeatureStateResponse> {
|
||||
const { data } = await apiClient.patch<FeatureStateResponse>(
|
||||
`/v1/environments/${envApiKey}/featurestates/${id}`,
|
||||
`/v1/environment/${envApiKey}/featurestate/${id}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function listFeatures(
|
||||
pageSize = 100,
|
||||
): Promise<PaginatedResponse<FeatureResponse>> {
|
||||
const { data } = await apiClient.get<PaginatedResponse<FeatureResponse>>(
|
||||
`/v1/projects/${projectId}/features`,
|
||||
`/v1/project/${projectId}/features`,
|
||||
{ params: { page, pageSize } },
|
||||
);
|
||||
return data;
|
||||
@@ -21,7 +21,7 @@ export async function listFeatures(
|
||||
|
||||
export async function getFeature(projectId: number, id: number): Promise<FeatureResponse> {
|
||||
const { data } = await apiClient.get<FeatureResponse>(
|
||||
`/v1/projects/${projectId}/features/${id}`,
|
||||
`/v1/project/${projectId}/feature/${id}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export async function createFeature(
|
||||
request: CreateFeatureRequest,
|
||||
): Promise<FeatureResponse> {
|
||||
const { data } = await apiClient.post<FeatureResponse>(
|
||||
`/v1/projects/${projectId}/features`,
|
||||
`/v1/project/${projectId}/features`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -43,7 +43,7 @@ export async function updateFeature(
|
||||
request: UpdateFeatureRequest,
|
||||
): Promise<FeatureResponse> {
|
||||
const { data } = await apiClient.put<FeatureResponse>(
|
||||
`/v1/projects/${projectId}/features/${id}`,
|
||||
`/v1/project/${projectId}/feature/${id}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -55,12 +55,34 @@ export async function patchFeature(
|
||||
request: PatchFeatureRequest,
|
||||
): Promise<FeatureResponse> {
|
||||
const { data } = await apiClient.patch<FeatureResponse>(
|
||||
`/v1/projects/${projectId}/features/${id}`,
|
||||
`/v1/project/${projectId}/feature/${id}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteFeature(projectId: number, id: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/projects/${projectId}/features/${id}`);
|
||||
await apiClient.delete(`/v1/project/${projectId}/feature/${id}`);
|
||||
}
|
||||
|
||||
export async function assignFeatureTag(
|
||||
projectId: number,
|
||||
featureId: number,
|
||||
tagId: number,
|
||||
): Promise<FeatureResponse> {
|
||||
const { data } = await apiClient.put<FeatureResponse>(
|
||||
`/v1/project/${projectId}/feature/${featureId}/tag/${tagId}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function removeFeatureTag(
|
||||
projectId: number,
|
||||
featureId: number,
|
||||
tagId: number,
|
||||
): Promise<FeatureResponse> {
|
||||
const { data } = await apiClient.delete<FeatureResponse>(
|
||||
`/v1/project/${projectId}/feature/${featureId}/tag/${tagId}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function listIdentities(
|
||||
pageSize = 100,
|
||||
): Promise<PaginatedResponse<IdentityResponse>> {
|
||||
const { data } = await apiClient.get<PaginatedResponse<IdentityResponse>>(
|
||||
`/v1/environments/${envApiKey}/identities`,
|
||||
`/v1/environment/${envApiKey}/identities`,
|
||||
{ params: { page, pageSize } },
|
||||
);
|
||||
return data;
|
||||
@@ -26,7 +26,7 @@ export async function createIdentity(
|
||||
request: CreateIdentityRequest,
|
||||
): Promise<IdentityResponse> {
|
||||
const { data } = await apiClient.post<IdentityResponse>(
|
||||
`/v1/environments/${envApiKey}/identities`,
|
||||
`/v1/environment/${envApiKey}/identities`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -34,7 +34,7 @@ export async function createIdentity(
|
||||
|
||||
export async function getIdentity(envApiKey: string, id: number): Promise<IdentityResponse> {
|
||||
const { data } = await apiClient.get<IdentityResponse>(
|
||||
`/v1/environments/${envApiKey}/identities/${id}`,
|
||||
`/v1/environment/${envApiKey}/identity/${id}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export async function upsertIdentityTrait(
|
||||
request: UpsertTraitRequest,
|
||||
): Promise<TraitResponse> {
|
||||
const { data } = await apiClient.put<TraitResponse>(
|
||||
`/v1/environments/${envApiKey}/identities/${identityId}/traits/${encodeURIComponent(key)}`,
|
||||
`/v1/environment/${envApiKey}/identity/${identityId}/trait/${encodeURIComponent(key)}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -58,12 +58,12 @@ export async function deleteIdentityTrait(
|
||||
key: string,
|
||||
): Promise<void> {
|
||||
await apiClient.delete(
|
||||
`/v1/environments/${envApiKey}/identities/${identityId}/traits/${encodeURIComponent(key)}`,
|
||||
`/v1/environment/${envApiKey}/identity/${identityId}/trait/${encodeURIComponent(key)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteIdentity(envApiKey: string, id: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/environments/${envApiKey}/identities/${id}`);
|
||||
await apiClient.delete(`/v1/environment/${envApiKey}/identity/${id}`);
|
||||
}
|
||||
|
||||
export async function getIdentityFeatureStates(
|
||||
@@ -71,7 +71,7 @@ export async function getIdentityFeatureStates(
|
||||
identityId: number,
|
||||
): Promise<FeatureStateResponse[]> {
|
||||
const { data } = await apiClient.get<FeatureStateResponse[]>(
|
||||
`/v1/environments/${envApiKey}/identities/${identityId}/featurestates`,
|
||||
`/v1/environment/${envApiKey}/identity/${identityId}/featurestates`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export async function setIdentityFeatureState(
|
||||
request: UpdateFeatureStateRequest,
|
||||
): Promise<FeatureStateResponse> {
|
||||
const { data } = await apiClient.put<FeatureStateResponse>(
|
||||
`/v1/environments/${envApiKey}/identities/${identityId}/featurestates/${featureId}`,
|
||||
`/v1/environment/${envApiKey}/identity/${identityId}/featurestate/${featureId}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -95,6 +95,6 @@ export async function deleteIdentityFeatureState(
|
||||
featureId: number,
|
||||
): Promise<void> {
|
||||
await apiClient.delete(
|
||||
`/v1/environments/${envApiKey}/identities/${identityId}/featurestates/${featureId}`,
|
||||
`/v1/environment/${envApiKey}/identity/${identityId}/featurestate/${featureId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function listOrganizations(page = 1, pageSize = 100): Promise<Organ
|
||||
}
|
||||
|
||||
export async function getOrganization(id: number): Promise<OrganizationResponse> {
|
||||
const { data } = await apiClient.get<OrganizationResponse>(`/v1/organisations/${id}`);
|
||||
const { data } = await apiClient.get<OrganizationResponse>(`/v1/organisation/${id}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -30,27 +30,27 @@ export async function createOrganization(request: CreateOrganizationRequest): Pr
|
||||
}
|
||||
|
||||
export async function updateOrganization(id: number, request: UpdateOrganizationRequest): Promise<OrganizationResponse> {
|
||||
const { data } = await apiClient.put<OrganizationResponse>(`/v1/organisations/${id}`, request);
|
||||
const { data } = await apiClient.put<OrganizationResponse>(`/v1/organisation/${id}`, request);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteOrganization(id: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/organisations/${id}`);
|
||||
await apiClient.delete(`/v1/organisation/${id}`);
|
||||
}
|
||||
|
||||
export async function listOrganizationMembers(organizationId: number): Promise<OrganizationMemberResponse[]> {
|
||||
const { data } = await apiClient.get<OrganizationMemberResponse[]>(
|
||||
`/v1/organisations/${organizationId}/users`,
|
||||
`/v1/organisation/${organizationId}/users`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function inviteOrganizationMember(organizationId: number, request: InviteUserRequest): Promise<void> {
|
||||
await apiClient.post(`/v1/organisations/${organizationId}/users/invite`, request);
|
||||
await apiClient.post(`/v1/organisation/${organizationId}/users/invite`, request);
|
||||
}
|
||||
|
||||
export async function removeOrganizationMember(organizationId: number, userId: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/organisations/${organizationId}/users/${userId}`);
|
||||
await apiClient.delete(`/v1/organisation/${organizationId}/user/${userId}`);
|
||||
}
|
||||
|
||||
export async function inviteOrganizationMembersByEmail(
|
||||
@@ -58,7 +58,7 @@ export async function inviteOrganizationMembersByEmail(
|
||||
request: InviteUsersByEmailRequest,
|
||||
): Promise<InviteByEmailResult[]> {
|
||||
const { data } = await apiClient.post<InviteByEmailResult[]>(
|
||||
`/v1/organisations/${organizationId}/users/invite-by-email`,
|
||||
`/v1/organisation/${organizationId}/users/invite-by-email`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -66,14 +66,14 @@ export async function inviteOrganizationMembersByEmail(
|
||||
|
||||
export async function getInviteLink(organizationId: number): Promise<InviteTokenResponse> {
|
||||
const { data } = await apiClient.get<InviteTokenResponse>(
|
||||
`/v1/organisations/${organizationId}/invite-link`,
|
||||
`/v1/organisation/${organizationId}/invite-link`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function regenerateInviteLink(organizationId: number): Promise<InviteTokenResponse> {
|
||||
const { data } = await apiClient.post<InviteTokenResponse>(
|
||||
`/v1/organisations/${organizationId}/invite-link/regenerate`,
|
||||
`/v1/organisation/${organizationId}/invite-link/regenerate`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -83,5 +83,5 @@ export async function acceptInvite(token: string): Promise<void> {
|
||||
}
|
||||
|
||||
export async function setPrimaryOrganization(id: number): Promise<void> {
|
||||
await apiClient.put(`/v1/organisations/${id}/primary`);
|
||||
await apiClient.put(`/v1/organisation/${id}/primary`);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function listProjects(organizationId: number, page = 1, pageSize =
|
||||
}
|
||||
|
||||
export async function getProject(id: number): Promise<ProjectResponse> {
|
||||
const { data } = await apiClient.get<ProjectResponse>(`/v1/projects/${id}`);
|
||||
const { data } = await apiClient.get<ProjectResponse>(`/v1/project/${id}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -27,17 +27,17 @@ export async function createProject(request: CreateProjectRequest): Promise<Proj
|
||||
}
|
||||
|
||||
export async function updateProject(id: number, request: UpdateProjectRequest): Promise<ProjectResponse> {
|
||||
const { data } = await apiClient.put<ProjectResponse>(`/v1/projects/${id}`, request);
|
||||
const { data } = await apiClient.put<ProjectResponse>(`/v1/project/${id}`, request);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteProject(id: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/projects/${id}`);
|
||||
await apiClient.delete(`/v1/project/${id}`);
|
||||
}
|
||||
|
||||
export async function listProjectUserPermissions(projectId: number): Promise<UserPermissionResponse[]> {
|
||||
const { data } = await apiClient.get<UserPermissionResponse[]>(
|
||||
`/v1/projects/${projectId}/user-permissions`,
|
||||
`/v1/project/${projectId}/user-permissions`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export async function setProjectUserPermissions(
|
||||
request: SetUserPermissionsRequest,
|
||||
): Promise<UserPermissionResponse> {
|
||||
const { data } = await apiClient.post<UserPermissionResponse>(
|
||||
`/v1/projects/${projectId}/user-permissions`,
|
||||
`/v1/project/${projectId}/user-permissions`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -59,12 +59,12 @@ export async function updateProjectUserPermissions(
|
||||
request: SetUserPermissionsRequest,
|
||||
): Promise<UserPermissionResponse> {
|
||||
const { data } = await apiClient.put<UserPermissionResponse>(
|
||||
`/v1/projects/${projectId}/user-permissions/${userId}`,
|
||||
`/v1/project/${projectId}/user-permission/${userId}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function removeProjectUserPermissions(projectId: number, userId: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/projects/${projectId}/user-permissions/${userId}`);
|
||||
await apiClient.delete(`/v1/project/${projectId}/user-permission/${userId}`);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
|
||||
export async function listSegments(projectId: number, page = 1, pageSize = 100): Promise<SegmentResponse[]> {
|
||||
const { data } = await apiClient.get<PaginatedResponse<SegmentResponse>>(
|
||||
`/v1/projects/${projectId}/segments`,
|
||||
`/v1/project/${projectId}/segments`,
|
||||
{ params: { page, pageSize } },
|
||||
);
|
||||
return data.results;
|
||||
@@ -16,7 +16,7 @@ export async function listSegments(projectId: number, page = 1, pageSize = 100):
|
||||
|
||||
export async function getSegment(projectId: number, id: number): Promise<SegmentResponse> {
|
||||
const { data } = await apiClient.get<SegmentResponse>(
|
||||
`/v1/projects/${projectId}/segments/${id}`,
|
||||
`/v1/project/${projectId}/segment/${id}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export async function createSegment(
|
||||
request: CreateSegmentRequest,
|
||||
): Promise<SegmentResponse> {
|
||||
const { data } = await apiClient.post<SegmentResponse>(
|
||||
`/v1/projects/${projectId}/segments`,
|
||||
`/v1/project/${projectId}/segments`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -38,12 +38,12 @@ export async function updateSegment(
|
||||
request: UpdateSegmentRequest,
|
||||
): Promise<SegmentResponse> {
|
||||
const { data } = await apiClient.put<SegmentResponse>(
|
||||
`/v1/projects/${projectId}/segments/${id}`,
|
||||
`/v1/project/${projectId}/segment/${id}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteSegment(projectId: number, id: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/projects/${projectId}/segments/${id}`);
|
||||
await apiClient.delete(`/v1/project/${projectId}/segment/${id}`);
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import apiClient from './client';
|
||||
import type { TagResponse, CreateTagRequest } from '@/types/api';
|
||||
|
||||
export async function listTags(projectId: number): Promise<TagResponse[]> {
|
||||
const { data } = await apiClient.get<TagResponse[]>(`/v1/projects/${projectId}/tags`);
|
||||
const { data } = await apiClient.get<TagResponse[]>(`/v1/project/${projectId}/tags`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createTag(projectId: number, request: CreateTagRequest): Promise<TagResponse> {
|
||||
const { data } = await apiClient.post<TagResponse>(`/v1/projects/${projectId}/tags`, request);
|
||||
const { data } = await apiClient.post<TagResponse>(`/v1/project/${projectId}/tags`, request);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteTag(projectId: number, id: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/projects/${projectId}/tags/${id}`);
|
||||
await apiClient.delete(`/v1/project/${projectId}/tag/${id}`);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ import apiClient from './client';
|
||||
import type { DashboardUsageResponse } from '@/types/api';
|
||||
|
||||
export async function getUsageDashboard(environmentId: number, days = 14): Promise<DashboardUsageResponse> {
|
||||
const { data } = await apiClient.get<DashboardUsageResponse>(`/v1/environments/${environmentId}/usage`, { params: { days } });
|
||||
const { data } = await apiClient.get<DashboardUsageResponse>(`/v1/environment/${environmentId}/usage`, { params: { days } });
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@ import apiClient from './client';
|
||||
import type { UserProfileResponse, UpdateProfileRequest, ChangePasswordRequest } from '@/types/api';
|
||||
|
||||
export async function getMe(): Promise<UserProfileResponse> {
|
||||
const { data } = await apiClient.get<UserProfileResponse>('/v1/users/me');
|
||||
const { data } = await apiClient.get<UserProfileResponse>('/v1/user/me');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateMe(request: UpdateProfileRequest): Promise<UserProfileResponse> {
|
||||
const { data } = await apiClient.put<UserProfileResponse>('/v1/users/me', request);
|
||||
const { data } = await apiClient.put<UserProfileResponse>('/v1/user/me', request);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function changePassword(request: ChangePasswordRequest): Promise<void> {
|
||||
await apiClient.post('/v1/users/me/change-password', request);
|
||||
await apiClient.post('/v1/user/me/change-password', request);
|
||||
}
|
||||
|
||||
export async function getUserById(id: number): Promise<UserProfileResponse> {
|
||||
const { data } = await apiClient.get<UserProfileResponse>(`/v1/users/${id}`);
|
||||
const { data } = await apiClient.get<UserProfileResponse>(`/v1/user/${id}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
// ─── Organization webhooks ────────────────────────────────────────────────────
|
||||
|
||||
export async function listOrgWebhooks(orgId: number): Promise<WebhookResponse[]> {
|
||||
const { data } = await apiClient.get<WebhookResponse[]>(`/v1/organisations/${orgId}/webhooks`);
|
||||
const { data } = await apiClient.get<WebhookResponse[]>(`/v1/organisation/${orgId}/webhooks`);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function createOrgWebhook(
|
||||
request: CreateWebhookRequest,
|
||||
): Promise<WebhookResponse> {
|
||||
const { data } = await apiClient.post<WebhookResponse>(
|
||||
`/v1/organisations/${orgId}/webhooks`,
|
||||
`/v1/organisation/${orgId}/webhooks`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -29,21 +29,21 @@ export async function updateOrgWebhook(
|
||||
request: CreateWebhookRequest,
|
||||
): Promise<WebhookResponse> {
|
||||
const { data } = await apiClient.put<WebhookResponse>(
|
||||
`/v1/organisations/${orgId}/webhooks/${webhookId}`,
|
||||
`/v1/organisation/${orgId}/webhook/${webhookId}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteOrgWebhook(orgId: number, webhookId: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/organisations/${orgId}/webhooks/${webhookId}`);
|
||||
await apiClient.delete(`/v1/organisation/${orgId}/webhook/${webhookId}`);
|
||||
}
|
||||
|
||||
// ─── Environment webhooks ─────────────────────────────────────────────────────
|
||||
|
||||
export async function listEnvWebhooks(envApiKey: string): Promise<WebhookResponse[]> {
|
||||
const { data } = await apiClient.get<WebhookResponse[]>(
|
||||
`/v1/environments/${envApiKey}/webhooks`,
|
||||
`/v1/environment/${envApiKey}/webhooks`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export async function createEnvWebhook(
|
||||
request: CreateWebhookRequest,
|
||||
): Promise<WebhookResponse> {
|
||||
const { data } = await apiClient.post<WebhookResponse>(
|
||||
`/v1/environments/${envApiKey}/webhooks`,
|
||||
`/v1/environment/${envApiKey}/webhooks`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
@@ -65,14 +65,14 @@ export async function updateEnvWebhook(
|
||||
request: CreateWebhookRequest,
|
||||
): Promise<WebhookResponse> {
|
||||
const { data } = await apiClient.put<WebhookResponse>(
|
||||
`/v1/environments/${envApiKey}/webhooks/${webhookId}`,
|
||||
`/v1/environment/${envApiKey}/webhook/${webhookId}`,
|
||||
request,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteEnvWebhook(envApiKey: string, webhookId: number): Promise<void> {
|
||||
await apiClient.delete(`/v1/environments/${envApiKey}/webhooks/${webhookId}`);
|
||||
await apiClient.delete(`/v1/environment/${envApiKey}/webhook/${webhookId}`);
|
||||
}
|
||||
|
||||
// ─── Delivery logs ────────────────────────────────────────────────────────────
|
||||
@@ -82,7 +82,7 @@ export async function listWebhookDeliveries(
|
||||
webhookId: number,
|
||||
): Promise<WebhookDeliveryLogResponse[]> {
|
||||
const { data } = await apiClient.get<WebhookDeliveryLogResponse[]>(
|
||||
`/v1/environments/${envApiKey}/webhooks/${webhookId}/deliveries`,
|
||||
`/v1/environment/${envApiKey}/webhook/${webhookId}/deliveries`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<v-divider class="my-5" />
|
||||
|
||||
<!-- Tags -->
|
||||
<TagsChipInput />
|
||||
<TagsChipInput v-if="feature" :feature="feature" @updated="onTagsUpdated" />
|
||||
</template>
|
||||
</v-tabs-window-item>
|
||||
|
||||
@@ -558,6 +558,10 @@ async function onSaveSettings(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function onTagsUpdated(updated: FeatureResponse): void {
|
||||
emit('updated', updated);
|
||||
}
|
||||
|
||||
async function onDelete(): Promise<void> {
|
||||
if (!props.feature || deleteConfirmName.value !== props.feature.name) return;
|
||||
const projectId = contextStore.currentProject?.id;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div data-testid="tags-chip-input">
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<span class="text-body-2 font-weight-medium">Project Tags</span>
|
||||
<span class="text-body-2 font-weight-medium">Tags</span>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
@@ -71,6 +71,8 @@
|
||||
</v-card>
|
||||
</v-expand-transition>
|
||||
|
||||
<p class="text-caption text-medium-emphasis mb-2">Click a tag to add or remove it from this feature.</p>
|
||||
|
||||
<!-- Tag chips -->
|
||||
<div v-if="isLoading" class="d-flex justify-center py-3">
|
||||
<v-progress-circular indeterminate size="20" width="2" color="primary" />
|
||||
@@ -82,8 +84,12 @@
|
||||
:key="tag.id"
|
||||
size="small"
|
||||
closable
|
||||
:variant="isAssigned(tag) ? 'flat' : 'outlined'"
|
||||
:color="tag.color"
|
||||
:prepend-icon="isAssigned(tag) ? 'ri-check-line' : undefined"
|
||||
:loading="togglingTagId === tag.id"
|
||||
:data-testid="`tag-chip-${tag.label}`"
|
||||
@click="onToggleTag(tag)"
|
||||
@click:close="onDeleteTag(tag)"
|
||||
>
|
||||
{{ tag.label }}
|
||||
@@ -103,14 +109,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||
import { listTags, createTag, deleteTag } from '@/api/tags';
|
||||
import { assignFeatureTag, removeFeatureTag } from '@/api/features';
|
||||
import { useContextStore } from '@/stores/context';
|
||||
import type { TagResponse } from '@/types/api';
|
||||
import type { TagResponse, FeatureResponse } from '@/types/api';
|
||||
|
||||
interface Props {
|
||||
feature: FeatureResponse;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
updated: [feature: FeatureResponse];
|
||||
}>();
|
||||
|
||||
const contextStore = useContextStore();
|
||||
|
||||
const tags = ref<TagResponse[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const togglingTagId = ref<number | null>(null);
|
||||
const showCreateForm = ref(false);
|
||||
const newTagLabel = ref('');
|
||||
const newTagColor = ref('#1565C0');
|
||||
@@ -118,6 +136,10 @@ const errorMessage = ref<string | null>(null);
|
||||
const createFormRef = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null);
|
||||
const tagLabelInputRef = ref<{ focus: () => void } | null>(null);
|
||||
|
||||
function isAssigned(tag: TagResponse): boolean {
|
||||
return props.feature.tags.some((t) => t.id === tag.id);
|
||||
}
|
||||
|
||||
function focusTagLabel(): void {
|
||||
nextTick(() => tagLabelInputRef.value?.focus());
|
||||
}
|
||||
@@ -170,6 +192,24 @@ async function onCreateTag(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleTag(tag: TagResponse): Promise<void> {
|
||||
const projectId = contextStore.currentProject?.id;
|
||||
if (!projectId) return;
|
||||
|
||||
togglingTagId.value = tag.id;
|
||||
errorMessage.value = null;
|
||||
try {
|
||||
const updated = isAssigned(tag)
|
||||
? await removeFeatureTag(projectId, props.feature.id, tag.id)
|
||||
: await assignFeatureTag(projectId, props.feature.id, tag.id);
|
||||
emit('updated', updated);
|
||||
} catch {
|
||||
errorMessage.value = 'Failed to update tag assignment. Please try again.';
|
||||
} finally {
|
||||
togglingTagId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteTag(tag: TagResponse): Promise<void> {
|
||||
const projectId = contextStore.currentProject?.id;
|
||||
if (!projectId) return;
|
||||
|
||||
164
src/admin/src/components/nav/OrgHeaderSwitcher.vue
Normal file
164
src/admin/src/components/nav/OrgHeaderSwitcher.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div data-testid="org-header-switcher">
|
||||
<v-menu data-testid="org-header-menu">
|
||||
<template #activator="{ props: menuProps }">
|
||||
<a
|
||||
href="#"
|
||||
class="org-header-link text-body-2 font-weight-medium d-flex align-center"
|
||||
data-testid="org-header-link"
|
||||
v-bind="menuProps"
|
||||
@click.prevent
|
||||
>
|
||||
<VIcon icon="ri-building-line" size="18" class="me-1" />
|
||||
{{ contextStore.currentOrganization?.name ?? 'Select organization' }}
|
||||
<VIcon icon="ri-arrow-down-s-line" size="18" />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<v-list density="compact" data-testid="org-header-list">
|
||||
<v-list-item
|
||||
v-for="org in organizations"
|
||||
:key="org.id"
|
||||
:title="org.name"
|
||||
:active="org.id === contextStore.currentOrganization?.id"
|
||||
:data-testid="`org-header-item-${org.id}`"
|
||||
@click="onOrgSelected(org)"
|
||||
/>
|
||||
|
||||
<v-divider v-if="organizations.length" class="my-1" />
|
||||
|
||||
<v-list-item
|
||||
title="New Organization"
|
||||
prepend-icon="ri-add-line"
|
||||
data-testid="org-header-create-item"
|
||||
@click="openCreateDialog"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<!-- Create Organization Dialog -->
|
||||
<v-dialog v-model="showDialog" max-width="420" data-testid="create-org-dialog">
|
||||
<v-card rounded="lg">
|
||||
<v-card-title class="pa-5 pb-3">Create organization</v-card-title>
|
||||
<v-card-text class="pa-5 pt-0">
|
||||
<v-text-field
|
||||
v-model="newOrgName"
|
||||
label="Organization name"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
autofocus
|
||||
:error-messages="createError ? [createError] : []"
|
||||
data-testid="org-name-input"
|
||||
@keydown.enter="onCreateConfirm"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-5 pt-0">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" data-testid="create-org-cancel-btn" @click="closeDialog">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:loading="isSaving"
|
||||
:disabled="!newOrgName.trim()"
|
||||
data-testid="create-org-confirm-btn"
|
||||
@click="onCreateConfirm"
|
||||
>
|
||||
Create
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useContextStore } from '@/stores/context';
|
||||
import { listOrganizations, createOrganization, setPrimaryOrganization } from '@/api/organizations';
|
||||
import type { OrganizationResponse } from '@/types/api';
|
||||
|
||||
const contextStore = useContextStore();
|
||||
|
||||
const organizations = ref<OrganizationResponse[]>([]);
|
||||
|
||||
const showDialog = ref(false);
|
||||
const newOrgName = ref('');
|
||||
const isSaving = ref(false);
|
||||
const createError = ref('');
|
||||
|
||||
async function fetchOrganizations(): Promise<void> {
|
||||
try {
|
||||
organizations.value = await listOrganizations();
|
||||
autoSelectOrganization();
|
||||
} catch {
|
||||
organizations.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function autoSelectOrganization(): void {
|
||||
if (contextStore.currentOrganization) {
|
||||
// Refresh stored org with latest data from API (name may have changed)
|
||||
const fresh = organizations.value.find(o => o.id === contextStore.currentOrganization!.id);
|
||||
if (fresh) contextStore.refreshOrganization(fresh);
|
||||
return;
|
||||
}
|
||||
|
||||
const primary = organizations.value.find(o => o.isPrimary);
|
||||
if (primary) {
|
||||
contextStore.setOrganization(primary);
|
||||
return;
|
||||
}
|
||||
|
||||
if (organizations.value.length === 1) {
|
||||
contextStore.setOrganization(organizations.value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
async function onOrgSelected(org: OrganizationResponse): Promise<void> {
|
||||
contextStore.setOrganization(org);
|
||||
try {
|
||||
await setPrimaryOrganization(org.id);
|
||||
organizations.value = organizations.value.map(o => ({ ...o, isPrimary: o.id === org.id }));
|
||||
} catch {
|
||||
// Non-critical — selection is already saved to localStorage
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog(): void {
|
||||
newOrgName.value = '';
|
||||
createError.value = '';
|
||||
showDialog.value = true;
|
||||
}
|
||||
|
||||
function closeDialog(): void {
|
||||
showDialog.value = false;
|
||||
}
|
||||
|
||||
async function onCreateConfirm(): Promise<void> {
|
||||
const name = newOrgName.value.trim();
|
||||
if (!name) return;
|
||||
|
||||
isSaving.value = true;
|
||||
createError.value = '';
|
||||
try {
|
||||
const created = await createOrganization({ name });
|
||||
organizations.value = [...organizations.value, created];
|
||||
contextStore.setOrganization(created);
|
||||
closeDialog();
|
||||
} catch {
|
||||
createError.value = 'Failed to create organization. Please try again.';
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchOrganizations);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.org-header-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import NavItems from '@/layouts/components/NavItems.vue';
|
||||
import Footer from '@/layouts/components/Footer.vue';
|
||||
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue';
|
||||
import UserProfile from '@/layouts/components/UserProfile.vue';
|
||||
import OrgHeaderSwitcher from '@/components/nav/OrgHeaderSwitcher.vue';
|
||||
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue';
|
||||
</script>
|
||||
|
||||
@@ -16,6 +17,7 @@ import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue';
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<OrgHeaderSwitcher class="me-4" />
|
||||
<NavbarThemeSwitcher class="me-1" />
|
||||
<UserProfile />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue';
|
||||
import VerticalNavSectionTitle from '@layouts/components/VerticalNavSectionTitle.vue';
|
||||
import OrgSelector from '@/components/nav/OrgSelector.vue';
|
||||
import ProjectSelector from '@/components/nav/ProjectSelector.vue';
|
||||
import EnvironmentTabBar from '@/components/nav/EnvironmentTabBar.vue';
|
||||
</script>
|
||||
@@ -15,7 +14,6 @@ import EnvironmentTabBar from '@/components/nav/EnvironmentTabBar.vue';
|
||||
|
||||
<VerticalNavSectionTitle :item="{ heading: 'Context' }" />
|
||||
<div class="nav-context-selectors px-4 d-flex flex-column ga-3">
|
||||
<OrgSelector />
|
||||
<ProjectSelector />
|
||||
<EnvironmentTabBar />
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import VueApexCharts from 'vue3-apexcharts';
|
||||
import App from './App.vue';
|
||||
|
||||
// Styles
|
||||
import '@fontsource-variable/inter';
|
||||
import '@core/scss/template/index.scss';
|
||||
import '@layouts/styles/index.scss';
|
||||
import router from './router';
|
||||
|
||||
@@ -215,6 +215,7 @@ export interface FeatureResponse {
|
||||
defaultEnabled: boolean;
|
||||
projectId: number;
|
||||
createdAt: string;
|
||||
tags: TagResponse[];
|
||||
}
|
||||
|
||||
export interface CreateFeatureRequest {
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<!-- Name + description -->
|
||||
<template #item.name="{ item }: { item: FeatureResponse }">
|
||||
<div>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<div class="d-flex flex-wrap align-center ga-1">
|
||||
<span class="text-subtitle-1 font-weight-bold">{{ item.name }}</span>
|
||||
<v-tooltip
|
||||
v-if="(featureSegmentsMap.get(item.id) ?? []).length > 0"
|
||||
@@ -72,6 +72,21 @@
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-chip
|
||||
v-for="tag in item.tags.slice(0, 5)"
|
||||
:key="tag.id"
|
||||
size="x-small"
|
||||
variant="flat"
|
||||
:color="tag.color"
|
||||
:data-testid="`feature-tag-chip-${item.id}-${tag.id}`"
|
||||
>
|
||||
{{ tag.label }}
|
||||
</v-chip>
|
||||
<span
|
||||
v-if="item.tags.length > 5"
|
||||
class="text-caption text-medium-emphasis"
|
||||
:title="`${item.tags.length - 5} more`"
|
||||
>…</span>
|
||||
</div>
|
||||
<p v-if="item.description" class="text-caption text-medium-emphasis mb-0">
|
||||
{{ item.description }}
|
||||
|
||||
@@ -18,7 +18,7 @@ public class AuditLogsController(
|
||||
ProjectService projectService,
|
||||
EnvironmentService environmentService) : ControllerBase
|
||||
{
|
||||
[HttpGet("api/v1/organisations/{id}/audit-logs")]
|
||||
[HttpGet("api/v1/organisation/{id}/audit-logs")]
|
||||
public async Task<ActionResult<PaginatedResponse<AuditLogResponse>>> ListByOrganization(
|
||||
int id,
|
||||
[FromQuery] AuditLogFilter filter,
|
||||
@@ -31,7 +31,7 @@ public class AuditLogsController(
|
||||
return Ok(new PaginatedResponse<AuditLogResponse>(total, null, null, logs.ToList()));
|
||||
}
|
||||
|
||||
[HttpGet("api/v1/projects/{projectId}/audit-logs")]
|
||||
[HttpGet("api/v1/project/{projectId}/audit-logs")]
|
||||
public async Task<ActionResult<PaginatedResponse<AuditLogResponse>>> ListByProject(
|
||||
int projectId,
|
||||
[FromQuery] AuditLogFilter filter,
|
||||
@@ -44,7 +44,7 @@ public class AuditLogsController(
|
||||
return Ok(new PaginatedResponse<AuditLogResponse>(total, null, null, logs.ToList()));
|
||||
}
|
||||
|
||||
[HttpGet("api/v1/environments/{apiKey}/audit-logs")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/audit-logs")]
|
||||
public async Task<ActionResult<PaginatedResponse<AuditLogResponse>>> ListByEnvironment(
|
||||
string apiKey,
|
||||
[FromQuery] AuditLogFilter filter,
|
||||
|
||||
@@ -7,11 +7,11 @@ public static class ApiKeyEndpoints
|
||||
public static void MapApiKeyEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app
|
||||
.MapGroup("/api/v1/organisations/{organizationId:int}/api-keys")
|
||||
.MapGroup("/api/v1/organisation/{organizationId:int}")
|
||||
.RequireAuthorization(AuthorizationPolicies.OrganizationAdmin)
|
||||
.WithTags("ApiKeys");
|
||||
|
||||
group.MapPost("/", async (int organizationId, CreateApiKeyRequest request, ApiKeyService apiKeyService, CancellationToken ct) =>
|
||||
group.MapPost("/api-keys", async (int organizationId, CreateApiKeyRequest request, ApiKeyService apiKeyService, CancellationToken ct) =>
|
||||
{
|
||||
var (key, rawKey) = await apiKeyService.CreateAsync(
|
||||
organizationId, request.Name, request.ExpiresAt, ct);
|
||||
@@ -20,14 +20,14 @@ public static class ApiKeyEndpoints
|
||||
|
||||
}).WithName("CreateApiKey");
|
||||
|
||||
group.MapGet("/", async (int organizationId, ApiKeyService apiKeyService, CancellationToken ct) =>
|
||||
group.MapGet("/api-keys", async (int organizationId, ApiKeyService apiKeyService, CancellationToken ct) =>
|
||||
{
|
||||
var keys = await apiKeyService.ListAsync(organizationId, ct);
|
||||
return Results.Ok(keys.Select(k => new ApiKeyResponse(k.Id, k.Name, k.Prefix, k.IsActive, k.ExpiresAt, k.CreatedAt)));
|
||||
|
||||
}).WithName("ListApiKeys");
|
||||
|
||||
group.MapDelete("/{keyId:int}", async (int organizationId, int keyId, ApiKeyService apiKeyService, CancellationToken ct) =>
|
||||
group.MapDelete("/api-key/{keyId:int}", async (int organizationId, int keyId, ApiKeyService apiKeyService, CancellationToken ct) =>
|
||||
{
|
||||
await apiKeyService.RevokeAsync(organizationId, keyId, ct);
|
||||
return Results.NoContent();
|
||||
|
||||
@@ -8,12 +8,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||
namespace MicCheck.Api.Environments;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/environments")]
|
||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||
[EnableRateLimiting("AdminApi")]
|
||||
public class EnvironmentsController(EnvironmentService environmentService, WebhookService webhookService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[HttpGet("api/v1/environments")]
|
||||
public async Task<ActionResult<PaginatedResponse<EnvironmentResponse>>> List(
|
||||
[FromQuery] int projectId,
|
||||
[FromQuery] int page = 1,
|
||||
@@ -26,14 +25,14 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(new PaginatedResponse<EnvironmentResponse>(all.Count, null, null, paged));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[HttpPost("api/v1/environments")]
|
||||
public async Task<ActionResult<EnvironmentResponse>> Create(CreateEnvironmentRequest request, CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.CreateAsync(request.ProjectId, request.Name, ct);
|
||||
return CreatedAtAction(nameof(GetByApiKey), new { apiKey = environment.ApiKey }, EnvironmentResponse.From(environment));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}")]
|
||||
[HttpGet("api/v1/environment/{apiKey}")]
|
||||
public async Task<ActionResult<EnvironmentResponse>> GetByApiKey(string apiKey, CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
@@ -41,7 +40,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(EnvironmentResponse.From(environment));
|
||||
}
|
||||
|
||||
[HttpPut("{apiKey}")]
|
||||
[HttpPut("api/v1/environment/{apiKey}")]
|
||||
public async Task<ActionResult<EnvironmentResponse>> Update(string apiKey, UpdateEnvironmentRequest request, CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
@@ -51,7 +50,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(EnvironmentResponse.From(updated));
|
||||
}
|
||||
|
||||
[HttpDelete("{apiKey}")]
|
||||
[HttpDelete("api/v1/environment/{apiKey}")]
|
||||
public async Task<IActionResult> Delete(string apiKey, CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
@@ -61,7 +60,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{apiKey}/clone")]
|
||||
[HttpPost("api/v1/environment/{apiKey}/clone")]
|
||||
public async Task<ActionResult<EnvironmentResponse>> Clone(string apiKey, CloneEnvironmentRequest request, CancellationToken ct)
|
||||
{
|
||||
var source = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
@@ -71,7 +70,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return CreatedAtAction(nameof(GetByApiKey), new { apiKey = cloned.ApiKey }, EnvironmentResponse.From(cloned));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/webhooks")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/webhooks")]
|
||||
public async Task<ActionResult<IReadOnlyList<WebhookResponse>>> ListWebhooks(string apiKey, CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
@@ -81,7 +80,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(webhooks.Select(WebhookResponse.From).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{apiKey}/webhooks")]
|
||||
[HttpPost("api/v1/environment/{apiKey}/webhooks")]
|
||||
public async Task<ActionResult<WebhookResponse>> CreateWebhook(
|
||||
string apiKey, CreateWebhookRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -92,7 +91,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return CreatedAtAction(nameof(GetByApiKey), new { apiKey }, WebhookResponse.From(webhook));
|
||||
}
|
||||
|
||||
[HttpPut("{apiKey}/webhooks/{id}")]
|
||||
[HttpPut("api/v1/environment/{apiKey}/webhook/{id}")]
|
||||
public async Task<ActionResult<WebhookResponse>> UpdateWebhook(
|
||||
string apiKey, int id, CreateWebhookRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -106,7 +105,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(WebhookResponse.From(updated));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/webhooks/{id}/deliveries")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/webhook/{id}/deliveries")]
|
||||
public async Task<ActionResult<IReadOnlyList<WebhookDeliveryLogResponse>>> ListWebhookDeliveries(
|
||||
string apiKey, int id, CancellationToken ct)
|
||||
{
|
||||
@@ -120,7 +119,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(logs.Select(WebhookDeliveryLogResponse.From).ToList());
|
||||
}
|
||||
|
||||
[HttpDelete("{apiKey}/webhooks/{id}")]
|
||||
[HttpDelete("api/v1/environment/{apiKey}/webhook/{id}")]
|
||||
public async Task<IActionResult> DeleteWebhook(string apiKey, int id, CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
@@ -133,7 +132,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/audit-logs")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/audit-logs")]
|
||||
public async Task<ActionResult<IReadOnlyList<Audit.AuditLogResponse>>> ListAuditLogs(
|
||||
string apiKey,
|
||||
[FromServices] Audit.AuditLogQueryService auditLogQueryService,
|
||||
@@ -146,7 +145,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(logs.ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/identities")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/identities")]
|
||||
public async Task<ActionResult<PaginatedResponse<Identities.AdminIdentityResponse>>> ListIdentities(
|
||||
string apiKey,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
@@ -163,7 +162,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(new PaginatedResponse<Identities.AdminIdentityResponse>(total, null, null, results));
|
||||
}
|
||||
|
||||
[HttpPost("{apiKey}/identities")]
|
||||
[HttpPost("api/v1/environment/{apiKey}/identities")]
|
||||
public async Task<ActionResult<Identities.AdminIdentityResponse>> CreateIdentity(
|
||||
string apiKey,
|
||||
[FromBody] Identities.CreateIdentityRequest request,
|
||||
@@ -181,7 +180,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
Identities.AdminIdentityResponse.From(identity));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/identities/{id}")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/identity/{id}")]
|
||||
public async Task<ActionResult<Identities.AdminIdentityResponse>> GetIdentity(
|
||||
string apiKey, int id,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
@@ -195,7 +194,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(Identities.AdminIdentityResponse.From(identity));
|
||||
}
|
||||
|
||||
[HttpDelete("{apiKey}/identities/{id}")]
|
||||
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}")]
|
||||
public async Task<IActionResult> DeleteIdentity(
|
||||
string apiKey, int id,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
@@ -211,7 +210,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut("{apiKey}/identities/{id}/traits/{key}")]
|
||||
[HttpPut("api/v1/environment/{apiKey}/identity/{id}/trait/{key}")]
|
||||
public async Task<ActionResult<Identities.TraitResponse>> UpsertIdentityTrait(
|
||||
string apiKey, int id, string key,
|
||||
[FromBody] Identities.UpsertTraitRequest request,
|
||||
@@ -226,7 +225,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpDelete("{apiKey}/identities/{id}/traits/{key}")]
|
||||
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}/trait/{key}")]
|
||||
public async Task<IActionResult> DeleteIdentityTrait(
|
||||
string apiKey, int id, string key,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
@@ -240,7 +239,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/identities/{id}/featurestates")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/identity/{id}/featurestates")]
|
||||
public async Task<ActionResult<IReadOnlyList<Features.FeatureStateResponse>>> GetIdentityFeatureStates(
|
||||
string apiKey, int id,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
@@ -256,7 +255,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(states.Select(Features.FeatureStateResponse.From).ToList());
|
||||
}
|
||||
|
||||
[HttpPut("{apiKey}/identities/{id}/featurestates/{featureId}")]
|
||||
[HttpPut("api/v1/environment/{apiKey}/identity/{id}/featurestate/{featureId}")]
|
||||
public async Task<ActionResult<Features.FeatureStateResponse>> SetIdentityFeatureState(
|
||||
string apiKey, int id, int featureId,
|
||||
Features.UpdateFeatureStateRequest request,
|
||||
@@ -273,7 +272,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(Features.FeatureStateResponse.From(state));
|
||||
}
|
||||
|
||||
[HttpDelete("{apiKey}/identities/{id}/featurestates/{featureId}")]
|
||||
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}/featurestate/{featureId}")]
|
||||
public async Task<IActionResult> DeleteIdentityFeatureState(
|
||||
string apiKey, int id, int featureId,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
@@ -289,7 +288,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/featurestates")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/featurestates")]
|
||||
public async Task<ActionResult<IReadOnlyList<Features.FeatureStateResponse>>> ListFeatureStates(
|
||||
string apiKey,
|
||||
[FromServices] Features.FeatureStateService featureStateService,
|
||||
@@ -302,7 +301,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(states.Select(Features.FeatureStateResponse.From).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/featurestates/{id}")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/featurestate/{id}")]
|
||||
public async Task<ActionResult<Features.FeatureStateResponse>> GetFeatureState(
|
||||
string apiKey, int id,
|
||||
[FromServices] Features.FeatureStateService featureStateService,
|
||||
@@ -316,7 +315,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(Features.FeatureStateResponse.From(state));
|
||||
}
|
||||
|
||||
[HttpPut("{apiKey}/featurestates/{id}")]
|
||||
[HttpPut("api/v1/environment/{apiKey}/featurestate/{id}")]
|
||||
public async Task<ActionResult<Features.FeatureStateResponse>> UpdateFeatureState(
|
||||
string apiKey, int id,
|
||||
Features.UpdateFeatureStateRequest request,
|
||||
@@ -333,7 +332,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(Features.FeatureStateResponse.From(updated));
|
||||
}
|
||||
|
||||
[HttpPatch("{apiKey}/featurestates/{id}")]
|
||||
[HttpPatch("api/v1/environment/{apiKey}/featurestate/{id}")]
|
||||
public async Task<ActionResult<Features.FeatureStateResponse>> PatchFeatureState(
|
||||
string apiKey, int id,
|
||||
Features.PatchFeatureStateRequest request,
|
||||
@@ -352,7 +351,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
|
||||
// ─── Feature Segments ────────────────────────────────────────────────────
|
||||
|
||||
[HttpGet("{apiKey}/features/{featureId}/segments")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/feature/{featureId}/segments")]
|
||||
public async Task<ActionResult<IReadOnlyList<Features.FeatureSegmentResponse>>> ListFeatureSegments(
|
||||
string apiKey, int featureId,
|
||||
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||
@@ -365,7 +364,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpPost("{apiKey}/features/{featureId}/segments")]
|
||||
[HttpPost("api/v1/environment/{apiKey}/feature/{featureId}/segments")]
|
||||
public async Task<ActionResult<Features.FeatureSegmentResponse>> CreateFeatureSegment(
|
||||
string apiKey, int featureId,
|
||||
Features.CreateFeatureSegmentRequest request,
|
||||
@@ -388,7 +387,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{apiKey}/features/{featureId}/segments/{id}")]
|
||||
[HttpPut("api/v1/environment/{apiKey}/feature/{featureId}/segment/{id}")]
|
||||
public async Task<ActionResult<Features.FeatureSegmentResponse>> UpdateFeatureSegment(
|
||||
string apiKey, int featureId, int id,
|
||||
Features.UpdateFeatureSegmentRequest request,
|
||||
@@ -403,7 +402,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpDelete("{apiKey}/features/{featureId}/segments/{id}")]
|
||||
[HttpDelete("api/v1/environment/{apiKey}/feature/{featureId}/segment/{id}")]
|
||||
public async Task<IActionResult> DeleteFeatureSegment(
|
||||
string apiKey, int featureId, int id,
|
||||
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||
@@ -418,7 +417,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
|
||||
// ─── Identity Segments ───────────────────────────────────────────────────
|
||||
|
||||
[HttpGet("{apiKey}/identities/{id}/segments")]
|
||||
[HttpGet("api/v1/environment/{apiKey}/identity/{id}/segments")]
|
||||
public async Task<ActionResult<IReadOnlyList<Segments.SegmentSummaryResponse>>> GetIdentitySegments(
|
||||
string apiKey, int id,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
|
||||
@@ -8,7 +8,8 @@ public record FeatureResponse(
|
||||
string? Description,
|
||||
bool DefaultEnabled,
|
||||
int ProjectId,
|
||||
DateTimeOffset CreatedAt
|
||||
DateTimeOffset CreatedAt,
|
||||
IReadOnlyList<TagResponse> Tags
|
||||
)
|
||||
{
|
||||
public static FeatureResponse From(Feature feature) => new(
|
||||
@@ -19,5 +20,6 @@ public record FeatureResponse(
|
||||
feature.Description,
|
||||
feature.DefaultEnabled,
|
||||
feature.ProjectId,
|
||||
feature.CreatedAt);
|
||||
feature.CreatedAt,
|
||||
feature.Tags.Select(TagResponse.From).ToList());
|
||||
}
|
||||
|
||||
@@ -13,13 +13,14 @@ public class FeatureService(MicCheckDbContext db, AuditService auditService, Web
|
||||
public async Task<IReadOnlyList<Feature>> ListByProjectAsync(int projectId, CancellationToken ct = default)
|
||||
{
|
||||
return await db.Features
|
||||
.Include(f => f.Tags)
|
||||
.Where(f => f.ProjectId == projectId)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<Feature?> FindByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
return await db.Features.FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
return await db.Features.Include(f => f.Tags).FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
}
|
||||
|
||||
public async Task<Feature> CreateAsync(
|
||||
@@ -80,7 +81,7 @@ public class FeatureService(MicCheckDbContext db, AuditService auditService, Web
|
||||
public async Task<Feature> UpdateAsync(
|
||||
int id, string name, string? description, CancellationToken ct = default)
|
||||
{
|
||||
var feature = await db.Features.FirstOrDefaultAsync(f => f.Id == id, ct)
|
||||
var feature = await db.Features.Include(f => f.Tags).FirstOrDefaultAsync(f => f.Id == id, ct)
|
||||
?? throw new KeyNotFoundException($"Feature {id} not found.");
|
||||
|
||||
var before = new { feature.Name, feature.Description };
|
||||
@@ -101,6 +102,40 @@ public class FeatureService(MicCheckDbContext db, AuditService auditService, Web
|
||||
return feature;
|
||||
}
|
||||
|
||||
public async Task<Feature> AssignTagAsync(int featureId, int tagId, CancellationToken ct = default)
|
||||
{
|
||||
var feature = await db.Features.Include(f => f.Tags).FirstOrDefaultAsync(f => f.Id == featureId, ct)
|
||||
?? throw new KeyNotFoundException($"Feature {featureId} not found.");
|
||||
var tag = await db.Tags.FirstOrDefaultAsync(t => t.Id == tagId, ct)
|
||||
?? throw new KeyNotFoundException($"Tag {tagId} not found.");
|
||||
|
||||
if (tag.ProjectId != feature.ProjectId)
|
||||
throw new DomainException("Tag does not belong to the feature's project.");
|
||||
|
||||
if (feature.Tags.All(t => t.Id != tagId))
|
||||
{
|
||||
feature.Tags.Add(tag);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
public async Task<Feature> RemoveTagAsync(int featureId, int tagId, CancellationToken ct = default)
|
||||
{
|
||||
var feature = await db.Features.Include(f => f.Tags).FirstOrDefaultAsync(f => f.Id == featureId, ct)
|
||||
?? throw new KeyNotFoundException($"Feature {featureId} not found.");
|
||||
|
||||
var tag = feature.Tags.FirstOrDefault(t => t.Id == tagId);
|
||||
if (tag is not null)
|
||||
{
|
||||
feature.Tags.Remove(tag);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var feature = await db.Features.FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
|
||||
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Caching.Memory;
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/environments/{environmentId}/usage")]
|
||||
[Route("api/v1/environment/{environmentId}/usage")]
|
||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||
[EnableRateLimiting("AdminApi")]
|
||||
public class FeatureUsageController(FeatureUsageQueryService queryService, IMemoryCache cache) : ControllerBase
|
||||
|
||||
@@ -7,12 +7,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/projects/{projectId}/features")]
|
||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||
[EnableRateLimiting("AdminApi")]
|
||||
public class FeaturesController(FeatureService featureService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[HttpGet("api/v1/project/{projectId}/features")]
|
||||
public async Task<ActionResult<PaginatedResponse<FeatureResponse>>> List(
|
||||
int projectId,
|
||||
[FromQuery] int page = 1,
|
||||
@@ -25,7 +24,7 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
||||
return Ok(new PaginatedResponse<FeatureResponse>(all.Count, null, null, paged));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[HttpPost("api/v1/project/{projectId}/features")]
|
||||
public async Task<ActionResult<FeatureResponse>> Create(
|
||||
int projectId, CreateFeatureRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -41,7 +40,7 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[HttpGet("api/v1/project/{projectId}/feature/{id}")]
|
||||
public async Task<ActionResult<FeatureResponse>> GetById(int projectId, int id, CancellationToken ct)
|
||||
{
|
||||
var feature = await featureService.FindByIdAsync(id, ct);
|
||||
@@ -49,7 +48,7 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
||||
return Ok(FeatureResponse.From(feature));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPut("api/v1/project/{projectId}/feature/{id}")]
|
||||
public async Task<ActionResult<FeatureResponse>> Update(
|
||||
int projectId, int id, UpdateFeatureRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -60,7 +59,7 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
||||
return Ok(FeatureResponse.From(updated));
|
||||
}
|
||||
|
||||
[HttpPatch("{id}")]
|
||||
[HttpPatch("api/v1/project/{projectId}/feature/{id}")]
|
||||
public async Task<ActionResult<FeatureResponse>> Patch(
|
||||
int projectId, int id, PatchFeatureRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -75,7 +74,7 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
||||
return Ok(FeatureResponse.From(updated));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpDelete("api/v1/project/{projectId}/feature/{id}")]
|
||||
public async Task<IActionResult> Delete(int projectId, int id, CancellationToken ct)
|
||||
{
|
||||
var feature = await featureService.FindByIdAsync(id, ct);
|
||||
@@ -84,4 +83,35 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
||||
await featureService.DeleteAsync(id, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut("api/v1/project/{projectId}/feature/{id}/tag/{tagId}")]
|
||||
public async Task<ActionResult<FeatureResponse>> AssignTag(int projectId, int id, int tagId, CancellationToken ct)
|
||||
{
|
||||
var feature = await featureService.FindByIdAsync(id, ct);
|
||||
if (feature is null || feature.ProjectId != projectId) return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
var updated = await featureService.AssignTagAsync(id, tagId, ct);
|
||||
return Ok(FeatureResponse.From(updated));
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
catch (DomainException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("api/v1/project/{projectId}/feature/{id}/tag/{tagId}")]
|
||||
public async Task<ActionResult<FeatureResponse>> RemoveTag(int projectId, int id, int tagId, CancellationToken ct)
|
||||
{
|
||||
var feature = await featureService.FindByIdAsync(id, ct);
|
||||
if (feature is null || feature.ProjectId != projectId) return NotFound();
|
||||
|
||||
var updated = await featureService.RemoveTagAsync(id, tagId, ct);
|
||||
return Ok(FeatureResponse.From(updated));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,26 +6,25 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/projects/{projectId}/tags")]
|
||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||
[EnableRateLimiting("AdminApi")]
|
||||
public class TagsController(TagService tagService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[HttpGet("api/v1/project/{projectId}/tags")]
|
||||
public async Task<ActionResult<IReadOnlyList<TagResponse>>> List(int projectId, CancellationToken ct)
|
||||
{
|
||||
var tags = await tagService.ListByProjectAsync(projectId, ct);
|
||||
return Ok(tags.Select(TagResponse.From).ToList());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[HttpPost("api/v1/project/{projectId}/tags")]
|
||||
public async Task<ActionResult<TagResponse>> Create(int projectId, CreateTagRequest request, CancellationToken ct)
|
||||
{
|
||||
var tag = await tagService.CreateAsync(projectId, request.Label, request.Color, ct);
|
||||
return CreatedAtAction(nameof(List), new { projectId }, TagResponse.From(tag));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpDelete("api/v1/project/{projectId}/tag/{id}")]
|
||||
public async Task<IActionResult> Delete(int projectId, int id, CancellationToken ct)
|
||||
{
|
||||
var tag = await tagService.FindByIdAsync(id, ct);
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace MicCheck.Api.Identities;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/identities")]
|
||||
[Route("api/v1/identity")]
|
||||
[Authorize(Policy = AuthorizationPolicies.FlagsApiAccess)]
|
||||
public class IdentitiesController(
|
||||
FeatureEvaluationService featureEvaluationService,
|
||||
|
||||
@@ -8,12 +8,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||
namespace MicCheck.Api.Organizations;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/organisations")]
|
||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||
[EnableRateLimiting("AdminApi")]
|
||||
public class OrganizationsController(OrganizationService organizationService, WebhookService webhookService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[HttpGet("api/v1/organisations")]
|
||||
public async Task<ActionResult<PaginatedResponse<OrganizationResponse>>> List(
|
||||
[FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
|
||||
{
|
||||
@@ -28,7 +27,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(new PaginatedResponse<OrganizationResponse>(all.Count, null, null, paged));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[HttpPost("api/v1/organisations")]
|
||||
public async Task<ActionResult<OrganizationResponse>> Create(
|
||||
CreateOrganizationRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -39,7 +38,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return CreatedAtAction(nameof(GetById), new { id = org.Id }, OrganizationResponse.From(org));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[HttpGet("api/v1/organisation/{id}")]
|
||||
public async Task<ActionResult<OrganizationResponse>> GetById(int id, CancellationToken ct)
|
||||
{
|
||||
var org = await organizationService.FindByIdAsync(id, ct);
|
||||
@@ -47,7 +46,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(OrganizationResponse.From(org));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPut("api/v1/organisation/{id}")]
|
||||
public async Task<ActionResult<OrganizationResponse>> Update(
|
||||
int id, UpdateOrganizationRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -58,7 +57,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(OrganizationResponse.From(updated));
|
||||
}
|
||||
|
||||
[HttpPut("{id}/primary")]
|
||||
[HttpPut("api/v1/organisation/{id}/primary")]
|
||||
public async Task<IActionResult> SetPrimary(int id, CancellationToken ct)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
@@ -75,7 +74,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpDelete("api/v1/organisation/{id}")]
|
||||
public async Task<IActionResult> Delete(int id, CancellationToken ct)
|
||||
{
|
||||
var org = await organizationService.FindByIdAsync(id, ct);
|
||||
@@ -85,7 +84,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/users")]
|
||||
[HttpGet("api/v1/organisation/{id}/users")]
|
||||
public async Task<ActionResult<IReadOnlyList<OrganizationMemberResponse>>> ListUsers(
|
||||
int id, CancellationToken ct)
|
||||
{
|
||||
@@ -96,7 +95,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(members.Select(OrganizationMemberResponse.From).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/users/invite")]
|
||||
[HttpPost("api/v1/organisation/{id}/users/invite")]
|
||||
public async Task<IActionResult> InviteUser(int id, InviteUserRequest request, CancellationToken ct)
|
||||
{
|
||||
var org = await organizationService.FindByIdAsync(id, ct);
|
||||
@@ -107,7 +106,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("{id}/users/invite-by-email")]
|
||||
[HttpPost("api/v1/organisation/{id}/users/invite-by-email")]
|
||||
public async Task<ActionResult<IReadOnlyList<InviteByEmailResult>>> InviteUsersByEmail(
|
||||
int id, InviteUsersByEmailRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -118,7 +117,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/invite-link")]
|
||||
[HttpGet("api/v1/organisation/{id}/invite-link")]
|
||||
public async Task<ActionResult<InviteTokenResponse>> GetInviteLink(int id, CancellationToken ct)
|
||||
{
|
||||
var org = await organizationService.FindByIdAsync(id, ct);
|
||||
@@ -128,7 +127,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(new InviteTokenResponse(token));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/invite-link/regenerate")]
|
||||
[HttpPost("api/v1/organisation/{id}/invite-link/regenerate")]
|
||||
public async Task<ActionResult<InviteTokenResponse>> RegenerateInviteLink(int id, CancellationToken ct)
|
||||
{
|
||||
var org = await organizationService.FindByIdAsync(id, ct);
|
||||
@@ -138,7 +137,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(new InviteTokenResponse(token));
|
||||
}
|
||||
|
||||
[HttpPost("invite/{token}/accept")]
|
||||
[HttpPost("api/v1/organisations/invite/{token}/accept")]
|
||||
public async Task<IActionResult> AcceptInvite(string token, CancellationToken ct)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
@@ -155,7 +154,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/users/{userId}")]
|
||||
[HttpDelete("api/v1/organisation/{id}/user/{userId}")]
|
||||
public async Task<IActionResult> RemoveUser(int id, int userId, CancellationToken ct)
|
||||
{
|
||||
var org = await organizationService.FindByIdAsync(id, ct);
|
||||
@@ -165,7 +164,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/webhooks")]
|
||||
[HttpGet("api/v1/organisation/{id}/webhooks")]
|
||||
public async Task<ActionResult<IReadOnlyList<WebhookResponse>>> ListWebhooks(int id, CancellationToken ct)
|
||||
{
|
||||
var org = await organizationService.FindByIdAsync(id, ct);
|
||||
@@ -175,7 +174,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(webhooks.Select(WebhookResponse.From).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/webhooks")]
|
||||
[HttpPost("api/v1/organisation/{id}/webhooks")]
|
||||
public async Task<ActionResult<WebhookResponse>> CreateWebhook(
|
||||
int id, CreateWebhookRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -186,7 +185,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return CreatedAtAction(nameof(ListWebhooks), new { id }, WebhookResponse.From(webhook));
|
||||
}
|
||||
|
||||
[HttpPut("{id}/webhooks/{webhookId}")]
|
||||
[HttpPut("api/v1/organisation/{id}/webhook/{webhookId}")]
|
||||
public async Task<ActionResult<WebhookResponse>> UpdateWebhook(
|
||||
int id, int webhookId, CreateWebhookRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -200,7 +199,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(WebhookResponse.From(updated));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/webhooks/{webhookId}")]
|
||||
[HttpDelete("api/v1/organisation/{id}/webhook/{webhookId}")]
|
||||
public async Task<IActionResult> DeleteWebhook(int id, int webhookId, CancellationToken ct)
|
||||
{
|
||||
var org = await organizationService.FindByIdAsync(id, ct);
|
||||
|
||||
@@ -7,12 +7,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||
namespace MicCheck.Api.Projects;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/projects")]
|
||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||
[EnableRateLimiting("AdminApi")]
|
||||
public class ProjectsController(ProjectService projectService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[HttpGet("api/v1/projects")]
|
||||
public async Task<ActionResult<PaginatedResponse<ProjectResponse>>> List(
|
||||
[FromQuery] int organizationId,
|
||||
[FromQuery] int page = 1,
|
||||
@@ -25,14 +24,14 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
||||
return Ok(new PaginatedResponse<ProjectResponse>(all.Count, null, null, paged));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[HttpPost("api/v1/projects")]
|
||||
public async Task<ActionResult<ProjectResponse>> Create(CreateProjectRequest request, CancellationToken ct)
|
||||
{
|
||||
var project = await projectService.CreateAsync(request.OrganizationId, request.Name, ct);
|
||||
return CreatedAtAction(nameof(GetById), new { id = project.Id }, ProjectResponse.From(project));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[HttpGet("api/v1/project/{id}")]
|
||||
public async Task<ActionResult<ProjectResponse>> GetById(int id, CancellationToken ct)
|
||||
{
|
||||
var project = await projectService.FindByIdAsync(id, ct);
|
||||
@@ -40,7 +39,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
||||
return Ok(ProjectResponse.From(project));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPut("api/v1/project/{id}")]
|
||||
public async Task<ActionResult<ProjectResponse>> Update(int id, UpdateProjectRequest request, CancellationToken ct)
|
||||
{
|
||||
var project = await projectService.FindByIdAsync(id, ct);
|
||||
@@ -50,7 +49,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
||||
return Ok(ProjectResponse.From(updated));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpDelete("api/v1/project/{id}")]
|
||||
public async Task<IActionResult> Delete(int id, CancellationToken ct)
|
||||
{
|
||||
var project = await projectService.FindByIdAsync(id, ct);
|
||||
@@ -60,7 +59,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/user-permissions")]
|
||||
[HttpGet("api/v1/project/{id}/user-permissions")]
|
||||
public async Task<ActionResult<IReadOnlyList<UserPermissionResponse>>> ListUserPermissions(
|
||||
int id, CancellationToken ct)
|
||||
{
|
||||
@@ -71,7 +70,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
||||
return Ok(perms.Select(UserPermissionResponse.From).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/user-permissions")]
|
||||
[HttpPost("api/v1/project/{id}/user-permissions")]
|
||||
public async Task<ActionResult<UserPermissionResponse>> CreateUserPermissions(
|
||||
int id, SetUserPermissionsRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -86,7 +85,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
||||
return Ok(UserPermissionResponse.From(perm));
|
||||
}
|
||||
|
||||
[HttpPut("{id}/user-permissions/{userId}")]
|
||||
[HttpPut("api/v1/project/{id}/user-permission/{userId}")]
|
||||
public async Task<ActionResult<UserPermissionResponse>> UpdateUserPermissions(
|
||||
int id, int userId, SetUserPermissionsRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -101,7 +100,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
||||
return Ok(UserPermissionResponse.From(perm));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/user-permissions/{userId}")]
|
||||
[HttpDelete("api/v1/project/{id}/user-permission/{userId}")]
|
||||
public async Task<IActionResult> DeleteUserPermissions(int id, int userId, CancellationToken ct)
|
||||
{
|
||||
var project = await projectService.FindByIdAsync(id, ct);
|
||||
|
||||
@@ -7,12 +7,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||
namespace MicCheck.Api.Segments;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/projects/{projectId}/segments")]
|
||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||
[EnableRateLimiting("AdminApi")]
|
||||
public class SegmentsController(SegmentService segmentService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[HttpGet("api/v1/project/{projectId}/segments")]
|
||||
public async Task<ActionResult<PaginatedResponse<SegmentResponse>>> List(
|
||||
int projectId,
|
||||
[FromQuery] int page = 1,
|
||||
@@ -25,7 +24,7 @@ public class SegmentsController(SegmentService segmentService) : ControllerBase
|
||||
return Ok(new PaginatedResponse<SegmentResponse>(all.Count, null, null, paged));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[HttpPost("api/v1/project/{projectId}/segments")]
|
||||
public async Task<ActionResult<SegmentResponse>> Create(
|
||||
int projectId, CreateSegmentRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -41,7 +40,7 @@ public class SegmentsController(SegmentService segmentService) : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[HttpGet("api/v1/project/{projectId}/segment/{id}")]
|
||||
public async Task<ActionResult<SegmentResponse>> GetById(int projectId, int id, CancellationToken ct)
|
||||
{
|
||||
var segment = await segmentService.FindByIdAsync(id, ct);
|
||||
@@ -49,7 +48,7 @@ public class SegmentsController(SegmentService segmentService) : ControllerBase
|
||||
return Ok(SegmentResponse.From(segment));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPut("api/v1/project/{projectId}/segment/{id}")]
|
||||
public async Task<ActionResult<SegmentResponse>> Update(
|
||||
int projectId, int id, CreateSegmentRequest request, CancellationToken ct)
|
||||
{
|
||||
@@ -68,7 +67,7 @@ public class SegmentsController(SegmentService segmentService) : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpDelete("api/v1/project/{projectId}/segment/{id}")]
|
||||
public async Task<IActionResult> Delete(int projectId, int id, CancellationToken ct)
|
||||
{
|
||||
var segment = await segmentService.FindByIdAsync(id, ct);
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||
namespace MicCheck.Api.Users;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/users")]
|
||||
[Route("api/v1/user")]
|
||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||
[EnableRateLimiting("AdminApi")]
|
||||
public class UsersController(UserService userService) : ControllerBase
|
||||
|
||||
@@ -97,7 +97,7 @@ public class AdminApiIntegrationTests
|
||||
db.Projects.Add(project);
|
||||
db.SaveChanges();
|
||||
|
||||
var response = await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/features", new
|
||||
var response = await client.PostAsJsonAsync($"/api/v1/project/{project.Id}/features", new
|
||||
{
|
||||
name = "dark_mode",
|
||||
type = "Standard",
|
||||
@@ -135,7 +135,7 @@ public class AdminApiIntegrationTests
|
||||
});
|
||||
db.SaveChanges();
|
||||
|
||||
await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/features", new
|
||||
await client.PostAsJsonAsync($"/api/v1/project/{project.Id}/features", new
|
||||
{
|
||||
name = "flag_x",
|
||||
type = "Standard",
|
||||
@@ -220,7 +220,7 @@ public class AdminApiIntegrationTests
|
||||
db.Projects.Add(project);
|
||||
db.SaveChanges();
|
||||
|
||||
var response = await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/features", new
|
||||
var response = await client.PostAsJsonAsync($"/api/v1/project/{project.Id}/features", new
|
||||
{
|
||||
name = "invalid name with spaces!",
|
||||
type = "Standard"
|
||||
@@ -247,7 +247,7 @@ public class AdminApiIntegrationTests
|
||||
db.Projects.Add(project);
|
||||
db.SaveChanges();
|
||||
|
||||
var response = await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/segments", new
|
||||
var response = await client.PostAsJsonAsync($"/api/v1/project/{project.Id}/segments", new
|
||||
{
|
||||
name = "Premium Users",
|
||||
rules = new[]
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", "not-a-jwt");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -69,7 +69,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", "Api-Key not-a-real-key");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", "not-a-jwt");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -69,7 +69,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", "Api-Key not-a-real-key");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class ApiKeyAuthenticationHandlerTests
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
|
||||
var response = await client.GetAsync("/api/v1/organisation/1/api-keys/");
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
}
|
||||
|
||||
@@ -137,4 +137,63 @@ public class FeatureServiceTests
|
||||
|
||||
Assert.That(features, Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAssigningATag_ThenItAppearsOnTheFeature()
|
||||
{
|
||||
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
|
||||
var tag = new Tag { Label = "beta", Color = "#FF0000", ProjectId = ProjectId };
|
||||
_db.Tags.Add(tag);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var updated = await _service.AssignTagAsync(feature.Id, tag.Id);
|
||||
|
||||
Assert.That(updated.Tags.Select(t => t.Id), Does.Contain(tag.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAssigningATagAlreadyOnTheFeature_ThenItIsNotDuplicated()
|
||||
{
|
||||
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
|
||||
var tag = new Tag { Label = "beta", Color = "#FF0000", ProjectId = ProjectId };
|
||||
_db.Tags.Add(tag);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _service.AssignTagAsync(feature.Id, tag.Id);
|
||||
var updated = await _service.AssignTagAsync(feature.Id, tag.Id);
|
||||
|
||||
Assert.That(updated.Tags.Count(t => t.Id == tag.Id), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAssigningATagFromAnotherProject_ThenDomainExceptionIsThrown()
|
||||
{
|
||||
_db.Projects.Add(new MicCheck.Api.Projects.Project
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Other Project",
|
||||
OrganizationId = OrganizationId,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
|
||||
var foreignTag = new Tag { Label = "beta", Color = "#FF0000", ProjectId = 2 };
|
||||
_db.Tags.Add(foreignTag);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Assert.ThrowsAsync<DomainException>(() => _service.AssignTagAsync(feature.Id, foreignTag.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRemovingAnAssignedTag_ThenItNoLongerAppearsOnTheFeature()
|
||||
{
|
||||
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
|
||||
var tag = new Tag { Label = "beta", Color = "#FF0000", ProjectId = ProjectId };
|
||||
_db.Tags.Add(tag);
|
||||
await _db.SaveChangesAsync();
|
||||
await _service.AssignTagAsync(feature.Id, tag.Id);
|
||||
|
||||
var updated = await _service.RemoveTagAsync(feature.Id, tag.Id);
|
||||
|
||||
Assert.That(updated.Tags.Select(t => t.Id), Does.Not.Contain(tag.Id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ public class FlagsApiIntegrationTests
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Environment-Key", _envApiKey);
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/identities/", new
|
||||
var response = await client.PostAsJsonAsync("/api/v1/identity/", new
|
||||
{
|
||||
identifier = "user-123",
|
||||
traits = new[] { new { trait_key = "plan", trait_value = "premium" } }
|
||||
|
||||
Reference in New Issue
Block a user