flows: migrate FlowExecutor error handler to native challenge instead of shell

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-09-06 18:48:15 +02:00
parent 2a4679e390
commit 60266b3345
16 changed files with 182 additions and 76 deletions

View file

@ -1,14 +1,14 @@
"""Challenge helpers"""
from dataclasses import asdict, is_dataclass
from enum import Enum
from traceback import format_tb
from typing import TYPE_CHECKING, Optional, TypedDict
from uuid import UUID
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.http import JsonResponse
from rest_framework.fields import ChoiceField, DictField
from rest_framework.serializers import CharField
from rest_framework.fields import CharField, ChoiceField, DictField
from authentik.core.api.utils import PassiveSerializer
@ -90,6 +90,34 @@ class WithUserInfoChallenge(Challenge):
pending_user_avatar = CharField()
class FlowErrorChallenge(WithUserInfoChallenge):
"""Challenge class when an unhandled error occurs during a stage. Normal users
are shown an error message, superusers are shown a full stacktrace."""
component = CharField(default="xak-flow-error")
request_id = CharField()
error = CharField(required=False)
traceback = CharField(required=False)
def __init__(self, *args, **kwargs):
request = kwargs.pop("request", None)
error = kwargs.pop("error", None)
super().__init__(*args, **kwargs)
if not request or not error:
return
self.request_id = request.request_id
from authentik.core.models import USER_ATTRIBUTE_DEBUG
if request.user and request.user.is_authenticated:
if request.user.is_superuser or request.user.group_attributes(request).get(
USER_ATTRIBUTE_DEBUG, False
):
self.error = error
self.traceback = "".join(format_tb(self.error.__traceback__))
class AccessDeniedChallenge(WithUserInfoChallenge):
"""Challenge when a flow's active stage calls `stage_invalid()`."""

View file

@ -1,22 +0,0 @@
{% load i18n %}
<style>
.ak-exception {
font-family: monospace;
overflow-x: scroll;
}
</style>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% trans 'Whoops!' %}
</h1>
</header>
<div class="pf-c-login__main-body">
<h3>
{% trans 'Something went wrong! Please try again later.' %}
</h3>
{% if debug %}
<pre class="ak-exception">{{ tb }}{{ error }}</pre>
{% endif %}
</div>

View file

