diff --git a/application/src/main/java/run/halo/app/notification/DefaultNotificationService.java b/application/src/main/java/run/halo/app/notification/DefaultNotificationService.java index a5171b90e3..1f3d579436 100644 --- a/application/src/main/java/run/halo/app/notification/DefaultNotificationService.java +++ b/application/src/main/java/run/halo/app/notification/DefaultNotificationService.java @@ -10,6 +10,7 @@ import run.halo.app.core.extension.notification.Notification; import run.halo.app.extension.ListResult; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.exception.AccessDeniedException; /** * A default implementation of {@link UserNotificationService}. @@ -49,6 +50,19 @@ public Flux markSpecifiedAsRead(String username, List names) { .map(notification -> notification.getMetadata().getName()); } + @Override + public Mono deleteByName(String username, String name) { + return client.get(Notification.class, name) + .doOnNext(notification -> { + var recipient = notification.getSpec().getRecipient(); + if (!username.equals(recipient)) { + throw new AccessDeniedException( + "You have no permission to delete this notification."); + } + }) + .flatMap(client::delete); + } + static boolean isRecipient(Notification notification, String username) { Assert.notNull(notification, "Notification must not be null"); Assert.notNull(username, "Username must not be null"); diff --git a/application/src/main/java/run/halo/app/notification/UserNotificationService.java b/application/src/main/java/run/halo/app/notification/UserNotificationService.java index 34bf5b5fcb..4ff448e602 100644 --- a/application/src/main/java/run/halo/app/notification/UserNotificationService.java +++ b/application/src/main/java/run/halo/app/notification/UserNotificationService.java @@ -37,4 +37,6 @@ public interface UserNotificationService { * @return the names of read notification that has been marked as read */ Flux markSpecifiedAsRead(String username, List names); + + Mono deleteByName(String username, String name); } diff --git a/application/src/main/java/run/halo/app/notification/endpoint/UserNotificationEndpoint.java b/application/src/main/java/run/halo/app/notification/endpoint/UserNotificationEndpoint.java index 6980d66e4e..53077f701f 100644 --- a/application/src/main/java/run/halo/app/notification/endpoint/UserNotificationEndpoint.java +++ b/application/src/main/java/run/halo/app/notification/endpoint/UserNotificationEndpoint.java @@ -106,9 +106,34 @@ Supplier> userspaceScopedApis() { ) .response(responseBuilder().implementationArray(String.class)) ) + .DELETE("/notifications/{name}", this::deleteNotification, + builder -> builder.operationId("DeleteSpecifiedNotification") + .description("Delete the specified notification.") + .tag(tag) + .parameter(parameterBuilder() + .in(ParameterIn.PATH) + .name("username") + .description("Username") + .required(true) + ) + .parameter(parameterBuilder() + .in(ParameterIn.PATH) + .name("name") + .description("Notification name") + .required(true) + ) + .response(responseBuilder().implementation(Notification.class)) + ) .build(); } + private Mono deleteNotification(ServerRequest request) { + var name = request.pathVariable("name"); + var username = request.pathVariable("username"); + return notificationService.deleteByName(username, name) + .flatMap(notification -> ServerResponse.ok().bodyValue(notification)); + } + @Override public GroupVersion groupVersion() { return GroupVersion.parseAPIVersion("api.notification.halo.run/v1alpha1"); diff --git a/application/src/main/resources/extensions/role-template-authenticated.yaml b/application/src/main/resources/extensions/role-template-authenticated.yaml index dff7d74dcc..de82a1d8a0 100644 --- a/application/src/main/resources/extensions/role-template-authenticated.yaml +++ b/application/src/main/resources/extensions/role-template-authenticated.yaml @@ -122,7 +122,7 @@ metadata: rules: - apiGroups: [ "api.notification.halo.run" ] resources: [ "notifications" ] - verbs: [ "get", "list" ] + verbs: [ "get", "list", "delete" ] - apiGroups: [ "api.notification.halo.run" ] resources: [ "notifications/mark-as-read", "notifications/mark-specified-as-read" ] verbs: [ "update" ] diff --git a/console/packages/api-client/src/api/api-notification-halo-run-v1alpha1-notification-api.ts b/console/packages/api-client/src/api/api-notification-halo-run-v1alpha1-notification-api.ts index df4dd56664..ad7fb8ad4d 100644 --- a/console/packages/api-client/src/api/api-notification-halo-run-v1alpha1-notification-api.ts +++ b/console/packages/api-client/src/api/api-notification-halo-run-v1alpha1-notification-api.ts @@ -54,6 +54,63 @@ import { ReasonTypeNotifierMatrix } from "../models"; export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * Delete the specified notification. + * @param {string} username Username + * @param {string} name Notification name + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteSpecifiedNotification: async ( + username: string, + name: string, + options: AxiosRequestConfig = {} + ): Promise => { + // verify required parameter 'username' is not null or undefined + assertParamExists("deleteSpecifiedNotification", "username", username); + // verify required parameter 'name' is not null or undefined + assertParamExists("deleteSpecifiedNotification", "name", name); + const localVarPath = + `/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/{name}` + .replace(`{${"username"}}`, encodeURIComponent(String(username))) + .replace(`{${"name"}}`, encodeURIComponent(String(name))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: "DELETE", + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication BasicAuth required + // http basic authentication required + setBasicAuthToObject(localVarRequestOptions, configuration); + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration); + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * List notification preferences for the authenticated user. * @param {string} username Username @@ -422,6 +479,33 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFp = function ( configuration ); return { + /** + * Delete the specified notification. + * @param {string} username Username + * @param {string} name Notification name + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteSpecifiedNotification( + username: string, + name: string, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.deleteSpecifiedNotification( + username, + name, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, /** * List notification preferences for the authenticated user. * @param {string} username Username @@ -599,6 +683,24 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFactory = function ( const localVarFp = ApiNotificationHaloRunV1alpha1NotificationApiFp(configuration); return { + /** + * Delete the specified notification. + * @param {ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteSpecifiedNotification( + requestParameters: ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest, + options?: AxiosRequestConfig + ): AxiosPromise { + return localVarFp + .deleteSpecifiedNotification( + requestParameters.username, + requestParameters.name, + options + ) + .then((request) => request(axios, basePath)); + }, /** * List notification preferences for the authenticated user. * @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest} requestParameters Request parameters. @@ -695,6 +797,27 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFactory = function ( }; }; +/** + * Request parameters for deleteSpecifiedNotification operation in ApiNotificationHaloRunV1alpha1NotificationApi. + * @export + * @interface ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest + */ +export interface ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest { + /** + * Username + * @type {string} + * @memberof ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotification + */ + readonly username: string; + + /** + * Notification name + * @type {string} + * @memberof ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotification + */ + readonly name: string; +} + /** * Request parameters for listUserNotificationPreferences operation in ApiNotificationHaloRunV1alpha1NotificationApi. * @export @@ -849,6 +972,26 @@ export interface ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificati * @extends {BaseAPI} */ export class ApiNotificationHaloRunV1alpha1NotificationApi extends BaseAPI { + /** + * Delete the specified notification. + * @param {ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiNotificationHaloRunV1alpha1NotificationApi + */ + public deleteSpecifiedNotification( + requestParameters: ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest, + options?: AxiosRequestConfig + ) { + return ApiNotificationHaloRunV1alpha1NotificationApiFp(this.configuration) + .deleteSpecifiedNotification( + requestParameters.username, + requestParameters.name, + options + ) + .then((request) => request(this.axios, this.basePath)); + } + /** * List notification preferences for the authenticated user. * @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest} requestParameters Request parameters. diff --git a/console/uc-src/modules/notifications/Notifications.vue b/console/uc-src/modules/notifications/Notifications.vue index 33374915f2..4da43d4415 100644 --- a/console/uc-src/modules/notifications/Notifications.vue +++ b/console/uc-src/modules/notifications/Notifications.vue @@ -37,6 +37,13 @@ const { return data; }, cacheTime: 0, + refetchInterval(data) { + const hasDeletingNotifications = data?.items.some( + (item) => item.metadata.deletionTimestamp !== undefined + ); + + return hasDeletingNotifications ? 1000 : false; + }, }); const selectedNotificationName = useRouteQuery("name"); diff --git a/console/uc-src/modules/notifications/components/NotificationListItem.vue b/console/uc-src/modules/notifications/components/NotificationListItem.vue index ff7a633d3a..41d894c886 100644 --- a/console/uc-src/modules/notifications/components/NotificationListItem.vue +++ b/console/uc-src/modules/notifications/components/NotificationListItem.vue @@ -4,6 +4,7 @@ import { apiClient } from "@/utils/api-client"; import { relativeTimeTo } from "@/utils/date"; import type { Notification } from "@halo-dev/api-client"; import { useMutation, useQueryClient } from "@tanstack/vue-query"; +import { Dialog, Toast, VStatusDot } from "@halo-dev/components"; import { watch } from "vue"; import { ref } from "vue"; @@ -40,6 +41,23 @@ const { mutate: handleMarkAsRead } = useMutation({ }, }); +function handleDelete() { + Dialog.warning({ + title: "删除消息", + description: "确定要删除该消息吗?", + async onConfirm() { + await apiClient.notification.deleteSpecifiedNotification({ + name: props.notification.metadata.name, + username: currentUser?.metadata.name as string, + }); + + await queryClient.invalidateQueries({ queryKey: ["user-notifications"] }); + + Toast.success("删除成功"); + }, + }); +} + watch( () => props.isSelected, (value) => { @@ -61,11 +79,19 @@ watch( v-if="isSelected" class="absolute inset-y-0 left-0 w-0.5 bg-primary" > -
- {{ notification.spec?.title }} +
+
+ {{ notification.spec?.title }} +
+
{{ relativeTimeTo(notification.metadata.creationTimestamp) }}
-
diff --git a/e2e/testsuite.yaml b/e2e/testsuite.yaml index 1776b5e32f..fac4fd4ed2 100644 --- a/e2e/testsuite.yaml +++ b/e2e/testsuite.yaml @@ -3,6 +3,9 @@ api: | {{default "http://halo:8090" (env "SERVER")}}/apis param: postName: "{{randAlpha 6}}" + userName: "{{randAlpha 6}}" + notificationName: "{{randAlpha 6}}" + auth: "Basic YWRtaW46MTIzNDU2" items: - name: init request: @@ -84,3 +87,48 @@ items: method: DELETE header: Authorization: "Basic YWRtaW46MTIzNDU2" + + # Notifications +- name: createNotification + request: + api: /notification.halo.run/v1alpha1/notifications + method: POST + body: | + { + "spec": { + "recipient": "admin", + "reason": "fake-reason", + "title": "test 评论了你的页面《关于我》", + "rawContent": "Fake raw content", + "htmlContent": "

Fake html content

", + "unread": true + }, + "apiVersion": "notification.halo.run/v1alpha1", + "kind": "Notification", + "metadata": { + "name": "{{.param.notificationName}}" + } + } + header: + Content-Type: application/json + Authorization: "{{.param.auth}}" + expect: + statusCode: 201 +- name: getNotificationByName + request: + api: /notification.halo.run/v1alpha1/notifications/{{.param.notificationName}} + method: GET + header: + Authorization: "{{.param.auth}}" + expect: + statusCode: 200 + verify: + - data.spec.reason == "fake-reason" + - data.spec.title == "test 评论了你的页面《关于我》" +- name: deleteUserNotification + request: + api: | + /api.notification.halo.run/v1alpha1/userspaces/admin/notifications/{{.param.notificationName}} + method: DELETE + header: + Authorization: "{{.param.auth}}"