Move org switcher to header, self-host Inter, add feature tags, normalize API route casing
- Replace sidebar org dropdown with a header link/menu (left of theme toggle) that
includes a "New Organization" entry opening the create dialog
- Self-host Inter via @fontsource-variable/inter instead of relying on system fonts
- Wire up real feature<->tag assignment (was UI-only before): expose Tags on
FeatureResponse, add assign/remove endpoints, make tag chips in the feature
detail dialog toggle assignment, and show up to 5 tags (+ellipsis) in the
features table next to the segment-override icon
- Normalize all API routes to singular/plural REST convention: singular resource
name when addressing one item by id/key (e.g. /feature/{id}), plural for
list/create endpoints (e.g. /features). Updated every controller, the admin
frontend API clients, and affected tests to match
This commit is contained in:
9
src/admin/package-lock.json
generated
9
src/admin/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "mic-check-admin",
|
"name": "mic-check-admin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@iconify-json/bxl": "^1.2.0",
|
"@iconify-json/bxl": "^1.2.0",
|
||||||
"@iconify-json/ri": "^1.2.0",
|
"@iconify-json/ri": "^1.2.0",
|
||||||
"@iconify/vue": "^4.1.0",
|
"@iconify/vue": "^4.1.0",
|
||||||
@@ -2002,6 +2003,14 @@
|
|||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"test": "jest --passWithNoTests"
|
"test": "jest --passWithNoTests"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@iconify-json/bxl": "^1.2.0",
|
"@iconify-json/bxl": "^1.2.0",
|
||||||
"@iconify-json/ri": "^1.2.0",
|
"@iconify-json/ri": "^1.2.0",
|
||||||
"@iconify/vue": "^4.1.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";
|
"Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
/* stylelint-disable length-zero-no-unit */
|
/* stylelint-disable length-zero-no-unit */
|
||||||
@forward "../../../base/libs/vuetify/variables" with (
|
@forward "../../../base/libs/vuetify/variables" with (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { ApiKeyResponse, CreateApiKeyRequest, CreateApiKeyResponse } from '
|
|||||||
|
|
||||||
export async function listApiKeys(organizationId: number): Promise<ApiKeyResponse[]> {
|
export async function listApiKeys(organizationId: number): Promise<ApiKeyResponse[]> {
|
||||||
const { data } = await apiClient.get<ApiKeyResponse[]>(
|
const { data } = await apiClient.get<ApiKeyResponse[]>(
|
||||||
`/v1/organisations/${organizationId}/api-keys`,
|
`/v1/organisation/${organizationId}/api-keys`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -13,12 +13,12 @@ export async function createApiKey(
|
|||||||
request: CreateApiKeyRequest,
|
request: CreateApiKeyRequest,
|
||||||
): Promise<CreateApiKeyResponse> {
|
): Promise<CreateApiKeyResponse> {
|
||||||
const { data } = await apiClient.post<CreateApiKeyResponse>(
|
const { data } = await apiClient.post<CreateApiKeyResponse>(
|
||||||
`/v1/organisations/${organizationId}/api-keys`,
|
`/v1/organisation/${organizationId}/api-keys`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteApiKey(organizationId: number, keyId: number): Promise<void> {
|
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 = {},
|
filter: AuditLogFilter = {},
|
||||||
): Promise<PaginatedResponse<AuditLogResponse>> {
|
): Promise<PaginatedResponse<AuditLogResponse>> {
|
||||||
const { data } = await apiClient.get<PaginatedResponse<AuditLogResponse>>(
|
const { data } = await apiClient.get<PaginatedResponse<AuditLogResponse>>(
|
||||||
`/v1/organisations/${orgId}/audit-logs`,
|
`/v1/organisation/${orgId}/audit-logs`,
|
||||||
{ params: toParams(filter) },
|
{ params: toParams(filter) },
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -28,7 +28,7 @@ export async function listAuditLogsByProject(
|
|||||||
filter: AuditLogFilter = {},
|
filter: AuditLogFilter = {},
|
||||||
): Promise<PaginatedResponse<AuditLogResponse>> {
|
): Promise<PaginatedResponse<AuditLogResponse>> {
|
||||||
const { data } = await apiClient.get<PaginatedResponse<AuditLogResponse>>(
|
const { data } = await apiClient.get<PaginatedResponse<AuditLogResponse>>(
|
||||||
`/v1/projects/${projectId}/audit-logs`,
|
`/v1/project/${projectId}/audit-logs`,
|
||||||
{ params: toParams(filter) },
|
{ params: toParams(filter) },
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function listEnvironments(projectId: number, page = 1, pageSize = 1
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getEnvironment(apiKey: string): Promise<EnvironmentResponse> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,17 +26,17 @@ export async function createEnvironment(request: CreateEnvironmentRequest): Prom
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateEnvironment(apiKey: string, request: UpdateEnvironmentRequest): Promise<EnvironmentResponse> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEnvironment(apiKey: string): Promise<void> {
|
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> {
|
export async function cloneEnvironment(apiKey: string, request: CloneEnvironmentRequest): Promise<EnvironmentResponse> {
|
||||||
const { data } = await apiClient.post<EnvironmentResponse>(
|
const { data } = await apiClient.post<EnvironmentResponse>(
|
||||||
`/v1/environments/${apiKey}/clone`,
|
`/v1/environment/${apiKey}/clone`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export async function listFeatureSegments(
|
|||||||
featureId: number,
|
featureId: number,
|
||||||
): Promise<FeatureSegmentResponse[]> {
|
): Promise<FeatureSegmentResponse[]> {
|
||||||
const { data } = await apiClient.get<FeatureSegmentResponse[]>(
|
const { data } = await apiClient.get<FeatureSegmentResponse[]>(
|
||||||
`/v1/environments/${envApiKey}/features/${featureId}/segments`,
|
`/v1/environment/${envApiKey}/feature/${featureId}/segments`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ export async function createFeatureSegment(
|
|||||||
request: CreateFeatureSegmentRequest,
|
request: CreateFeatureSegmentRequest,
|
||||||
): Promise<FeatureSegmentResponse> {
|
): Promise<FeatureSegmentResponse> {
|
||||||
const { data } = await apiClient.post<FeatureSegmentResponse>(
|
const { data } = await apiClient.post<FeatureSegmentResponse>(
|
||||||
`/v1/environments/${envApiKey}/features/${featureId}/segments`,
|
`/v1/environment/${envApiKey}/feature/${featureId}/segments`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -35,7 +35,7 @@ export async function updateFeatureSegment(
|
|||||||
request: UpdateFeatureSegmentRequest,
|
request: UpdateFeatureSegmentRequest,
|
||||||
): Promise<FeatureSegmentResponse> {
|
): Promise<FeatureSegmentResponse> {
|
||||||
const { data } = await apiClient.put<FeatureSegmentResponse>(
|
const { data } = await apiClient.put<FeatureSegmentResponse>(
|
||||||
`/v1/environments/${envApiKey}/features/${featureId}/segments/${id}`,
|
`/v1/environment/${envApiKey}/feature/${featureId}/segment/${id}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -46,7 +46,7 @@ export async function deleteFeatureSegment(
|
|||||||
featureId: number,
|
featureId: number,
|
||||||
id: number,
|
id: number,
|
||||||
): Promise<void> {
|
): 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(
|
export async function listIdentitySegments(
|
||||||
@@ -54,7 +54,7 @@ export async function listIdentitySegments(
|
|||||||
identityId: number,
|
identityId: number,
|
||||||
): Promise<SegmentSummaryResponse[]> {
|
): Promise<SegmentSummaryResponse[]> {
|
||||||
const { data } = await apiClient.get<SegmentSummaryResponse[]>(
|
const { data } = await apiClient.get<SegmentSummaryResponse[]>(
|
||||||
`/v1/environments/${envApiKey}/identities/${identityId}/segments`,
|
`/v1/environment/${envApiKey}/identity/${identityId}/segments`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import type {
|
|||||||
|
|
||||||
export async function listFeatureStates(envApiKey: string): Promise<FeatureStateResponse[]> {
|
export async function listFeatureStates(envApiKey: string): Promise<FeatureStateResponse[]> {
|
||||||
const { data } = await apiClient.get<FeatureStateResponse[]>(
|
const { data } = await apiClient.get<FeatureStateResponse[]>(
|
||||||
`/v1/environments/${envApiKey}/featurestates`,
|
`/v1/environment/${envApiKey}/featurestates`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFeatureState(envApiKey: string, id: number): Promise<FeatureStateResponse> {
|
export async function getFeatureState(envApiKey: string, id: number): Promise<FeatureStateResponse> {
|
||||||
const { data } = await apiClient.get<FeatureStateResponse>(
|
const { data } = await apiClient.get<FeatureStateResponse>(
|
||||||
`/v1/environments/${envApiKey}/featurestates/${id}`,
|
`/v1/environment/${envApiKey}/featurestate/${id}`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export async function updateFeatureState(
|
|||||||
request: UpdateFeatureStateRequest,
|
request: UpdateFeatureStateRequest,
|
||||||
): Promise<FeatureStateResponse> {
|
): Promise<FeatureStateResponse> {
|
||||||
const { data } = await apiClient.put<FeatureStateResponse>(
|
const { data } = await apiClient.put<FeatureStateResponse>(
|
||||||
`/v1/environments/${envApiKey}/featurestates/${id}`,
|
`/v1/environment/${envApiKey}/featurestate/${id}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -37,7 +37,7 @@ export async function patchFeatureState(
|
|||||||
request: PatchFeatureStateRequest,
|
request: PatchFeatureStateRequest,
|
||||||
): Promise<FeatureStateResponse> {
|
): Promise<FeatureStateResponse> {
|
||||||
const { data } = await apiClient.patch<FeatureStateResponse>(
|
const { data } = await apiClient.patch<FeatureStateResponse>(
|
||||||
`/v1/environments/${envApiKey}/featurestates/${id}`,
|
`/v1/environment/${envApiKey}/featurestate/${id}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function listFeatures(
|
|||||||
pageSize = 100,
|
pageSize = 100,
|
||||||
): Promise<PaginatedResponse<FeatureResponse>> {
|
): Promise<PaginatedResponse<FeatureResponse>> {
|
||||||
const { data } = await apiClient.get<PaginatedResponse<FeatureResponse>>(
|
const { data } = await apiClient.get<PaginatedResponse<FeatureResponse>>(
|
||||||
`/v1/projects/${projectId}/features`,
|
`/v1/project/${projectId}/features`,
|
||||||
{ params: { page, pageSize } },
|
{ params: { page, pageSize } },
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -21,7 +21,7 @@ export async function listFeatures(
|
|||||||
|
|
||||||
export async function getFeature(projectId: number, id: number): Promise<FeatureResponse> {
|
export async function getFeature(projectId: number, id: number): Promise<FeatureResponse> {
|
||||||
const { data } = await apiClient.get<FeatureResponse>(
|
const { data } = await apiClient.get<FeatureResponse>(
|
||||||
`/v1/projects/${projectId}/features/${id}`,
|
`/v1/project/${projectId}/feature/${id}`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ export async function createFeature(
|
|||||||
request: CreateFeatureRequest,
|
request: CreateFeatureRequest,
|
||||||
): Promise<FeatureResponse> {
|
): Promise<FeatureResponse> {
|
||||||
const { data } = await apiClient.post<FeatureResponse>(
|
const { data } = await apiClient.post<FeatureResponse>(
|
||||||
`/v1/projects/${projectId}/features`,
|
`/v1/project/${projectId}/features`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -43,7 +43,7 @@ export async function updateFeature(
|
|||||||
request: UpdateFeatureRequest,
|
request: UpdateFeatureRequest,
|
||||||
): Promise<FeatureResponse> {
|
): Promise<FeatureResponse> {
|
||||||
const { data } = await apiClient.put<FeatureResponse>(
|
const { data } = await apiClient.put<FeatureResponse>(
|
||||||
`/v1/projects/${projectId}/features/${id}`,
|
`/v1/project/${projectId}/feature/${id}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -55,12 +55,34 @@ export async function patchFeature(
|
|||||||
request: PatchFeatureRequest,
|
request: PatchFeatureRequest,
|
||||||
): Promise<FeatureResponse> {
|
): Promise<FeatureResponse> {
|
||||||
const { data } = await apiClient.patch<FeatureResponse>(
|
const { data } = await apiClient.patch<FeatureResponse>(
|
||||||
`/v1/projects/${projectId}/features/${id}`,
|
`/v1/project/${projectId}/feature/${id}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteFeature(projectId: number, id: number): Promise<void> {
|
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,
|
pageSize = 100,
|
||||||
): Promise<PaginatedResponse<IdentityResponse>> {
|
): Promise<PaginatedResponse<IdentityResponse>> {
|
||||||
const { data } = await apiClient.get<PaginatedResponse<IdentityResponse>>(
|
const { data } = await apiClient.get<PaginatedResponse<IdentityResponse>>(
|
||||||
`/v1/environments/${envApiKey}/identities`,
|
`/v1/environment/${envApiKey}/identities`,
|
||||||
{ params: { page, pageSize } },
|
{ params: { page, pageSize } },
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -26,7 +26,7 @@ export async function createIdentity(
|
|||||||
request: CreateIdentityRequest,
|
request: CreateIdentityRequest,
|
||||||
): Promise<IdentityResponse> {
|
): Promise<IdentityResponse> {
|
||||||
const { data } = await apiClient.post<IdentityResponse>(
|
const { data } = await apiClient.post<IdentityResponse>(
|
||||||
`/v1/environments/${envApiKey}/identities`,
|
`/v1/environment/${envApiKey}/identities`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -34,7 +34,7 @@ export async function createIdentity(
|
|||||||
|
|
||||||
export async function getIdentity(envApiKey: string, id: number): Promise<IdentityResponse> {
|
export async function getIdentity(envApiKey: string, id: number): Promise<IdentityResponse> {
|
||||||
const { data } = await apiClient.get<IdentityResponse>(
|
const { data } = await apiClient.get<IdentityResponse>(
|
||||||
`/v1/environments/${envApiKey}/identities/${id}`,
|
`/v1/environment/${envApiKey}/identity/${id}`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ export async function upsertIdentityTrait(
|
|||||||
request: UpsertTraitRequest,
|
request: UpsertTraitRequest,
|
||||||
): Promise<TraitResponse> {
|
): Promise<TraitResponse> {
|
||||||
const { data } = await apiClient.put<TraitResponse>(
|
const { data } = await apiClient.put<TraitResponse>(
|
||||||
`/v1/environments/${envApiKey}/identities/${identityId}/traits/${encodeURIComponent(key)}`,
|
`/v1/environment/${envApiKey}/identity/${identityId}/trait/${encodeURIComponent(key)}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -58,12 +58,12 @@ export async function deleteIdentityTrait(
|
|||||||
key: string,
|
key: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await apiClient.delete(
|
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> {
|
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(
|
export async function getIdentityFeatureStates(
|
||||||
@@ -71,7 +71,7 @@ export async function getIdentityFeatureStates(
|
|||||||
identityId: number,
|
identityId: number,
|
||||||
): Promise<FeatureStateResponse[]> {
|
): Promise<FeatureStateResponse[]> {
|
||||||
const { data } = await apiClient.get<FeatureStateResponse[]>(
|
const { data } = await apiClient.get<FeatureStateResponse[]>(
|
||||||
`/v1/environments/${envApiKey}/identities/${identityId}/featurestates`,
|
`/v1/environment/${envApiKey}/identity/${identityId}/featurestates`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ export async function setIdentityFeatureState(
|
|||||||
request: UpdateFeatureStateRequest,
|
request: UpdateFeatureStateRequest,
|
||||||
): Promise<FeatureStateResponse> {
|
): Promise<FeatureStateResponse> {
|
||||||
const { data } = await apiClient.put<FeatureStateResponse>(
|
const { data } = await apiClient.put<FeatureStateResponse>(
|
||||||
`/v1/environments/${envApiKey}/identities/${identityId}/featurestates/${featureId}`,
|
`/v1/environment/${envApiKey}/identity/${identityId}/featurestate/${featureId}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -95,6 +95,6 @@ export async function deleteIdentityFeatureState(
|
|||||||
featureId: number,
|
featureId: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await apiClient.delete(
|
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> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,27 +30,27 @@ export async function createOrganization(request: CreateOrganizationRequest): Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateOrganization(id: number, request: UpdateOrganizationRequest): Promise<OrganizationResponse> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteOrganization(id: number): Promise<void> {
|
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[]> {
|
export async function listOrganizationMembers(organizationId: number): Promise<OrganizationMemberResponse[]> {
|
||||||
const { data } = await apiClient.get<OrganizationMemberResponse[]>(
|
const { data } = await apiClient.get<OrganizationMemberResponse[]>(
|
||||||
`/v1/organisations/${organizationId}/users`,
|
`/v1/organisation/${organizationId}/users`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function inviteOrganizationMember(organizationId: number, request: InviteUserRequest): Promise<void> {
|
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> {
|
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(
|
export async function inviteOrganizationMembersByEmail(
|
||||||
@@ -58,7 +58,7 @@ export async function inviteOrganizationMembersByEmail(
|
|||||||
request: InviteUsersByEmailRequest,
|
request: InviteUsersByEmailRequest,
|
||||||
): Promise<InviteByEmailResult[]> {
|
): Promise<InviteByEmailResult[]> {
|
||||||
const { data } = await apiClient.post<InviteByEmailResult[]>(
|
const { data } = await apiClient.post<InviteByEmailResult[]>(
|
||||||
`/v1/organisations/${organizationId}/users/invite-by-email`,
|
`/v1/organisation/${organizationId}/users/invite-by-email`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -66,14 +66,14 @@ export async function inviteOrganizationMembersByEmail(
|
|||||||
|
|
||||||
export async function getInviteLink(organizationId: number): Promise<InviteTokenResponse> {
|
export async function getInviteLink(organizationId: number): Promise<InviteTokenResponse> {
|
||||||
const { data } = await apiClient.get<InviteTokenResponse>(
|
const { data } = await apiClient.get<InviteTokenResponse>(
|
||||||
`/v1/organisations/${organizationId}/invite-link`,
|
`/v1/organisation/${organizationId}/invite-link`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function regenerateInviteLink(organizationId: number): Promise<InviteTokenResponse> {
|
export async function regenerateInviteLink(organizationId: number): Promise<InviteTokenResponse> {
|
||||||
const { data } = await apiClient.post<InviteTokenResponse>(
|
const { data } = await apiClient.post<InviteTokenResponse>(
|
||||||
`/v1/organisations/${organizationId}/invite-link/regenerate`,
|
`/v1/organisation/${organizationId}/invite-link/regenerate`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -83,5 +83,5 @@ export async function acceptInvite(token: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setPrimaryOrganization(id: number): 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> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,17 +27,17 @@ export async function createProject(request: CreateProjectRequest): Promise<Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProject(id: number, request: UpdateProjectRequest): Promise<ProjectResponse> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProject(id: number): Promise<void> {
|
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[]> {
|
export async function listProjectUserPermissions(projectId: number): Promise<UserPermissionResponse[]> {
|
||||||
const { data } = await apiClient.get<UserPermissionResponse[]>(
|
const { data } = await apiClient.get<UserPermissionResponse[]>(
|
||||||
`/v1/projects/${projectId}/user-permissions`,
|
`/v1/project/${projectId}/user-permissions`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ export async function setProjectUserPermissions(
|
|||||||
request: SetUserPermissionsRequest,
|
request: SetUserPermissionsRequest,
|
||||||
): Promise<UserPermissionResponse> {
|
): Promise<UserPermissionResponse> {
|
||||||
const { data } = await apiClient.post<UserPermissionResponse>(
|
const { data } = await apiClient.post<UserPermissionResponse>(
|
||||||
`/v1/projects/${projectId}/user-permissions`,
|
`/v1/project/${projectId}/user-permissions`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -59,12 +59,12 @@ export async function updateProjectUserPermissions(
|
|||||||
request: SetUserPermissionsRequest,
|
request: SetUserPermissionsRequest,
|
||||||
): Promise<UserPermissionResponse> {
|
): Promise<UserPermissionResponse> {
|
||||||
const { data } = await apiClient.put<UserPermissionResponse>(
|
const { data } = await apiClient.put<UserPermissionResponse>(
|
||||||
`/v1/projects/${projectId}/user-permissions/${userId}`,
|
`/v1/project/${projectId}/user-permission/${userId}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeProjectUserPermissions(projectId: number, userId: number): Promise<void> {
|
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[]> {
|
export async function listSegments(projectId: number, page = 1, pageSize = 100): Promise<SegmentResponse[]> {
|
||||||
const { data } = await apiClient.get<PaginatedResponse<SegmentResponse>>(
|
const { data } = await apiClient.get<PaginatedResponse<SegmentResponse>>(
|
||||||
`/v1/projects/${projectId}/segments`,
|
`/v1/project/${projectId}/segments`,
|
||||||
{ params: { page, pageSize } },
|
{ params: { page, pageSize } },
|
||||||
);
|
);
|
||||||
return data.results;
|
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> {
|
export async function getSegment(projectId: number, id: number): Promise<SegmentResponse> {
|
||||||
const { data } = await apiClient.get<SegmentResponse>(
|
const { data } = await apiClient.get<SegmentResponse>(
|
||||||
`/v1/projects/${projectId}/segments/${id}`,
|
`/v1/project/${projectId}/segment/${id}`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ export async function createSegment(
|
|||||||
request: CreateSegmentRequest,
|
request: CreateSegmentRequest,
|
||||||
): Promise<SegmentResponse> {
|
): Promise<SegmentResponse> {
|
||||||
const { data } = await apiClient.post<SegmentResponse>(
|
const { data } = await apiClient.post<SegmentResponse>(
|
||||||
`/v1/projects/${projectId}/segments`,
|
`/v1/project/${projectId}/segments`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -38,12 +38,12 @@ export async function updateSegment(
|
|||||||
request: UpdateSegmentRequest,
|
request: UpdateSegmentRequest,
|
||||||
): Promise<SegmentResponse> {
|
): Promise<SegmentResponse> {
|
||||||
const { data } = await apiClient.put<SegmentResponse>(
|
const { data } = await apiClient.put<SegmentResponse>(
|
||||||
`/v1/projects/${projectId}/segments/${id}`,
|
`/v1/project/${projectId}/segment/${id}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSegment(projectId: number, id: number): Promise<void> {
|
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';
|
import type { TagResponse, CreateTagRequest } from '@/types/api';
|
||||||
|
|
||||||
export async function listTags(projectId: number): Promise<TagResponse[]> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTag(projectId: number, request: CreateTagRequest): Promise<TagResponse> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTag(projectId: number, id: number): Promise<void> {
|
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';
|
import type { DashboardUsageResponse } from '@/types/api';
|
||||||
|
|
||||||
export async function getUsageDashboard(environmentId: number, days = 14): Promise<DashboardUsageResponse> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@ import apiClient from './client';
|
|||||||
import type { UserProfileResponse, UpdateProfileRequest, ChangePasswordRequest } from '@/types/api';
|
import type { UserProfileResponse, UpdateProfileRequest, ChangePasswordRequest } from '@/types/api';
|
||||||
|
|
||||||
export async function getMe(): Promise<UserProfileResponse> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMe(request: UpdateProfileRequest): Promise<UserProfileResponse> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function changePassword(request: ChangePasswordRequest): Promise<void> {
|
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> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
// ─── Organization webhooks ────────────────────────────────────────────────────
|
// ─── Organization webhooks ────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function listOrgWebhooks(orgId: number): Promise<WebhookResponse[]> {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export async function createOrgWebhook(
|
|||||||
request: CreateWebhookRequest,
|
request: CreateWebhookRequest,
|
||||||
): Promise<WebhookResponse> {
|
): Promise<WebhookResponse> {
|
||||||
const { data } = await apiClient.post<WebhookResponse>(
|
const { data } = await apiClient.post<WebhookResponse>(
|
||||||
`/v1/organisations/${orgId}/webhooks`,
|
`/v1/organisation/${orgId}/webhooks`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -29,21 +29,21 @@ export async function updateOrgWebhook(
|
|||||||
request: CreateWebhookRequest,
|
request: CreateWebhookRequest,
|
||||||
): Promise<WebhookResponse> {
|
): Promise<WebhookResponse> {
|
||||||
const { data } = await apiClient.put<WebhookResponse>(
|
const { data } = await apiClient.put<WebhookResponse>(
|
||||||
`/v1/organisations/${orgId}/webhooks/${webhookId}`,
|
`/v1/organisation/${orgId}/webhook/${webhookId}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteOrgWebhook(orgId: number, webhookId: number): Promise<void> {
|
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 ─────────────────────────────────────────────────────
|
// ─── Environment webhooks ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function listEnvWebhooks(envApiKey: string): Promise<WebhookResponse[]> {
|
export async function listEnvWebhooks(envApiKey: string): Promise<WebhookResponse[]> {
|
||||||
const { data } = await apiClient.get<WebhookResponse[]>(
|
const { data } = await apiClient.get<WebhookResponse[]>(
|
||||||
`/v1/environments/${envApiKey}/webhooks`,
|
`/v1/environment/${envApiKey}/webhooks`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ export async function createEnvWebhook(
|
|||||||
request: CreateWebhookRequest,
|
request: CreateWebhookRequest,
|
||||||
): Promise<WebhookResponse> {
|
): Promise<WebhookResponse> {
|
||||||
const { data } = await apiClient.post<WebhookResponse>(
|
const { data } = await apiClient.post<WebhookResponse>(
|
||||||
`/v1/environments/${envApiKey}/webhooks`,
|
`/v1/environment/${envApiKey}/webhooks`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -65,14 +65,14 @@ export async function updateEnvWebhook(
|
|||||||
request: CreateWebhookRequest,
|
request: CreateWebhookRequest,
|
||||||
): Promise<WebhookResponse> {
|
): Promise<WebhookResponse> {
|
||||||
const { data } = await apiClient.put<WebhookResponse>(
|
const { data } = await apiClient.put<WebhookResponse>(
|
||||||
`/v1/environments/${envApiKey}/webhooks/${webhookId}`,
|
`/v1/environment/${envApiKey}/webhook/${webhookId}`,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEnvWebhook(envApiKey: string, webhookId: number): Promise<void> {
|
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 ────────────────────────────────────────────────────────────
|
// ─── Delivery logs ────────────────────────────────────────────────────────────
|
||||||
@@ -82,7 +82,7 @@ export async function listWebhookDeliveries(
|
|||||||
webhookId: number,
|
webhookId: number,
|
||||||
): Promise<WebhookDeliveryLogResponse[]> {
|
): Promise<WebhookDeliveryLogResponse[]> {
|
||||||
const { data } = await apiClient.get<WebhookDeliveryLogResponse[]>(
|
const { data } = await apiClient.get<WebhookDeliveryLogResponse[]>(
|
||||||
`/v1/environments/${envApiKey}/webhooks/${webhookId}/deliveries`,
|
`/v1/environment/${envApiKey}/webhook/${webhookId}/deliveries`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
<v-divider class="my-5" />
|
<v-divider class="my-5" />
|
||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<TagsChipInput />
|
<TagsChipInput v-if="feature" :feature="feature" @updated="onTagsUpdated" />
|
||||||
</template>
|
</template>
|
||||||
</v-tabs-window-item>
|
</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> {
|
async function onDelete(): Promise<void> {
|
||||||
if (!props.feature || deleteConfirmName.value !== props.feature.name) return;
|
if (!props.feature || deleteConfirmName.value !== props.feature.name) return;
|
||||||
const projectId = contextStore.currentProject?.id;
|
const projectId = contextStore.currentProject?.id;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div data-testid="tags-chip-input">
|
<div data-testid="tags-chip-input">
|
||||||
<div class="d-flex align-center justify-space-between mb-2">
|
<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
|
<v-btn
|
||||||
size="x-small"
|
size="x-small"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
@@ -71,6 +71,8 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-expand-transition>
|
</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 -->
|
<!-- Tag chips -->
|
||||||
<div v-if="isLoading" class="d-flex justify-center py-3">
|
<div v-if="isLoading" class="d-flex justify-center py-3">
|
||||||
<v-progress-circular indeterminate size="20" width="2" color="primary" />
|
<v-progress-circular indeterminate size="20" width="2" color="primary" />
|
||||||
@@ -82,8 +84,12 @@
|
|||||||
:key="tag.id"
|
:key="tag.id"
|
||||||
size="small"
|
size="small"
|
||||||
closable
|
closable
|
||||||
|
:variant="isAssigned(tag) ? 'flat' : 'outlined'"
|
||||||
:color="tag.color"
|
:color="tag.color"
|
||||||
|
:prepend-icon="isAssigned(tag) ? 'ri-check-line' : undefined"
|
||||||
|
:loading="togglingTagId === tag.id"
|
||||||
:data-testid="`tag-chip-${tag.label}`"
|
:data-testid="`tag-chip-${tag.label}`"
|
||||||
|
@click="onToggleTag(tag)"
|
||||||
@click:close="onDeleteTag(tag)"
|
@click:close="onDeleteTag(tag)"
|
||||||
>
|
>
|
||||||
{{ tag.label }}
|
{{ tag.label }}
|
||||||
@@ -103,14 +109,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||||
import { listTags, createTag, deleteTag } from '@/api/tags';
|
import { listTags, createTag, deleteTag } from '@/api/tags';
|
||||||
|
import { assignFeatureTag, removeFeatureTag } from '@/api/features';
|
||||||
import { useContextStore } from '@/stores/context';
|
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 contextStore = useContextStore();
|
||||||
|
|
||||||
const tags = ref<TagResponse[]>([]);
|
const tags = ref<TagResponse[]>([]);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const isCreating = ref(false);
|
const isCreating = ref(false);
|
||||||
|
const togglingTagId = ref<number | null>(null);
|
||||||
const showCreateForm = ref(false);
|
const showCreateForm = ref(false);
|
||||||
const newTagLabel = ref('');
|
const newTagLabel = ref('');
|
||||||
const newTagColor = ref('#1565C0');
|
const newTagColor = ref('#1565C0');
|
||||||
@@ -118,6 +136,10 @@ const errorMessage = ref<string | null>(null);
|
|||||||
const createFormRef = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null);
|
const createFormRef = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null);
|
||||||
const tagLabelInputRef = ref<{ focus: () => void } | 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 {
|
function focusTagLabel(): void {
|
||||||
nextTick(() => tagLabelInputRef.value?.focus());
|
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> {
|
async function onDeleteTag(tag: TagResponse): Promise<void> {
|
||||||
const projectId = contextStore.currentProject?.id;
|
const projectId = contextStore.currentProject?.id;
|
||||||
if (!projectId) return;
|
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 Footer from '@/layouts/components/Footer.vue';
|
||||||
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue';
|
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue';
|
||||||
import UserProfile from '@/layouts/components/UserProfile.vue';
|
import UserProfile from '@/layouts/components/UserProfile.vue';
|
||||||
|
import OrgHeaderSwitcher from '@/components/nav/OrgHeaderSwitcher.vue';
|
||||||
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue';
|
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue';
|
|||||||
|
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
|
|
||||||
|
<OrgHeaderSwitcher class="me-4" />
|
||||||
<NavbarThemeSwitcher class="me-1" />
|
<NavbarThemeSwitcher class="me-1" />
|
||||||
<UserProfile />
|
<UserProfile />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue';
|
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue';
|
||||||
import VerticalNavSectionTitle from '@layouts/components/VerticalNavSectionTitle.vue';
|
import VerticalNavSectionTitle from '@layouts/components/VerticalNavSectionTitle.vue';
|
||||||
import OrgSelector from '@/components/nav/OrgSelector.vue';
|
|
||||||
import ProjectSelector from '@/components/nav/ProjectSelector.vue';
|
import ProjectSelector from '@/components/nav/ProjectSelector.vue';
|
||||||
import EnvironmentTabBar from '@/components/nav/EnvironmentTabBar.vue';
|
import EnvironmentTabBar from '@/components/nav/EnvironmentTabBar.vue';
|
||||||
</script>
|
</script>
|
||||||
@@ -15,7 +14,6 @@ import EnvironmentTabBar from '@/components/nav/EnvironmentTabBar.vue';
|
|||||||
|
|
||||||
<VerticalNavSectionTitle :item="{ heading: 'Context' }" />
|
<VerticalNavSectionTitle :item="{ heading: 'Context' }" />
|
||||||
<div class="nav-context-selectors px-4 d-flex flex-column ga-3">
|
<div class="nav-context-selectors px-4 d-flex flex-column ga-3">
|
||||||
<OrgSelector />
|
|
||||||
<ProjectSelector />
|
<ProjectSelector />
|
||||||
<EnvironmentTabBar />
|
<EnvironmentTabBar />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import VueApexCharts from 'vue3-apexcharts';
|
|||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
|
import '@fontsource-variable/inter';
|
||||||
import '@core/scss/template/index.scss';
|
import '@core/scss/template/index.scss';
|
||||||
import '@layouts/styles/index.scss';
|
import '@layouts/styles/index.scss';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export interface FeatureResponse {
|
|||||||
defaultEnabled: boolean;
|
defaultEnabled: boolean;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
tags: TagResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateFeatureRequest {
|
export interface CreateFeatureRequest {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<!-- Name + description -->
|
<!-- Name + description -->
|
||||||
<template #item.name="{ item }: { item: FeatureResponse }">
|
<template #item.name="{ item }: { item: FeatureResponse }">
|
||||||
<div>
|
<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>
|
<span class="text-subtitle-1 font-weight-bold">{{ item.name }}</span>
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
v-if="(featureSegmentsMap.get(item.id) ?? []).length > 0"
|
v-if="(featureSegmentsMap.get(item.id) ?? []).length > 0"
|
||||||
@@ -72,6 +72,21 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</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>
|
</div>
|
||||||
<p v-if="item.description" class="text-caption text-medium-emphasis mb-0">
|
<p v-if="item.description" class="text-caption text-medium-emphasis mb-0">
|
||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class AuditLogsController(
|
|||||||
ProjectService projectService,
|
ProjectService projectService,
|
||||||
EnvironmentService environmentService) : ControllerBase
|
EnvironmentService environmentService) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("api/v1/organisations/{id}/audit-logs")]
|
[HttpGet("api/v1/organisation/{id}/audit-logs")]
|
||||||
public async Task<ActionResult<PaginatedResponse<AuditLogResponse>>> ListByOrganization(
|
public async Task<ActionResult<PaginatedResponse<AuditLogResponse>>> ListByOrganization(
|
||||||
int id,
|
int id,
|
||||||
[FromQuery] AuditLogFilter filter,
|
[FromQuery] AuditLogFilter filter,
|
||||||
@@ -31,7 +31,7 @@ public class AuditLogsController(
|
|||||||
return Ok(new PaginatedResponse<AuditLogResponse>(total, null, null, logs.ToList()));
|
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(
|
public async Task<ActionResult<PaginatedResponse<AuditLogResponse>>> ListByProject(
|
||||||
int projectId,
|
int projectId,
|
||||||
[FromQuery] AuditLogFilter filter,
|
[FromQuery] AuditLogFilter filter,
|
||||||
@@ -44,7 +44,7 @@ public class AuditLogsController(
|
|||||||
return Ok(new PaginatedResponse<AuditLogResponse>(total, null, null, logs.ToList()));
|
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(
|
public async Task<ActionResult<PaginatedResponse<AuditLogResponse>>> ListByEnvironment(
|
||||||
string apiKey,
|
string apiKey,
|
||||||
[FromQuery] AuditLogFilter filter,
|
[FromQuery] AuditLogFilter filter,
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ public static class ApiKeyEndpoints
|
|||||||
public static void MapApiKeyEndpoints(this WebApplication app)
|
public static void MapApiKeyEndpoints(this WebApplication app)
|
||||||
{
|
{
|
||||||
var group = app
|
var group = app
|
||||||
.MapGroup("/api/v1/organisations/{organizationId:int}/api-keys")
|
.MapGroup("/api/v1/organisation/{organizationId:int}")
|
||||||
.RequireAuthorization(AuthorizationPolicies.OrganizationAdmin)
|
.RequireAuthorization(AuthorizationPolicies.OrganizationAdmin)
|
||||||
.WithTags("ApiKeys");
|
.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(
|
var (key, rawKey) = await apiKeyService.CreateAsync(
|
||||||
organizationId, request.Name, request.ExpiresAt, ct);
|
organizationId, request.Name, request.ExpiresAt, ct);
|
||||||
@@ -20,14 +20,14 @@ public static class ApiKeyEndpoints
|
|||||||
|
|
||||||
}).WithName("CreateApiKey");
|
}).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);
|
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)));
|
return Results.Ok(keys.Select(k => new ApiKeyResponse(k.Id, k.Name, k.Prefix, k.IsActive, k.ExpiresAt, k.CreatedAt)));
|
||||||
|
|
||||||
}).WithName("ListApiKeys");
|
}).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);
|
await apiKeyService.RevokeAsync(organizationId, keyId, ct);
|
||||||
return Results.NoContent();
|
return Results.NoContent();
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
|||||||
namespace MicCheck.Api.Environments;
|
namespace MicCheck.Api.Environments;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/environments")]
|
|
||||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||||
[EnableRateLimiting("AdminApi")]
|
[EnableRateLimiting("AdminApi")]
|
||||||
public class EnvironmentsController(EnvironmentService environmentService, WebhookService webhookService) : ControllerBase
|
public class EnvironmentsController(EnvironmentService environmentService, WebhookService webhookService) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet("api/v1/environments")]
|
||||||
public async Task<ActionResult<PaginatedResponse<EnvironmentResponse>>> List(
|
public async Task<ActionResult<PaginatedResponse<EnvironmentResponse>>> List(
|
||||||
[FromQuery] int projectId,
|
[FromQuery] int projectId,
|
||||||
[FromQuery] int page = 1,
|
[FromQuery] int page = 1,
|
||||||
@@ -26,14 +25,14 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(new PaginatedResponse<EnvironmentResponse>(all.Count, null, null, paged));
|
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)
|
public async Task<ActionResult<EnvironmentResponse>> Create(CreateEnvironmentRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.CreateAsync(request.ProjectId, request.Name, ct);
|
var environment = await environmentService.CreateAsync(request.ProjectId, request.Name, ct);
|
||||||
return CreatedAtAction(nameof(GetByApiKey), new { apiKey = environment.ApiKey }, EnvironmentResponse.From(environment));
|
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)
|
public async Task<ActionResult<EnvironmentResponse>> GetByApiKey(string apiKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -41,7 +40,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(EnvironmentResponse.From(environment));
|
return Ok(EnvironmentResponse.From(environment));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{apiKey}")]
|
[HttpPut("api/v1/environment/{apiKey}")]
|
||||||
public async Task<ActionResult<EnvironmentResponse>> Update(string apiKey, UpdateEnvironmentRequest request, CancellationToken ct)
|
public async Task<ActionResult<EnvironmentResponse>> Update(string apiKey, UpdateEnvironmentRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -51,7 +50,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(EnvironmentResponse.From(updated));
|
return Ok(EnvironmentResponse.From(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{apiKey}")]
|
[HttpDelete("api/v1/environment/{apiKey}")]
|
||||||
public async Task<IActionResult> Delete(string apiKey, CancellationToken ct)
|
public async Task<IActionResult> Delete(string apiKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -61,7 +60,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{apiKey}/clone")]
|
[HttpPost("api/v1/environment/{apiKey}/clone")]
|
||||||
public async Task<ActionResult<EnvironmentResponse>> Clone(string apiKey, CloneEnvironmentRequest request, CancellationToken ct)
|
public async Task<ActionResult<EnvironmentResponse>> Clone(string apiKey, CloneEnvironmentRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var source = await environmentService.FindByApiKeyAsync(apiKey, 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));
|
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)
|
public async Task<ActionResult<IReadOnlyList<WebhookResponse>>> ListWebhooks(string apiKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, 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());
|
return Ok(webhooks.Select(WebhookResponse.From).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{apiKey}/webhooks")]
|
[HttpPost("api/v1/environment/{apiKey}/webhooks")]
|
||||||
public async Task<ActionResult<WebhookResponse>> CreateWebhook(
|
public async Task<ActionResult<WebhookResponse>> CreateWebhook(
|
||||||
string apiKey, CreateWebhookRequest request, CancellationToken ct)
|
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));
|
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(
|
public async Task<ActionResult<WebhookResponse>> UpdateWebhook(
|
||||||
string apiKey, int id, CreateWebhookRequest request, CancellationToken ct)
|
string apiKey, int id, CreateWebhookRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -106,7 +105,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(WebhookResponse.From(updated));
|
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(
|
public async Task<ActionResult<IReadOnlyList<WebhookDeliveryLogResponse>>> ListWebhookDeliveries(
|
||||||
string apiKey, int id, CancellationToken ct)
|
string apiKey, int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -120,7 +119,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(logs.Select(WebhookDeliveryLogResponse.From).ToList());
|
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)
|
public async Task<IActionResult> DeleteWebhook(string apiKey, int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -133,7 +132,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{apiKey}/audit-logs")]
|
[HttpGet("api/v1/environment/{apiKey}/audit-logs")]
|
||||||
public async Task<ActionResult<IReadOnlyList<Audit.AuditLogResponse>>> ListAuditLogs(
|
public async Task<ActionResult<IReadOnlyList<Audit.AuditLogResponse>>> ListAuditLogs(
|
||||||
string apiKey,
|
string apiKey,
|
||||||
[FromServices] Audit.AuditLogQueryService auditLogQueryService,
|
[FromServices] Audit.AuditLogQueryService auditLogQueryService,
|
||||||
@@ -146,7 +145,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(logs.ToList());
|
return Ok(logs.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{apiKey}/identities")]
|
[HttpGet("api/v1/environment/{apiKey}/identities")]
|
||||||
public async Task<ActionResult<PaginatedResponse<Identities.AdminIdentityResponse>>> ListIdentities(
|
public async Task<ActionResult<PaginatedResponse<Identities.AdminIdentityResponse>>> ListIdentities(
|
||||||
string apiKey,
|
string apiKey,
|
||||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
@@ -163,7 +162,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(new PaginatedResponse<Identities.AdminIdentityResponse>(total, null, null, results));
|
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(
|
public async Task<ActionResult<Identities.AdminIdentityResponse>> CreateIdentity(
|
||||||
string apiKey,
|
string apiKey,
|
||||||
[FromBody] Identities.CreateIdentityRequest request,
|
[FromBody] Identities.CreateIdentityRequest request,
|
||||||
@@ -181,7 +180,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
Identities.AdminIdentityResponse.From(identity));
|
Identities.AdminIdentityResponse.From(identity));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{apiKey}/identities/{id}")]
|
[HttpGet("api/v1/environment/{apiKey}/identity/{id}")]
|
||||||
public async Task<ActionResult<Identities.AdminIdentityResponse>> GetIdentity(
|
public async Task<ActionResult<Identities.AdminIdentityResponse>> GetIdentity(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
@@ -195,7 +194,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(Identities.AdminIdentityResponse.From(identity));
|
return Ok(Identities.AdminIdentityResponse.From(identity));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{apiKey}/identities/{id}")]
|
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}")]
|
||||||
public async Task<IActionResult> DeleteIdentity(
|
public async Task<IActionResult> DeleteIdentity(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
@@ -211,7 +210,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{apiKey}/identities/{id}/traits/{key}")]
|
[HttpPut("api/v1/environment/{apiKey}/identity/{id}/trait/{key}")]
|
||||||
public async Task<ActionResult<Identities.TraitResponse>> UpsertIdentityTrait(
|
public async Task<ActionResult<Identities.TraitResponse>> UpsertIdentityTrait(
|
||||||
string apiKey, int id, string key,
|
string apiKey, int id, string key,
|
||||||
[FromBody] Identities.UpsertTraitRequest request,
|
[FromBody] Identities.UpsertTraitRequest request,
|
||||||
@@ -226,7 +225,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{apiKey}/identities/{id}/traits/{key}")]
|
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}/trait/{key}")]
|
||||||
public async Task<IActionResult> DeleteIdentityTrait(
|
public async Task<IActionResult> DeleteIdentityTrait(
|
||||||
string apiKey, int id, string key,
|
string apiKey, int id, string key,
|
||||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
@@ -240,7 +239,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{apiKey}/identities/{id}/featurestates")]
|
[HttpGet("api/v1/environment/{apiKey}/identity/{id}/featurestates")]
|
||||||
public async Task<ActionResult<IReadOnlyList<Features.FeatureStateResponse>>> GetIdentityFeatureStates(
|
public async Task<ActionResult<IReadOnlyList<Features.FeatureStateResponse>>> GetIdentityFeatureStates(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
@@ -256,7 +255,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(states.Select(Features.FeatureStateResponse.From).ToList());
|
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(
|
public async Task<ActionResult<Features.FeatureStateResponse>> SetIdentityFeatureState(
|
||||||
string apiKey, int id, int featureId,
|
string apiKey, int id, int featureId,
|
||||||
Features.UpdateFeatureStateRequest request,
|
Features.UpdateFeatureStateRequest request,
|
||||||
@@ -273,7 +272,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(Features.FeatureStateResponse.From(state));
|
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(
|
public async Task<IActionResult> DeleteIdentityFeatureState(
|
||||||
string apiKey, int id, int featureId,
|
string apiKey, int id, int featureId,
|
||||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
@@ -289,7 +288,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{apiKey}/featurestates")]
|
[HttpGet("api/v1/environment/{apiKey}/featurestates")]
|
||||||
public async Task<ActionResult<IReadOnlyList<Features.FeatureStateResponse>>> ListFeatureStates(
|
public async Task<ActionResult<IReadOnlyList<Features.FeatureStateResponse>>> ListFeatureStates(
|
||||||
string apiKey,
|
string apiKey,
|
||||||
[FromServices] Features.FeatureStateService featureStateService,
|
[FromServices] Features.FeatureStateService featureStateService,
|
||||||
@@ -302,7 +301,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(states.Select(Features.FeatureStateResponse.From).ToList());
|
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(
|
public async Task<ActionResult<Features.FeatureStateResponse>> GetFeatureState(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] Features.FeatureStateService featureStateService,
|
[FromServices] Features.FeatureStateService featureStateService,
|
||||||
@@ -316,7 +315,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(Features.FeatureStateResponse.From(state));
|
return Ok(Features.FeatureStateResponse.From(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{apiKey}/featurestates/{id}")]
|
[HttpPut("api/v1/environment/{apiKey}/featurestate/{id}")]
|
||||||
public async Task<ActionResult<Features.FeatureStateResponse>> UpdateFeatureState(
|
public async Task<ActionResult<Features.FeatureStateResponse>> UpdateFeatureState(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
Features.UpdateFeatureStateRequest request,
|
Features.UpdateFeatureStateRequest request,
|
||||||
@@ -333,7 +332,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(Features.FeatureStateResponse.From(updated));
|
return Ok(Features.FeatureStateResponse.From(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{apiKey}/featurestates/{id}")]
|
[HttpPatch("api/v1/environment/{apiKey}/featurestate/{id}")]
|
||||||
public async Task<ActionResult<Features.FeatureStateResponse>> PatchFeatureState(
|
public async Task<ActionResult<Features.FeatureStateResponse>> PatchFeatureState(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
Features.PatchFeatureStateRequest request,
|
Features.PatchFeatureStateRequest request,
|
||||||
@@ -352,7 +351,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
|
|
||||||
// ─── Feature Segments ────────────────────────────────────────────────────
|
// ─── Feature Segments ────────────────────────────────────────────────────
|
||||||
|
|
||||||
[HttpGet("{apiKey}/features/{featureId}/segments")]
|
[HttpGet("api/v1/environment/{apiKey}/feature/{featureId}/segments")]
|
||||||
public async Task<ActionResult<IReadOnlyList<Features.FeatureSegmentResponse>>> ListFeatureSegments(
|
public async Task<ActionResult<IReadOnlyList<Features.FeatureSegmentResponse>>> ListFeatureSegments(
|
||||||
string apiKey, int featureId,
|
string apiKey, int featureId,
|
||||||
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||||
@@ -365,7 +364,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(results);
|
return Ok(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{apiKey}/features/{featureId}/segments")]
|
[HttpPost("api/v1/environment/{apiKey}/feature/{featureId}/segments")]
|
||||||
public async Task<ActionResult<Features.FeatureSegmentResponse>> CreateFeatureSegment(
|
public async Task<ActionResult<Features.FeatureSegmentResponse>> CreateFeatureSegment(
|
||||||
string apiKey, int featureId,
|
string apiKey, int featureId,
|
||||||
Features.CreateFeatureSegmentRequest request,
|
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(
|
public async Task<ActionResult<Features.FeatureSegmentResponse>> UpdateFeatureSegment(
|
||||||
string apiKey, int featureId, int id,
|
string apiKey, int featureId, int id,
|
||||||
Features.UpdateFeatureSegmentRequest request,
|
Features.UpdateFeatureSegmentRequest request,
|
||||||
@@ -403,7 +402,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{apiKey}/features/{featureId}/segments/{id}")]
|
[HttpDelete("api/v1/environment/{apiKey}/feature/{featureId}/segment/{id}")]
|
||||||
public async Task<IActionResult> DeleteFeatureSegment(
|
public async Task<IActionResult> DeleteFeatureSegment(
|
||||||
string apiKey, int featureId, int id,
|
string apiKey, int featureId, int id,
|
||||||
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||||
@@ -418,7 +417,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
|
|
||||||
// ─── Identity Segments ───────────────────────────────────────────────────
|
// ─── Identity Segments ───────────────────────────────────────────────────
|
||||||
|
|
||||||
[HttpGet("{apiKey}/identities/{id}/segments")]
|
[HttpGet("api/v1/environment/{apiKey}/identity/{id}/segments")]
|
||||||
public async Task<ActionResult<IReadOnlyList<Segments.SegmentSummaryResponse>>> GetIdentitySegments(
|
public async Task<ActionResult<IReadOnlyList<Segments.SegmentSummaryResponse>>> GetIdentitySegments(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ public record FeatureResponse(
|
|||||||
string? Description,
|
string? Description,
|
||||||
bool DefaultEnabled,
|
bool DefaultEnabled,
|
||||||
int ProjectId,
|
int ProjectId,
|
||||||
DateTimeOffset CreatedAt
|
DateTimeOffset CreatedAt,
|
||||||
|
IReadOnlyList<TagResponse> Tags
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public static FeatureResponse From(Feature feature) => new(
|
public static FeatureResponse From(Feature feature) => new(
|
||||||
@@ -19,5 +20,6 @@ public record FeatureResponse(
|
|||||||
feature.Description,
|
feature.Description,
|
||||||
feature.DefaultEnabled,
|
feature.DefaultEnabled,
|
||||||
feature.ProjectId,
|
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)
|
public async Task<IReadOnlyList<Feature>> ListByProjectAsync(int projectId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await db.Features
|
return await db.Features
|
||||||
|
.Include(f => f.Tags)
|
||||||
.Where(f => f.ProjectId == projectId)
|
.Where(f => f.ProjectId == projectId)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Feature?> FindByIdAsync(int id, CancellationToken ct = default)
|
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(
|
public async Task<Feature> CreateAsync(
|
||||||
@@ -80,7 +81,7 @@ public class FeatureService(MicCheckDbContext db, AuditService auditService, Web
|
|||||||
public async Task<Feature> UpdateAsync(
|
public async Task<Feature> UpdateAsync(
|
||||||
int id, string name, string? description, CancellationToken ct = default)
|
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.");
|
?? throw new KeyNotFoundException($"Feature {id} not found.");
|
||||||
|
|
||||||
var before = new { feature.Name, feature.Description };
|
var before = new { feature.Name, feature.Description };
|
||||||
@@ -101,6 +102,40 @@ public class FeatureService(MicCheckDbContext db, AuditService auditService, Web
|
|||||||
return feature;
|
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)
|
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var feature = await db.Features.FirstOrDefaultAsync(f => f.Id == id, ct);
|
var feature = await db.Features.FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Caching.Memory;
|
|||||||
namespace MicCheck.Api.Features;
|
namespace MicCheck.Api.Features;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/environments/{environmentId}/usage")]
|
[Route("api/v1/environment/{environmentId}/usage")]
|
||||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||||
[EnableRateLimiting("AdminApi")]
|
[EnableRateLimiting("AdminApi")]
|
||||||
public class FeatureUsageController(FeatureUsageQueryService queryService, IMemoryCache cache) : ControllerBase
|
public class FeatureUsageController(FeatureUsageQueryService queryService, IMemoryCache cache) : ControllerBase
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
|||||||
namespace MicCheck.Api.Features;
|
namespace MicCheck.Api.Features;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/projects/{projectId}/features")]
|
|
||||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||||
[EnableRateLimiting("AdminApi")]
|
[EnableRateLimiting("AdminApi")]
|
||||||
public class FeaturesController(FeatureService featureService) : ControllerBase
|
public class FeaturesController(FeatureService featureService) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet("api/v1/project/{projectId}/features")]
|
||||||
public async Task<ActionResult<PaginatedResponse<FeatureResponse>>> List(
|
public async Task<ActionResult<PaginatedResponse<FeatureResponse>>> List(
|
||||||
int projectId,
|
int projectId,
|
||||||
[FromQuery] int page = 1,
|
[FromQuery] int page = 1,
|
||||||
@@ -25,7 +24,7 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
|||||||
return Ok(new PaginatedResponse<FeatureResponse>(all.Count, null, null, paged));
|
return Ok(new PaginatedResponse<FeatureResponse>(all.Count, null, null, paged));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost("api/v1/project/{projectId}/features")]
|
||||||
public async Task<ActionResult<FeatureResponse>> Create(
|
public async Task<ActionResult<FeatureResponse>> Create(
|
||||||
int projectId, CreateFeatureRequest request, CancellationToken ct)
|
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)
|
public async Task<ActionResult<FeatureResponse>> GetById(int projectId, int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var feature = await featureService.FindByIdAsync(id, ct);
|
var feature = await featureService.FindByIdAsync(id, ct);
|
||||||
@@ -49,7 +48,7 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
|||||||
return Ok(FeatureResponse.From(feature));
|
return Ok(FeatureResponse.From(feature));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("api/v1/project/{projectId}/feature/{id}")]
|
||||||
public async Task<ActionResult<FeatureResponse>> Update(
|
public async Task<ActionResult<FeatureResponse>> Update(
|
||||||
int projectId, int id, UpdateFeatureRequest request, CancellationToken ct)
|
int projectId, int id, UpdateFeatureRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -60,7 +59,7 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
|||||||
return Ok(FeatureResponse.From(updated));
|
return Ok(FeatureResponse.From(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{id}")]
|
[HttpPatch("api/v1/project/{projectId}/feature/{id}")]
|
||||||
public async Task<ActionResult<FeatureResponse>> Patch(
|
public async Task<ActionResult<FeatureResponse>> Patch(
|
||||||
int projectId, int id, PatchFeatureRequest request, CancellationToken ct)
|
int projectId, int id, PatchFeatureRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -75,7 +74,7 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
|||||||
return Ok(FeatureResponse.From(updated));
|
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)
|
public async Task<IActionResult> Delete(int projectId, int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var feature = await featureService.FindByIdAsync(id, ct);
|
var feature = await featureService.FindByIdAsync(id, ct);
|
||||||
@@ -84,4 +83,35 @@ public class FeaturesController(FeatureService featureService) : ControllerBase
|
|||||||
await featureService.DeleteAsync(id, ct);
|
await featureService.DeleteAsync(id, ct);
|
||||||
return NoContent();
|
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;
|
namespace MicCheck.Api.Features;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/projects/{projectId}/tags")]
|
|
||||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||||
[EnableRateLimiting("AdminApi")]
|
[EnableRateLimiting("AdminApi")]
|
||||||
public class TagsController(TagService tagService) : ControllerBase
|
public class TagsController(TagService tagService) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet("api/v1/project/{projectId}/tags")]
|
||||||
public async Task<ActionResult<IReadOnlyList<TagResponse>>> List(int projectId, CancellationToken ct)
|
public async Task<ActionResult<IReadOnlyList<TagResponse>>> List(int projectId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tags = await tagService.ListByProjectAsync(projectId, ct);
|
var tags = await tagService.ListByProjectAsync(projectId, ct);
|
||||||
return Ok(tags.Select(TagResponse.From).ToList());
|
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)
|
public async Task<ActionResult<TagResponse>> Create(int projectId, CreateTagRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tag = await tagService.CreateAsync(projectId, request.Label, request.Color, ct);
|
var tag = await tagService.CreateAsync(projectId, request.Label, request.Color, ct);
|
||||||
return CreatedAtAction(nameof(List), new { projectId }, TagResponse.From(tag));
|
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)
|
public async Task<IActionResult> Delete(int projectId, int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tag = await tagService.FindByIdAsync(id, ct);
|
var tag = await tagService.FindByIdAsync(id, ct);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
namespace MicCheck.Api.Identities;
|
namespace MicCheck.Api.Identities;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/identities")]
|
[Route("api/v1/identity")]
|
||||||
[Authorize(Policy = AuthorizationPolicies.FlagsApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.FlagsApiAccess)]
|
||||||
public class IdentitiesController(
|
public class IdentitiesController(
|
||||||
FeatureEvaluationService featureEvaluationService,
|
FeatureEvaluationService featureEvaluationService,
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
|||||||
namespace MicCheck.Api.Organizations;
|
namespace MicCheck.Api.Organizations;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/organisations")]
|
|
||||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||||
[EnableRateLimiting("AdminApi")]
|
[EnableRateLimiting("AdminApi")]
|
||||||
public class OrganizationsController(OrganizationService organizationService, WebhookService webhookService) : ControllerBase
|
public class OrganizationsController(OrganizationService organizationService, WebhookService webhookService) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet("api/v1/organisations")]
|
||||||
public async Task<ActionResult<PaginatedResponse<OrganizationResponse>>> List(
|
public async Task<ActionResult<PaginatedResponse<OrganizationResponse>>> List(
|
||||||
[FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
|
[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));
|
return Ok(new PaginatedResponse<OrganizationResponse>(all.Count, null, null, paged));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost("api/v1/organisations")]
|
||||||
public async Task<ActionResult<OrganizationResponse>> Create(
|
public async Task<ActionResult<OrganizationResponse>> Create(
|
||||||
CreateOrganizationRequest request, CancellationToken ct)
|
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));
|
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)
|
public async Task<ActionResult<OrganizationResponse>> GetById(int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var org = await organizationService.FindByIdAsync(id, ct);
|
var org = await organizationService.FindByIdAsync(id, ct);
|
||||||
@@ -47,7 +46,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return Ok(OrganizationResponse.From(org));
|
return Ok(OrganizationResponse.From(org));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("api/v1/organisation/{id}")]
|
||||||
public async Task<ActionResult<OrganizationResponse>> Update(
|
public async Task<ActionResult<OrganizationResponse>> Update(
|
||||||
int id, UpdateOrganizationRequest request, CancellationToken ct)
|
int id, UpdateOrganizationRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -58,7 +57,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return Ok(OrganizationResponse.From(updated));
|
return Ok(OrganizationResponse.From(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}/primary")]
|
[HttpPut("api/v1/organisation/{id}/primary")]
|
||||||
public async Task<IActionResult> SetPrimary(int id, CancellationToken ct)
|
public async Task<IActionResult> SetPrimary(int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var userId = GetCurrentUserId();
|
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)
|
public async Task<IActionResult> Delete(int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var org = await organizationService.FindByIdAsync(id, ct);
|
var org = await organizationService.FindByIdAsync(id, ct);
|
||||||
@@ -85,7 +84,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/users")]
|
[HttpGet("api/v1/organisation/{id}/users")]
|
||||||
public async Task<ActionResult<IReadOnlyList<OrganizationMemberResponse>>> ListUsers(
|
public async Task<ActionResult<IReadOnlyList<OrganizationMemberResponse>>> ListUsers(
|
||||||
int id, CancellationToken ct)
|
int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -96,7 +95,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return Ok(members.Select(OrganizationMemberResponse.From).ToList());
|
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)
|
public async Task<IActionResult> InviteUser(int id, InviteUserRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var org = await organizationService.FindByIdAsync(id, ct);
|
var org = await organizationService.FindByIdAsync(id, ct);
|
||||||
@@ -107,7 +106,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/users/invite-by-email")]
|
[HttpPost("api/v1/organisation/{id}/users/invite-by-email")]
|
||||||
public async Task<ActionResult<IReadOnlyList<InviteByEmailResult>>> InviteUsersByEmail(
|
public async Task<ActionResult<IReadOnlyList<InviteByEmailResult>>> InviteUsersByEmail(
|
||||||
int id, InviteUsersByEmailRequest request, CancellationToken ct)
|
int id, InviteUsersByEmailRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -118,7 +117,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return Ok(results);
|
return Ok(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/invite-link")]
|
[HttpGet("api/v1/organisation/{id}/invite-link")]
|
||||||
public async Task<ActionResult<InviteTokenResponse>> GetInviteLink(int id, CancellationToken ct)
|
public async Task<ActionResult<InviteTokenResponse>> GetInviteLink(int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var org = await organizationService.FindByIdAsync(id, ct);
|
var org = await organizationService.FindByIdAsync(id, ct);
|
||||||
@@ -128,7 +127,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return Ok(new InviteTokenResponse(token));
|
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)
|
public async Task<ActionResult<InviteTokenResponse>> RegenerateInviteLink(int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var org = await organizationService.FindByIdAsync(id, ct);
|
var org = await organizationService.FindByIdAsync(id, ct);
|
||||||
@@ -138,7 +137,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return Ok(new InviteTokenResponse(token));
|
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)
|
public async Task<IActionResult> AcceptInvite(string token, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var userId = GetCurrentUserId();
|
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)
|
public async Task<IActionResult> RemoveUser(int id, int userId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var org = await organizationService.FindByIdAsync(id, ct);
|
var org = await organizationService.FindByIdAsync(id, ct);
|
||||||
@@ -165,7 +164,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/webhooks")]
|
[HttpGet("api/v1/organisation/{id}/webhooks")]
|
||||||
public async Task<ActionResult<IReadOnlyList<WebhookResponse>>> ListWebhooks(int id, CancellationToken ct)
|
public async Task<ActionResult<IReadOnlyList<WebhookResponse>>> ListWebhooks(int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var org = await organizationService.FindByIdAsync(id, 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());
|
return Ok(webhooks.Select(WebhookResponse.From).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/webhooks")]
|
[HttpPost("api/v1/organisation/{id}/webhooks")]
|
||||||
public async Task<ActionResult<WebhookResponse>> CreateWebhook(
|
public async Task<ActionResult<WebhookResponse>> CreateWebhook(
|
||||||
int id, CreateWebhookRequest request, CancellationToken ct)
|
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));
|
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(
|
public async Task<ActionResult<WebhookResponse>> UpdateWebhook(
|
||||||
int id, int webhookId, CreateWebhookRequest request, CancellationToken ct)
|
int id, int webhookId, CreateWebhookRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -200,7 +199,7 @@ public class OrganizationsController(OrganizationService organizationService, We
|
|||||||
return Ok(WebhookResponse.From(updated));
|
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)
|
public async Task<IActionResult> DeleteWebhook(int id, int webhookId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var org = await organizationService.FindByIdAsync(id, ct);
|
var org = await organizationService.FindByIdAsync(id, ct);
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
|||||||
namespace MicCheck.Api.Projects;
|
namespace MicCheck.Api.Projects;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/projects")]
|
|
||||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||||
[EnableRateLimiting("AdminApi")]
|
[EnableRateLimiting("AdminApi")]
|
||||||
public class ProjectsController(ProjectService projectService) : ControllerBase
|
public class ProjectsController(ProjectService projectService) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet("api/v1/projects")]
|
||||||
public async Task<ActionResult<PaginatedResponse<ProjectResponse>>> List(
|
public async Task<ActionResult<PaginatedResponse<ProjectResponse>>> List(
|
||||||
[FromQuery] int organizationId,
|
[FromQuery] int organizationId,
|
||||||
[FromQuery] int page = 1,
|
[FromQuery] int page = 1,
|
||||||
@@ -25,14 +24,14 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
|||||||
return Ok(new PaginatedResponse<ProjectResponse>(all.Count, null, null, paged));
|
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)
|
public async Task<ActionResult<ProjectResponse>> Create(CreateProjectRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var project = await projectService.CreateAsync(request.OrganizationId, request.Name, ct);
|
var project = await projectService.CreateAsync(request.OrganizationId, request.Name, ct);
|
||||||
return CreatedAtAction(nameof(GetById), new { id = project.Id }, ProjectResponse.From(project));
|
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)
|
public async Task<ActionResult<ProjectResponse>> GetById(int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var project = await projectService.FindByIdAsync(id, ct);
|
var project = await projectService.FindByIdAsync(id, ct);
|
||||||
@@ -40,7 +39,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
|||||||
return Ok(ProjectResponse.From(project));
|
return Ok(ProjectResponse.From(project));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("api/v1/project/{id}")]
|
||||||
public async Task<ActionResult<ProjectResponse>> Update(int id, UpdateProjectRequest request, CancellationToken ct)
|
public async Task<ActionResult<ProjectResponse>> Update(int id, UpdateProjectRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var project = await projectService.FindByIdAsync(id, ct);
|
var project = await projectService.FindByIdAsync(id, ct);
|
||||||
@@ -50,7 +49,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
|||||||
return Ok(ProjectResponse.From(updated));
|
return Ok(ProjectResponse.From(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("api/v1/project/{id}")]
|
||||||
public async Task<IActionResult> Delete(int id, CancellationToken ct)
|
public async Task<IActionResult> Delete(int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var project = await projectService.FindByIdAsync(id, ct);
|
var project = await projectService.FindByIdAsync(id, ct);
|
||||||
@@ -60,7 +59,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/user-permissions")]
|
[HttpGet("api/v1/project/{id}/user-permissions")]
|
||||||
public async Task<ActionResult<IReadOnlyList<UserPermissionResponse>>> ListUserPermissions(
|
public async Task<ActionResult<IReadOnlyList<UserPermissionResponse>>> ListUserPermissions(
|
||||||
int id, CancellationToken ct)
|
int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -71,7 +70,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
|||||||
return Ok(perms.Select(UserPermissionResponse.From).ToList());
|
return Ok(perms.Select(UserPermissionResponse.From).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/user-permissions")]
|
[HttpPost("api/v1/project/{id}/user-permissions")]
|
||||||
public async Task<ActionResult<UserPermissionResponse>> CreateUserPermissions(
|
public async Task<ActionResult<UserPermissionResponse>> CreateUserPermissions(
|
||||||
int id, SetUserPermissionsRequest request, CancellationToken ct)
|
int id, SetUserPermissionsRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -86,7 +85,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
|||||||
return Ok(UserPermissionResponse.From(perm));
|
return Ok(UserPermissionResponse.From(perm));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}/user-permissions/{userId}")]
|
[HttpPut("api/v1/project/{id}/user-permission/{userId}")]
|
||||||
public async Task<ActionResult<UserPermissionResponse>> UpdateUserPermissions(
|
public async Task<ActionResult<UserPermissionResponse>> UpdateUserPermissions(
|
||||||
int id, int userId, SetUserPermissionsRequest request, CancellationToken ct)
|
int id, int userId, SetUserPermissionsRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -101,7 +100,7 @@ public class ProjectsController(ProjectService projectService) : ControllerBase
|
|||||||
return Ok(UserPermissionResponse.From(perm));
|
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)
|
public async Task<IActionResult> DeleteUserPermissions(int id, int userId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var project = await projectService.FindByIdAsync(id, ct);
|
var project = await projectService.FindByIdAsync(id, ct);
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ using Microsoft.AspNetCore.RateLimiting;
|
|||||||
namespace MicCheck.Api.Segments;
|
namespace MicCheck.Api.Segments;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/projects/{projectId}/segments")]
|
|
||||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||||
[EnableRateLimiting("AdminApi")]
|
[EnableRateLimiting("AdminApi")]
|
||||||
public class SegmentsController(SegmentService segmentService) : ControllerBase
|
public class SegmentsController(SegmentService segmentService) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet("api/v1/project/{projectId}/segments")]
|
||||||
public async Task<ActionResult<PaginatedResponse<SegmentResponse>>> List(
|
public async Task<ActionResult<PaginatedResponse<SegmentResponse>>> List(
|
||||||
int projectId,
|
int projectId,
|
||||||
[FromQuery] int page = 1,
|
[FromQuery] int page = 1,
|
||||||
@@ -25,7 +24,7 @@ public class SegmentsController(SegmentService segmentService) : ControllerBase
|
|||||||
return Ok(new PaginatedResponse<SegmentResponse>(all.Count, null, null, paged));
|
return Ok(new PaginatedResponse<SegmentResponse>(all.Count, null, null, paged));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost("api/v1/project/{projectId}/segments")]
|
||||||
public async Task<ActionResult<SegmentResponse>> Create(
|
public async Task<ActionResult<SegmentResponse>> Create(
|
||||||
int projectId, CreateSegmentRequest request, CancellationToken ct)
|
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)
|
public async Task<ActionResult<SegmentResponse>> GetById(int projectId, int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var segment = await segmentService.FindByIdAsync(id, ct);
|
var segment = await segmentService.FindByIdAsync(id, ct);
|
||||||
@@ -49,7 +48,7 @@ public class SegmentsController(SegmentService segmentService) : ControllerBase
|
|||||||
return Ok(SegmentResponse.From(segment));
|
return Ok(SegmentResponse.From(segment));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("api/v1/project/{projectId}/segment/{id}")]
|
||||||
public async Task<ActionResult<SegmentResponse>> Update(
|
public async Task<ActionResult<SegmentResponse>> Update(
|
||||||
int projectId, int id, CreateSegmentRequest request, CancellationToken ct)
|
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)
|
public async Task<IActionResult> Delete(int projectId, int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var segment = await segmentService.FindByIdAsync(id, ct);
|
var segment = await segmentService.FindByIdAsync(id, ct);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.RateLimiting;
|
|||||||
namespace MicCheck.Api.Users;
|
namespace MicCheck.Api.Users;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/users")]
|
[Route("api/v1/user")]
|
||||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||||
[EnableRateLimiting("AdminApi")]
|
[EnableRateLimiting("AdminApi")]
|
||||||
public class UsersController(UserService userService) : ControllerBase
|
public class UsersController(UserService userService) : ControllerBase
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ public class AdminApiIntegrationTests
|
|||||||
db.Projects.Add(project);
|
db.Projects.Add(project);
|
||||||
db.SaveChanges();
|
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",
|
name = "dark_mode",
|
||||||
type = "Standard",
|
type = "Standard",
|
||||||
@@ -135,7 +135,7 @@ public class AdminApiIntegrationTests
|
|||||||
});
|
});
|
||||||
db.SaveChanges();
|
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",
|
name = "flag_x",
|
||||||
type = "Standard",
|
type = "Standard",
|
||||||
@@ -220,7 +220,7 @@ public class AdminApiIntegrationTests
|
|||||||
db.Projects.Add(project);
|
db.Projects.Add(project);
|
||||||
db.SaveChanges();
|
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!",
|
name = "invalid name with spaces!",
|
||||||
type = "Standard"
|
type = "Standard"
|
||||||
@@ -247,7 +247,7 @@ public class AdminApiIntegrationTests
|
|||||||
db.Projects.Add(project);
|
db.Projects.Add(project);
|
||||||
db.SaveChanges();
|
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",
|
name = "Premium Users",
|
||||||
rules = new[]
|
rules = new[]
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
{
|
{
|
||||||
var client = _factory.CreateClient();
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
client.DefaultRequestHeaders.Authorization =
|
client.DefaultRequestHeaders.Authorization =
|
||||||
new AuthenticationHeaderValue("Bearer", "not-a-jwt");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
var client = _factory.CreateClient();
|
var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", "Api-Key not-a-real-key");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
var client = _factory.CreateClient();
|
var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
var client = _factory.CreateClient();
|
var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
var client = _factory.CreateClient();
|
var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
{
|
{
|
||||||
var client = _factory.CreateClient();
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
client.DefaultRequestHeaders.Authorization =
|
client.DefaultRequestHeaders.Authorization =
|
||||||
new AuthenticationHeaderValue("Bearer", "not-a-jwt");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
var client = _factory.CreateClient();
|
var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", "Api-Key not-a-real-key");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
var client = _factory.CreateClient();
|
var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
var client = _factory.CreateClient();
|
var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ public class ApiKeyAuthenticationHandlerTests
|
|||||||
var client = _factory.CreateClient();
|
var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
|
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));
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,4 +137,63 @@ public class FeatureServiceTests
|
|||||||
|
|
||||||
Assert.That(features, Has.Count.EqualTo(2));
|
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();
|
var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("X-Environment-Key", _envApiKey);
|
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",
|
identifier = "user-123",
|
||||||
traits = new[] { new { trait_key = "plan", trait_value = "premium" } }
|
traits = new[] { new { trait_key = "plan", trait_value = "premium" } }
|
||||||
|
|||||||
Reference in New Issue
Block a user