From c39fcebc44e753563715fea1c172e1494987d3ba Mon Sep 17 00:00:00 2001
From: Gavinok <34443260+Gavinok@users.noreply.github.com>
Date: Thu, 29 Aug 2024 14:52:13 -0700
Subject: [PATCH] Merge pull request #1330 from
bcgov/block-api-for-deleted-tenant
Improvements to Tenant Deletion
---
.../traction/templates/acapy/deployment.yaml | 6 +--
plugins/traction_innkeeper/poetry.lock | 40 ++++++++++++++++-
plugins/traction_innkeeper/pyproject.toml | 1 +
.../v1_0/innkeeper/models.py | 10 ++++-
.../v1_0/innkeeper/routes.py | 43 +++++++++++++++++++
.../v1_0/innkeeper/utils.py | 11 +++--
services/aca-py/ngrok-wait.sh | 2 +-
.../components/innkeeper/tenants/Tenants.vue | 1 +
.../deleteTenant/ConfirmTenantDeletion.vue | 15 +++++--
.../tenants/deleteTenant/DeleteTenant.vue | 2 +
.../frontend/src/plugins/i18n/locales/en.json | 6 ++-
11 files changed, 121 insertions(+), 16 deletions(-)
diff --git a/charts/traction/templates/acapy/deployment.yaml b/charts/traction/templates/acapy/deployment.yaml
index a2bc962a7..48c41b8b0 100644
--- a/charts/traction/templates/acapy/deployment.yaml
+++ b/charts/traction/templates/acapy/deployment.yaml
@@ -51,6 +51,9 @@ spec:
--endpoint https://{{ include "acapy.host" . }} \
--arg-file '/home/aries/argfile.yml' \
--plugin 'aries_cloudagent.messaging.jsonld' \
+ {{- if .Values.acapy.plugins.multitenantProvider }}
+ --plugin multitenant_provider.v1_0 \
+ {{- end }}
{{- if .Values.acapy.plugins.tractionInnkeeper }}
--plugin traction_plugins.traction_innkeeper.v1_0 \
--plugin-config-value traction_innkeeper.innkeeper_wallet.tenant_id=\"$(INNKEEPER_WALLET_TENANT_ID)\" \
@@ -62,9 +65,6 @@ spec:
{{- if .Values.acapy.plugins.connectionUpdate }}
--plugin connection_update.v1_0 \
{{- end }}
- {{- if .Values.acapy.plugins.multitenantProvider }}
- --plugin multitenant_provider.v1_0 \
- {{- end }}
{{- if .Values.acapy.plugins.rpc }}
--plugin rpc.v1_0 \
{{- end }}
diff --git a/plugins/traction_innkeeper/poetry.lock b/plugins/traction_innkeeper/poetry.lock
index 0b01c4b6d..abfbfc7cb 100644
--- a/plugins/traction_innkeeper/poetry.lock
+++ b/plugins/traction_innkeeper/poetry.lock
@@ -1188,9 +1188,13 @@ files = [
{file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"},
{file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"},
+ {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"},
+ {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"},
+ {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"},
+ {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"},
{file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"},
{file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"},
@@ -1498,6 +1502,30 @@ files = [
{file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
]
+[[package]]
+name = "multitenant-provider"
+version = "0.1.0"
+description = " (Supported aries-cloudagent version: 0.12.2) "
+optional = false
+python-versions = "^3.9"
+files = []
+develop = false
+
+[package.dependencies]
+bcrypt = "^4.1.3"
+mergedeep = "^1.3.4"
+python-dateutil = "^2.8.2"
+
+[package.extras]
+aca-py = ["aries-cloudagent (>=0.10.3,<1.0.0)"]
+
+[package.source]
+type = "git"
+url = "https://github.com/hyperledger/aries-acapy-plugins"
+reference = "0.12.2"
+resolved_reference = "e9ddf9da53b84949cc94f419a92ee423812db190"
+subdirectory = "multitenant_provider"
+
[[package]]
name = "mypy-extensions"
version = "1.0.0"
@@ -1945,6 +1973,7 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -1952,8 +1981,15 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+ {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -1970,6 +2006,7 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -1977,6 +2014,7 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -2394,4 +2432,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "c079927241e9effcf416cfcaf9cd722b181646a5e1d9011d28a1e342b64f1e34"
+content-hash = "80b930aab481989c533d6293f8461ce762869520df9a80977e37347120ea56da"
diff --git a/plugins/traction_innkeeper/pyproject.toml b/plugins/traction_innkeeper/pyproject.toml
index bf9998c9d..6f3100039 100644
--- a/plugins/traction_innkeeper/pyproject.toml
+++ b/plugins/traction_innkeeper/pyproject.toml
@@ -15,6 +15,7 @@ bcrypt = "^4.2.0"
mergedeep = "^1.3.4"
typing-extensions = "4.8.0"
anoncreds = "^0.2.0"
+multitenant-provider = {git = "https://github.com/hyperledger/aries-acapy-plugins", rev = "0.12.2", subdirectory = "multitenant_provider"}
[tool.poetry.dev-dependencies]
black = "^24.8.0"
diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/models.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/models.py
index f16eb5be6..d414ee6ab 100644
--- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/models.py
+++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/models.py
@@ -4,7 +4,6 @@
from typing import Optional, Union, List
from aries_cloudagent.core.profile import ProfileSession
-from aries_cloudagent.ledger.base import LOGGER
from aries_cloudagent.messaging.models.base_record import BaseRecord, BaseRecordSchema
from aries_cloudagent.messaging.util import datetime_to_str, str_to_datetime
from aries_cloudagent.messaging.valid import UUIDFour
@@ -357,12 +356,19 @@ async def soft_delete(self, session: ProfileSession):
Soft delete the tenant record by setting its state to 'deleted'.
Note: This method should be called on an instance of the TenantRecord.
"""
+ # Delete api records
+ recs = await TenantAuthenticationApiRecord.query_by_tenant_id(
+ session, self.tenant_id
+ )
+ for rec in recs:
+ if rec.tenant_id == self.tenant_id:
+ await rec.delete_record(session)
+
if self.state != self.STATE_DELETED:
self.state = self.STATE_DELETED
self.deleted_at = datetime_to_str(datetime.utcnow())
await self.save(session, reason="Soft delete")
-
async def restore_deleted(self, session: ProfileSession):
"""
Un-soft-delete the tenant record by setting its state to 'active'.
diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py
index 89eac03c2..30281f022 100644
--- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py
+++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py
@@ -25,6 +25,7 @@
from aries_cloudagent.version import __version__
from aries_cloudagent.wallet.error import WalletSettingsError
from aries_cloudagent.wallet.models.wallet_record import WalletRecord
+from multitenant_provider.v1_0.routes import plugin_wallet_create_token
from marshmallow import fields, validate
from . import TenantManager
@@ -522,6 +523,30 @@ async def tenant_create_token(request: web.BaseRequest):
return web.json_response({"token": token})
+@docs(
+ tags=["multitenancy"],
+ summary="Get auth token for a subwallet (innkeeper plugin override)",
+)
+@request_schema(CreateWalletTokenRequestSchema)
+@response_schema(CreateWalletTokenResponseSchema(), 200, description="")
+@error_handler
+async def tenant_wallet_create_token(request: web.BaseRequest):
+ context: AdminRequestContext = request["context"]
+ wallet_id = request.match_info["wallet_id"]
+
+ mgr = context.inject(TenantManager)
+ profile = mgr.profile
+
+ # Tenants must always be fetch by their wallet id.
+ async with profile.session() as session:
+ rec = await TenantRecord.query_by_wallet_id(session, wallet_id)
+ LOGGER.debug("when creating token ", rec)
+ if rec.state == TenantRecord.STATE_DELETED:
+ raise web.HTTPUnauthorized(reason="Tenant is disabled")
+
+ return await plugin_wallet_create_token(request)
+
+
@docs(
tags=[SWAGGER_CATEGORY],
)
@@ -1029,8 +1054,26 @@ async def register(app: web.Application):
"/multitenancy/reservations/{reservation_id}/check-in", tenant_checkin
),
web.post("/multitenancy/tenant/{tenant_id}/token", tenant_create_token),
+ web.post(
+ "/multitenancy/wallet/{wallet_id}/token", tenant_wallet_create_token
+ ),
]
)
+ # Find the endpoint for token creation that already exists and
+ # override it
+ for r in app.router.routes():
+ if r.method == "POST":
+ if (
+ r.resource
+ and r.resource.canonical == "/multitenancy/wallet/{wallet_id}/token"
+ ):
+ LOGGER.info(
+ f"found route: {r.method} {r.resource.canonical} ({r.handler})"
+ )
+ LOGGER.info(f"... replacing current handler: {r.handler}")
+ r._handler = tenant_wallet_create_token
+ LOGGER.info(f"... with new handler: {r.handler}")
+ has_wallet_create_token = True
# routes that require a tenant token for the innkeeper wallet/tenant/agent.
# these require not only a tenant, but it has to be the innkeeper tenant!
app.add_routes(
diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/utils.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/utils.py
index ed6a4741a..065cb010f 100644
--- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/utils.py
+++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/utils.py
@@ -7,7 +7,7 @@
from aries_cloudagent.messaging.models.openapi import OpenAPISchema
from marshmallow import fields
-from .models import ReservationRecord, TenantAuthenticationApiRecord
+from .models import ReservationRecord, TenantAuthenticationApiRecord, TenantRecord
from . import TenantManager
@@ -114,7 +114,9 @@ async def refresh_registration_token(reservation_id: str, manager: TenantManager
)
except Exception as err:
LOGGER.error("Failed to retrieve reservation: %s", err)
- raise ReservationException("Could not retrieve reservation record.") from err
+ raise ReservationException(
+ "Could not retrieve reservation record."
+ ) from err
if reservation.state != ReservationRecord.STATE_APPROVED:
raise ReservationException("Only approved reservations can refresh tokens.")
@@ -140,7 +142,8 @@ async def refresh_registration_token(reservation_id: str, manager: TenantManager
LOGGER.info("Refreshed token for reservation %s", reservation_id)
- return _pwd
+ return _pwd
+
def generate_api_key_data():
_key = str(uuid.uuid4().hex)
@@ -156,6 +159,8 @@ def generate_api_key_data():
async def create_api_key(rec: TenantAuthenticationApiRecord, manager: TenantManager):
+ if rec.state == TenantRecord.STATE_DELETED:
+ raise ValueError("Tenant is disabled")
async with manager.profile.session() as session:
_key, _salt, _hash = generate_api_key_data()
rec.api_key_token_salt = _salt.decode("utf-8")
diff --git a/services/aca-py/ngrok-wait.sh b/services/aca-py/ngrok-wait.sh
index a9db533f7..f55bed2f8 100755
--- a/services/aca-py/ngrok-wait.sh
+++ b/services/aca-py/ngrok-wait.sh
@@ -37,8 +37,8 @@ exec aca-py start \
--wallet-storage-config "{\"url\":\"${POSTGRESQL_HOST}:5432\",\"max_connections\":5, \"wallet_scheme\":\"${TRACTION_ACAPY_WALLET_SCHEME}\"}" \
--wallet-storage-creds "{\"account\":\"${POSTGRESQL_USER}\",\"password\":\"${POSTGRESQL_PASSWORD}\",\"admin_account\":\"${POSTGRESQL_USER}\",\"admin_password\":\"${POSTGRESQL_PASSWORD}\"}" \
--admin "0.0.0.0" ${TRACTION_ACAPY_ADMIN_PORT} \
+ --plugin multitenant_provider.v1_0 \
--plugin traction_plugins.traction_innkeeper.v1_0 \
--plugin basicmessage_storage.v1_0 \
--plugin connection_update.v1_0 \
- --plugin multitenant_provider.v1_0 \
--plugin rpc.v1_0 \
diff --git a/services/tenant-ui/frontend/src/components/innkeeper/tenants/Tenants.vue b/services/tenant-ui/frontend/src/components/innkeeper/tenants/Tenants.vue
index 8929890f6..89691cc86 100644
--- a/services/tenant-ui/frontend/src/components/innkeeper/tenants/Tenants.vue
+++ b/services/tenant-ui/frontend/src/components/innkeeper/tenants/Tenants.vue
@@ -52,6 +52,7 @@
{{ $t('common.deleted') }}