@ -1,7 +1,6 @@
"""authentik multi-stage authentication engine"""
from copy import deepcopy
from traceback import format_tb
from typing import Any, Optional
from typing import Optional
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
@ -23,12 +22,12 @@ from sentry_sdk.api import set_tag
from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
ChallengeTypes,
FlowErrorChallenge,
HttpChallengeResponse,
RedirectChallenge,
ShellChallenge,
@ -253,7 +252,9 @@ class FlowExecutorView(APIView):
action=EventAction.SYSTEM_EXCEPTION,
message=exception_to_string(exc),
).from_http(self.request)
return to_stage_response(self.request, FlowErrorResponse(self.request, exc))
return to_stage_response(
self.request, HttpChallengeResponse(FlowErrorChallenge(self.request, exc))
)
@extend_schema(
responses={
@ -440,30 +441,6 @@ class FlowExecutorView(APIView):
del self.request.session[key]
class FlowErrorResponse(TemplateResponse):
"""Response class when an unhandled error occurs during a stage. Normal users
are shown an error message, superusers are shown a full stacktrace."""
error: Exception
def __init__(self, request: HttpRequest, error: Exception) -> None:
# For some reason pyright complains about keyword argument usage here
# pyright: reportGeneralTypeIssues=false
super().__init__(request=request, template="flows/error.html")
self.error = error
def resolve_context(self, context: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
if not context:
context = {}
context["error"] = self.error
if self._request.user and self._request.user.is_authenticated:
if self._request.user.is_superuser or self._request.user.group_attributes(
self._request
).get(USER_ATTRIBUTE_DEBUG, False):
context["tb"] = "".join(format_tb(self.error.__traceback__))
return context
class CancelView(View):
"""View which canels the currently active plan"""

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-08-27 18:46+0000\n"
"POT-Creation-Date: 2022-09-06 16:46+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -31,19 +31,19 @@ msgstr ""
msgid "Validation Error"
msgstr ""
#: authentik/blueprints/models.py:29
#: authentik/blueprints/models.py:42
msgid "Managed by authentik"
msgstr ""
#: authentik/blueprints/models.py:98
#: authentik/blueprints/models.py:156
msgid "Blueprint Instance"
msgstr ""
#: authentik/blueprints/models.py:99
#: authentik/blueprints/models.py:157
msgid "Blueprint Instances"
msgstr ""
#: authentik/blueprints/v1/exporter.py:53
#: authentik/blueprints/v1/exporter.py:62
#, python-format
msgid "authentik Export - %(date)s"
msgstr ""
@ -184,20 +184,17 @@ msgstr ""
msgid "Successfully linked %(source)s!"
msgstr ""
#: authentik/core/templates/error/generic.html:27
msgid "Go to home"
msgstr ""
#: authentik/core/templates/if/admin.html:24
#: authentik/core/templates/if/admin.html:30
#: authentik/core/templates/if/flow.html:38
#: authentik/core/templates/if/flow.html:44
#: authentik/core/templates/if/user.html:24
#: authentik/core/templates/if/user.html:30
#: authentik/core/templates/if/admin.html:26
#: authentik/core/templates/if/admin.html:32
#: authentik/core/templates/if/flow.html:40
#: authentik/core/templates/if/flow.html:46
#: authentik/core/templates/if/user.html:26
#: authentik/core/templates/if/user.html:32
msgid "Loading..."
msgstr ""
#: authentik/core/templates/if/end_session.html:7
#: authentik/core/templates/if/error.html:7
msgid "End session"
msgstr ""
@ -234,6 +231,10 @@ msgid ""
" "
msgstr ""
#: authentik/core/templates/if/error.html:18
msgid "Go home"
msgstr ""
#: authentik/core/templates/login/base_full.html:89
msgid "Powered by authentik"
msgstr ""
@ -367,7 +368,7 @@ msgstr ""
msgid "Stage (%(type)s)"
msgstr ""
#: authentik/flows/api/flows.py:375
#: authentik/flows/api/flows.py:380
#, python-format
msgid "Flow not applicable to current user/request: %(messages)s"
msgstr ""
@ -434,14 +435,6 @@ msgstr ""
msgid "Flow Tokens"
msgstr ""
#: authentik/flows/templates/flows/error.html:12
msgid "Whoops!"
msgstr ""
#: authentik/flows/templates/flows/error.html:17
msgid "Something went wrong! Please try again later."
msgstr ""
#: authentik/lib/utils/time.py:27
#, python-format
msgid "%(value)s is not in the correct format of 'hours=3;minutes=1'."

View file

@ -21253,6 +21253,7 @@ components:
- $ref: '#/components/schemas/ConsentChallenge'
- $ref: '#/components/schemas/DummyChallenge'
- $ref: '#/components/schemas/EmailChallenge'
- $ref: '#/components/schemas/FlowErrorChallenge'
- $ref: '#/components/schemas/IdentificationChallenge'
- $ref: '#/components/schemas/PasswordChallenge'
- $ref: '#/components/schemas/PlexAuthenticationChallenge'
@ -21275,6 +21276,7 @@ components:
ak-stage-consent: '#/components/schemas/ConsentChallenge'
ak-stage-dummy: '#/components/schemas/DummyChallenge'
ak-stage-email: '#/components/schemas/EmailChallenge'
xak-flow-error: '#/components/schemas/FlowErrorChallenge'
ak-stage-identification: '#/components/schemas/IdentificationChallenge'
ak-stage-password: '#/components/schemas/PasswordChallenge'
ak-flow-sources-plex: '#/components/schemas/PlexAuthenticationChallenge'
@ -22506,6 +22508,40 @@ components:
readOnly: true
required:
- diagram
FlowErrorChallenge:
type: object
description: |-
Challenge class when an unhandled error occurs during a stage. Normal users
are shown an error message, superusers are shown a full stacktrace.
properties:
type:
$ref: '#/components/schemas/ChallengeChoices'
flow_info:
$ref: '#/components/schemas/ContextualFlowInfo'
component:
type: string
default: xak-flow-error
response_errors:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ErrorDetail'
pending_user:
type: string
pending_user_avatar:
type: string
request_id:
type: string
error:
type: string
traceback:
type: string
required:
- pending_user
- pending_user_avatar
- request_id
- type
FlowInspection:
type: object
description: Serializer for inspect endpoint

View file

@ -0,0 +1,64 @@
import "@goauthentik/web/elements/EmptyState";
import "@goauthentik/web/flows/FormStatic";
import { BaseStage } from "@goauthentik/web/flows/stages/base";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import AKGlobal from "@goauthentik/web/authentik.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { FlowChallengeResponseRequest, FlowErrorChallenge } from "@goauthentik/api";
@customElement("ak-stage-flow-error")
export class FlowErrorStage extends BaseStage<FlowErrorChallenge, FlowChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<div class="pf-c-form__group">
<p>
${this.challenge?.error
? this.challenge.error
: t`Something went wrong! Please try again later.`}
</p>
${this.challenge?.traceback
? html`<pre class="ak-exception">
${this.challenge.error}${this.challenge.traceback}</pre
>`
: html``}
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View file

@ -3062,6 +3062,7 @@ msgstr "Server laden"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3700,6 +3701,7 @@ msgstr "Noch nicht synchronisiert."
msgid "Not used by any other object."
msgstr "Von keinem anderen Objekt verwendet."
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5069,6 +5071,7 @@ msgid "Slug"
msgstr "Slug"
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr "Etwas ist schiefgelaufen. Bitte probiere es später wieder"

View file

@ -3114,6 +3114,7 @@ msgstr "Load servers"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3755,6 +3756,7 @@ msgstr "Not synced yet."
msgid "Not used by any other object."
msgstr "Not used by any other object."
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5158,6 +5160,7 @@ msgid "Slug"
msgstr "Slug"
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr "Something went wrong! Please try again later."

View file

@ -3055,6 +3055,7 @@ msgstr "Servidores de carga"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3693,6 +3694,7 @@ msgstr "Aún no se ha sincronizado."
msgid "Not used by any other object."
msgstr "No lo usa ningún otro objeto."
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5062,6 +5064,7 @@ msgid "Slug"
msgstr "babosa"
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr "¡Algo salió mal! Inténtelo de nuevo más tarde."

View file

@ -3086,6 +3086,7 @@ msgstr "Charger les serveurs"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3726,6 +3727,7 @@ msgstr "Pas encore synchronisé."
msgid "Not used by any other object."
msgstr "Pas utilisé par un autre objet."
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5112,6 +5114,7 @@ msgid "Slug"
msgstr "Slug"
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr "Une erreur s'est produite ! Veuillez réessayer plus tard."

View file

@ -3052,6 +3052,7 @@ msgstr "Załaduj serwery"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3690,6 +3691,7 @@ msgstr "Jeszcze nie zsynchronizowano."
msgid "Not used by any other object."
msgstr "Nie używany przez żaden inny obiekt."
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5059,6 +5061,7 @@ msgid "Slug"
msgstr "Ślimak"
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr "Coś poszło nie tak! Spróbuj ponownie później."

View file

@ -3096,6 +3096,7 @@ msgstr ""
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3737,6 +3738,7 @@ msgstr ""
msgid "Not used by any other object."
msgstr ""
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5138,6 +5140,7 @@ msgid "Slug"
msgstr ""
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr ""

View file

@ -3056,6 +3056,7 @@ msgstr "Sunucuları yükle"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3694,6 +3695,7 @@ msgstr "Henüz senkronize edilmedi."
msgid "Not used by any other object."
msgstr "Başka bir nesne tarafından kullanılmaz."
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5064,6 +5066,7 @@ msgid "Slug"
msgstr "Kısa İsim"
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr "Bir şeyler ters gitti! Lütfen daha sonra tekrar deneyin."

View file

@ -3039,6 +3039,7 @@ msgstr "加载服务器"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3676,6 +3677,7 @@ msgstr "尚未同步。"
msgid "Not used by any other object."
msgstr "不被任何其他对象使用。"
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5029,6 +5031,7 @@ msgid "Slug"
msgstr "Slug"
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr "发生了某些错误!请稍后重试。"

View file

@ -3043,6 +3043,7 @@ msgstr "加载服务器"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3680,6 +3681,7 @@ msgstr "尚未同步。"
msgid "Not used by any other object."
msgstr "不被任何其他对象使用。"
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5036,6 +5038,7 @@ msgid "Slug"
msgstr "Slug"
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr "发生错误,请稍后重试。"

View file

@ -3043,6 +3043,7 @@ msgstr "加载服务器"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -3680,6 +3681,7 @@ msgstr "尚未同步。"
msgid "Not used by any other object."
msgstr "不被任何其他对象使用。"
#: src/flows/stages/FlowErrorStage.ts
#: src/flows/stages/access_denied/AccessDeniedStage.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_sms/AuthenticatorSMSStage.ts
@ -5036,6 +5038,7 @@ msgid "Slug"
msgstr "Slug"
#: src/flows/FlowExecutor.ts
#: src/flows/stages/FlowErrorStage.ts
#: src/user/user-settings/details/UserSettingsFlowExecutor.ts
msgid "Something went wrong! Please try again later."
msgstr "发生错误,请稍后重试。"