@open-mercato/core 0.4.8-develop-4e71d95aba → 0.4.8-develop-665ca9216b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/auth/api/profile/route.js +67 -6
- package/dist/modules/auth/api/profile/route.js.map +2 -2
- package/dist/modules/auth/backend/auth/profile/page.js +39 -3
- package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
- package/dist/modules/auth/backend/profile/change-password/page.js +39 -3
- package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
- package/package.json +3 -3
- package/src/modules/auth/api/profile/route.ts +69 -6
- package/src/modules/auth/backend/auth/profile/page.tsx +42 -4
- package/src/modules/auth/backend/profile/change-password/page.tsx +42 -4
- package/src/modules/auth/i18n/de.json +6 -0
- package/src/modules/auth/i18n/en.json +6 -0
- package/src/modules/auth/i18n/es.json +6 -0
- package/src/modules/auth/i18n/pl.json +6 -0
|
@@ -13,13 +13,46 @@ const profileResponseSchema = z.object({
|
|
|
13
13
|
roles: z.array(z.string())
|
|
14
14
|
});
|
|
15
15
|
const passwordSchema = buildPasswordSchema();
|
|
16
|
-
const
|
|
16
|
+
const updateSchemaBase = z.object({
|
|
17
17
|
email: z.string().email().optional(),
|
|
18
|
+
currentPassword: z.string().trim().min(1).optional(),
|
|
18
19
|
password: passwordSchema.optional()
|
|
19
|
-
}).refine((data) => Boolean(data.email || data.password), {
|
|
20
|
-
message: "Provide an email or password.",
|
|
21
|
-
path: ["email"]
|
|
22
20
|
});
|
|
21
|
+
function buildUpdateSchema(translate) {
|
|
22
|
+
return updateSchemaBase.superRefine((data, ctx) => {
|
|
23
|
+
if (!data.email && !data.password) {
|
|
24
|
+
ctx.addIssue({
|
|
25
|
+
code: z.ZodIssueCode.custom,
|
|
26
|
+
message: translate(
|
|
27
|
+
"auth.profile.form.errors.emailOrPasswordRequired",
|
|
28
|
+
"Provide an email or password."
|
|
29
|
+
),
|
|
30
|
+
path: ["email"]
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (data.password && !data.currentPassword) {
|
|
34
|
+
ctx.addIssue({
|
|
35
|
+
code: z.ZodIssueCode.custom,
|
|
36
|
+
message: translate(
|
|
37
|
+
"auth.profile.form.errors.currentPasswordRequired",
|
|
38
|
+
"Current password is required."
|
|
39
|
+
),
|
|
40
|
+
path: ["currentPassword"]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (data.currentPassword && !data.password) {
|
|
44
|
+
ctx.addIssue({
|
|
45
|
+
code: z.ZodIssueCode.custom,
|
|
46
|
+
message: translate(
|
|
47
|
+
"auth.profile.form.errors.newPasswordRequired",
|
|
48
|
+
"New password is required."
|
|
49
|
+
),
|
|
50
|
+
path: ["password"]
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const updateSchema = buildUpdateSchema((_key, fallback) => fallback);
|
|
23
56
|
const profileUpdateResponseSchema = z.object({
|
|
24
57
|
ok: z.literal(true),
|
|
25
58
|
email: z.string().email()
|
|
@@ -71,7 +104,7 @@ async function PUT(req) {
|
|
|
71
104
|
}
|
|
72
105
|
try {
|
|
73
106
|
const body = await req.json().catch(() => ({}));
|
|
74
|
-
const parsed =
|
|
107
|
+
const parsed = buildUpdateSchema(translate).safeParse(body);
|
|
75
108
|
if (!parsed.success) {
|
|
76
109
|
return NextResponse.json(
|
|
77
110
|
{
|
|
@@ -82,6 +115,35 @@ async function PUT(req) {
|
|
|
82
115
|
);
|
|
83
116
|
}
|
|
84
117
|
const container = await createRequestContainer();
|
|
118
|
+
const em = container.resolve("em");
|
|
119
|
+
const authService = container.resolve("authService");
|
|
120
|
+
if (parsed.data.password) {
|
|
121
|
+
const user = await findOneWithDecryption(
|
|
122
|
+
em,
|
|
123
|
+
User,
|
|
124
|
+
{ id: auth.sub, deletedAt: null },
|
|
125
|
+
void 0,
|
|
126
|
+
{ tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null }
|
|
127
|
+
);
|
|
128
|
+
if (!user) {
|
|
129
|
+
return NextResponse.json({ error: translate("auth.users.form.errors.notFound", "User not found") }, { status: 404 });
|
|
130
|
+
}
|
|
131
|
+
const currentPassword = parsed.data.currentPassword?.trim() ?? "";
|
|
132
|
+
const isCurrentPasswordValid = await authService.verifyPassword(user, currentPassword);
|
|
133
|
+
if (!isCurrentPasswordValid) {
|
|
134
|
+
const message = translate(
|
|
135
|
+
"auth.profile.form.errors.currentPasswordInvalid",
|
|
136
|
+
"Current password is incorrect."
|
|
137
|
+
);
|
|
138
|
+
return NextResponse.json(
|
|
139
|
+
{
|
|
140
|
+
error: message,
|
|
141
|
+
issues: [{ path: ["currentPassword"], message }]
|
|
142
|
+
},
|
|
143
|
+
{ status: 400 }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
85
147
|
const commandBus = container.resolve("commandBus");
|
|
86
148
|
const ctx = buildCommandContext(container, auth, req);
|
|
87
149
|
const { result } = await commandBus.execute(
|
|
@@ -95,7 +157,6 @@ async function PUT(req) {
|
|
|
95
157
|
ctx
|
|
96
158
|
}
|
|
97
159
|
);
|
|
98
|
-
const authService = container.resolve("authService");
|
|
99
160
|
const roles = await authService.getUserRoles(result, result.tenantId ? String(result.tenantId) : null);
|
|
100
161
|
const jwt = signJwt({
|
|
101
162
|
sub: String(result.id),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/auth/api/profile/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nconst profileResponseSchema = z.object({\n email: z.string().email(),\n roles: z.array(z.string()),\n})\n\nconst passwordSchema = buildPasswordSchema()\n\nconst
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAGlB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,eAAe;AACxB,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAE9B,SAAS,YAAY;AAErB,SAAS,6BAA6B;AACtC,SAAS,2BAA2B;AAEpC,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC;AAC3B,CAAC;AAED,MAAM,iBAAiB,oBAAoB;AAE3C,MAAM,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nconst profileResponseSchema = z.object({\n email: z.string().email(),\n roles: z.array(z.string()),\n})\n\nconst passwordSchema = buildPasswordSchema()\n\nconst updateSchemaBase = z.object({\n email: z.string().email().optional(),\n currentPassword: z.string().trim().min(1).optional(),\n password: passwordSchema.optional(),\n})\n\nfunction buildUpdateSchema(translate: (key: string, fallback: string) => string) {\n return updateSchemaBase.superRefine((data, ctx) => {\n if (!data.email && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.emailOrPasswordRequired',\n 'Provide an email or password.',\n ),\n path: ['email'],\n })\n }\n if (data.password && !data.currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.currentPasswordRequired',\n 'Current password is required.',\n ),\n path: ['currentPassword'],\n })\n }\n if (data.currentPassword && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.newPasswordRequired',\n 'New password is required.',\n ),\n path: ['password'],\n })\n }\n })\n}\n\nconst updateSchema = buildUpdateSchema((_key, fallback) => fallback)\n\nconst profileUpdateResponseSchema = z.object({\n ok: z.literal(true),\n email: z.string().email(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true },\n}\n\nfunction buildCommandContext(container: Awaited<ReturnType<typeof createRequestContainer>>, auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n return NextResponse.json({ email: String(user.email), roles: auth.roles ?? [] })\n } catch (err) {\n console.error('auth.profile.load failed', err)\n return NextResponse.json({ error: translate('auth.profile.form.errors.load', 'Failed to load profile.') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = buildUpdateSchema(translate).safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n {\n error: translate('auth.profile.form.errors.invalid', 'Invalid profile update.'),\n issues: parsed.error.issues,\n },\n { status: 400 },\n )\n }\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const authService = container.resolve('authService') as AuthService\n if (parsed.data.password) {\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n const currentPassword = parsed.data.currentPassword?.trim() ?? ''\n const isCurrentPasswordValid = await authService.verifyPassword(user, currentPassword)\n if (!isCurrentPasswordValid) {\n const message = translate(\n 'auth.profile.form.errors.currentPasswordInvalid',\n 'Current password is incorrect.',\n )\n return NextResponse.json(\n {\n error: message,\n issues: [{ path: ['currentPassword'], message }],\n },\n { status: 400 },\n )\n }\n }\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const ctx = buildCommandContext(container, auth, req)\n const { result } = await commandBus.execute<{ id: string; email?: string; password?: string }, User>(\n 'auth.users.update',\n {\n input: {\n id: auth.sub,\n email: parsed.data.email,\n password: parsed.data.password,\n },\n ctx,\n },\n )\n const roles = await authService.getUserRoles(result, result.tenantId ? String(result.tenantId) : null)\n const jwt = signJwt({\n sub: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n orgId: result.organizationId ? String(result.organizationId) : null,\n email: result.email,\n roles,\n })\n const res = NextResponse.json({ ok: true, email: String(result.email) })\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n return res\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('auth.profile.update failed', err)\n return NextResponse.json({ error: translate('auth.profile.form.errors.save', 'Failed to update profile.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Profile settings',\n methods: {\n GET: {\n summary: 'Get current profile',\n description: 'Returns the email address for the signed-in user.',\n responses: [\n { status: 200, description: 'Profile payload', schema: profileResponseSchema },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'User not found', schema: z.object({ error: z.string() }) },\n ],\n },\n PUT: {\n summary: 'Update current profile',\n description: 'Updates the email address or password for the signed-in user.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Profile updated', schema: profileUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -51,13 +51,22 @@ function AuthProfilePage() {
|
|
|
51
51
|
}, [t]);
|
|
52
52
|
const fields = React.useMemo(() => [
|
|
53
53
|
{ id: "email", label: t("auth.profile.form.email", "Email"), type: "text", required: true },
|
|
54
|
+
{
|
|
55
|
+
id: "currentPassword",
|
|
56
|
+
label: t("auth.profile.form.currentPassword", "Current password"),
|
|
57
|
+
type: "password"
|
|
58
|
+
},
|
|
54
59
|
{
|
|
55
60
|
id: "password",
|
|
56
61
|
label: t("auth.profile.form.password", "New password"),
|
|
57
|
-
type: "
|
|
62
|
+
type: "password",
|
|
58
63
|
description: passwordDescription
|
|
59
64
|
},
|
|
60
|
-
{
|
|
65
|
+
{
|
|
66
|
+
id: "confirmPassword",
|
|
67
|
+
label: t("auth.profile.form.confirmPassword", "Confirm new password"),
|
|
68
|
+
type: "password"
|
|
69
|
+
}
|
|
61
70
|
], [passwordDescription, t]);
|
|
62
71
|
const schema = React.useMemo(() => {
|
|
63
72
|
const passwordSchema = buildPasswordSchema({
|
|
@@ -67,12 +76,36 @@ function AuthProfilePage() {
|
|
|
67
76
|
const optionalPasswordSchema = z.union([z.literal(""), passwordSchema]).optional();
|
|
68
77
|
return z.object({
|
|
69
78
|
email: z.string().trim().min(1, t("auth.profile.form.errors.emailRequired", "Email is required.")),
|
|
79
|
+
currentPassword: z.string().optional(),
|
|
70
80
|
password: optionalPasswordSchema,
|
|
71
81
|
confirmPassword: z.string().optional()
|
|
72
82
|
}).superRefine((values, ctx) => {
|
|
83
|
+
const currentPassword = values.currentPassword?.trim() ?? "";
|
|
73
84
|
const password = values.password?.trim() ?? "";
|
|
74
85
|
const confirmPassword = values.confirmPassword?.trim() ?? "";
|
|
75
|
-
|
|
86
|
+
const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword);
|
|
87
|
+
if (hasPasswordIntent && !currentPassword) {
|
|
88
|
+
ctx.addIssue({
|
|
89
|
+
code: z.ZodIssueCode.custom,
|
|
90
|
+
message: t("auth.profile.form.errors.currentPasswordRequired", "Current password is required."),
|
|
91
|
+
path: ["currentPassword"]
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (hasPasswordIntent && !password) {
|
|
95
|
+
ctx.addIssue({
|
|
96
|
+
code: z.ZodIssueCode.custom,
|
|
97
|
+
message: t("auth.profile.form.errors.newPasswordRequired", "New password is required."),
|
|
98
|
+
path: ["password"]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (hasPasswordIntent && !confirmPassword) {
|
|
102
|
+
ctx.addIssue({
|
|
103
|
+
code: z.ZodIssueCode.custom,
|
|
104
|
+
message: t("auth.profile.form.errors.confirmPasswordRequired", "Please confirm the new password."),
|
|
105
|
+
path: ["confirmPassword"]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (password && confirmPassword && password !== confirmPassword) {
|
|
76
109
|
ctx.addIssue({
|
|
77
110
|
code: z.ZodIssueCode.custom,
|
|
78
111
|
message: t("auth.profile.form.errors.passwordMismatch", "Passwords do not match."),
|
|
@@ -83,12 +116,14 @@ function AuthProfilePage() {
|
|
|
83
116
|
}, [passwordPolicy, t]);
|
|
84
117
|
const handleSubmit = React.useCallback(async (values) => {
|
|
85
118
|
const nextEmail = values.email?.trim() ?? "";
|
|
119
|
+
const currentPassword = values.currentPassword?.trim() ?? "";
|
|
86
120
|
const password = values.password?.trim() ?? "";
|
|
87
121
|
if (!password && nextEmail === email) {
|
|
88
122
|
throw createCrudFormError(t("auth.profile.form.errors.noChanges", "No changes to save."));
|
|
89
123
|
}
|
|
90
124
|
const payload = { email: nextEmail };
|
|
91
125
|
if (password) payload.password = password;
|
|
126
|
+
if (password) payload.currentPassword = currentPassword;
|
|
92
127
|
const result = await readApiResultOrThrow(
|
|
93
128
|
"/api/auth/profile",
|
|
94
129
|
{
|
|
@@ -123,6 +158,7 @@ function AuthProfilePage() {
|
|
|
123
158
|
fields,
|
|
124
159
|
initialValues: {
|
|
125
160
|
email,
|
|
161
|
+
currentPassword: "",
|
|
126
162
|
password: "",
|
|
127
163
|
confirmPassword: ""
|
|
128
164
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/auth/backend/auth/profile/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\ntype ProfileResponse = {\n email?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function AuthProfilePage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error('load_failed')\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n console.error('Failed to load auth profile', err)\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: '
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\ntype ProfileResponse = {\n email?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function AuthProfilePage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error('load_failed')\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n console.error('Failed to load auth profile', err)\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n return (\n <Page>\n <PageBody>\n {loading ? (\n <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n ) : error ? (\n <ErrorMessage label={error} />\n ) : (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.profile.title', 'Profile')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -50,13 +50,22 @@ function ProfileChangePasswordPage() {
|
|
|
50
50
|
}, [t]);
|
|
51
51
|
const fields = React.useMemo(() => [
|
|
52
52
|
{ id: "email", label: t("auth.profile.form.email", "Email"), type: "text", required: true },
|
|
53
|
+
{
|
|
54
|
+
id: "currentPassword",
|
|
55
|
+
label: t("auth.profile.form.currentPassword", "Current password"),
|
|
56
|
+
type: "password"
|
|
57
|
+
},
|
|
53
58
|
{
|
|
54
59
|
id: "password",
|
|
55
60
|
label: t("auth.profile.form.password", "New password"),
|
|
56
|
-
type: "
|
|
61
|
+
type: "password",
|
|
57
62
|
description: passwordDescription
|
|
58
63
|
},
|
|
59
|
-
{
|
|
64
|
+
{
|
|
65
|
+
id: "confirmPassword",
|
|
66
|
+
label: t("auth.profile.form.confirmPassword", "Confirm new password"),
|
|
67
|
+
type: "password"
|
|
68
|
+
}
|
|
60
69
|
], [passwordDescription, t]);
|
|
61
70
|
const schema = React.useMemo(() => {
|
|
62
71
|
const passwordSchema = buildPasswordSchema({
|
|
@@ -66,12 +75,36 @@ function ProfileChangePasswordPage() {
|
|
|
66
75
|
const optionalPasswordSchema = z.union([z.literal(""), passwordSchema]).optional();
|
|
67
76
|
return z.object({
|
|
68
77
|
email: z.string().trim().min(1, t("auth.profile.form.errors.emailRequired", "Email is required.")),
|
|
78
|
+
currentPassword: z.string().optional(),
|
|
69
79
|
password: optionalPasswordSchema,
|
|
70
80
|
confirmPassword: z.string().optional()
|
|
71
81
|
}).superRefine((values, ctx) => {
|
|
82
|
+
const currentPassword = values.currentPassword?.trim() ?? "";
|
|
72
83
|
const password = values.password?.trim() ?? "";
|
|
73
84
|
const confirmPassword = values.confirmPassword?.trim() ?? "";
|
|
74
|
-
|
|
85
|
+
const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword);
|
|
86
|
+
if (hasPasswordIntent && !currentPassword) {
|
|
87
|
+
ctx.addIssue({
|
|
88
|
+
code: z.ZodIssueCode.custom,
|
|
89
|
+
message: t("auth.profile.form.errors.currentPasswordRequired", "Current password is required."),
|
|
90
|
+
path: ["currentPassword"]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (hasPasswordIntent && !password) {
|
|
94
|
+
ctx.addIssue({
|
|
95
|
+
code: z.ZodIssueCode.custom,
|
|
96
|
+
message: t("auth.profile.form.errors.newPasswordRequired", "New password is required."),
|
|
97
|
+
path: ["password"]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (hasPasswordIntent && !confirmPassword) {
|
|
101
|
+
ctx.addIssue({
|
|
102
|
+
code: z.ZodIssueCode.custom,
|
|
103
|
+
message: t("auth.profile.form.errors.confirmPasswordRequired", "Please confirm the new password."),
|
|
104
|
+
path: ["confirmPassword"]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (password && confirmPassword && password !== confirmPassword) {
|
|
75
108
|
ctx.addIssue({
|
|
76
109
|
code: z.ZodIssueCode.custom,
|
|
77
110
|
message: t("auth.profile.form.errors.passwordMismatch", "Passwords do not match."),
|
|
@@ -82,12 +115,14 @@ function ProfileChangePasswordPage() {
|
|
|
82
115
|
}, [passwordPolicy, t]);
|
|
83
116
|
const handleSubmit = React.useCallback(async (values) => {
|
|
84
117
|
const nextEmail = values.email?.trim() ?? "";
|
|
118
|
+
const currentPassword = values.currentPassword?.trim() ?? "";
|
|
85
119
|
const password = values.password?.trim() ?? "";
|
|
86
120
|
if (!password && nextEmail === email) {
|
|
87
121
|
throw createCrudFormError(t("auth.profile.form.errors.noChanges", "No changes to save."));
|
|
88
122
|
}
|
|
89
123
|
const payload = { email: nextEmail };
|
|
90
124
|
if (password) payload.password = password;
|
|
125
|
+
if (password) payload.currentPassword = currentPassword;
|
|
91
126
|
const result = await readApiResultOrThrow(
|
|
92
127
|
"/api/auth/profile",
|
|
93
128
|
{
|
|
@@ -128,6 +163,7 @@ function ProfileChangePasswordPage() {
|
|
|
128
163
|
fields,
|
|
129
164
|
initialValues: {
|
|
130
165
|
email,
|
|
166
|
+
currentPassword: "",
|
|
131
167
|
password: "",
|
|
132
168
|
confirmPassword: ""
|
|
133
169
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/auth/backend/profile/change-password/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\ntype ProfileResponse = {\n email?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function ProfileChangePasswordPage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error('load_failed')\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n console.error('Failed to load auth profile', err)\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: '
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\ntype ProfileResponse = {\n email?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function ProfileChangePasswordPage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error('load_failed')\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n console.error('Failed to load auth profile', err)\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n if (loading) {\n return <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n }\n\n if (error) {\n return <ErrorMessage label={error} />\n }\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6 max-w-2xl\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.changePassword.title', 'Change Password')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.8-develop-
|
|
3
|
+
"version": "0.4.8-develop-665ca9216b",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -217,10 +217,10 @@
|
|
|
217
217
|
"semver": "^7.6.3"
|
|
218
218
|
},
|
|
219
219
|
"peerDependencies": {
|
|
220
|
-
"@open-mercato/shared": "0.4.8-develop-
|
|
220
|
+
"@open-mercato/shared": "0.4.8-develop-665ca9216b"
|
|
221
221
|
},
|
|
222
222
|
"devDependencies": {
|
|
223
|
-
"@open-mercato/shared": "0.4.8-develop-
|
|
223
|
+
"@open-mercato/shared": "0.4.8-develop-665ca9216b",
|
|
224
224
|
"@testing-library/dom": "^10.4.1",
|
|
225
225
|
"@testing-library/jest-dom": "^6.9.1",
|
|
226
226
|
"@testing-library/react": "^16.3.1",
|
|
@@ -20,14 +20,49 @@ const profileResponseSchema = z.object({
|
|
|
20
20
|
|
|
21
21
|
const passwordSchema = buildPasswordSchema()
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const updateSchemaBase = z.object({
|
|
24
24
|
email: z.string().email().optional(),
|
|
25
|
+
currentPassword: z.string().trim().min(1).optional(),
|
|
25
26
|
password: passwordSchema.optional(),
|
|
26
|
-
}).refine((data) => Boolean(data.email || data.password), {
|
|
27
|
-
message: 'Provide an email or password.',
|
|
28
|
-
path: ['email'],
|
|
29
27
|
})
|
|
30
28
|
|
|
29
|
+
function buildUpdateSchema(translate: (key: string, fallback: string) => string) {
|
|
30
|
+
return updateSchemaBase.superRefine((data, ctx) => {
|
|
31
|
+
if (!data.email && !data.password) {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: z.ZodIssueCode.custom,
|
|
34
|
+
message: translate(
|
|
35
|
+
'auth.profile.form.errors.emailOrPasswordRequired',
|
|
36
|
+
'Provide an email or password.',
|
|
37
|
+
),
|
|
38
|
+
path: ['email'],
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
if (data.password && !data.currentPassword) {
|
|
42
|
+
ctx.addIssue({
|
|
43
|
+
code: z.ZodIssueCode.custom,
|
|
44
|
+
message: translate(
|
|
45
|
+
'auth.profile.form.errors.currentPasswordRequired',
|
|
46
|
+
'Current password is required.',
|
|
47
|
+
),
|
|
48
|
+
path: ['currentPassword'],
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
if (data.currentPassword && !data.password) {
|
|
52
|
+
ctx.addIssue({
|
|
53
|
+
code: z.ZodIssueCode.custom,
|
|
54
|
+
message: translate(
|
|
55
|
+
'auth.profile.form.errors.newPasswordRequired',
|
|
56
|
+
'New password is required.',
|
|
57
|
+
),
|
|
58
|
+
path: ['password'],
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const updateSchema = buildUpdateSchema((_key, fallback) => fallback)
|
|
65
|
+
|
|
31
66
|
const profileUpdateResponseSchema = z.object({
|
|
32
67
|
ok: z.literal(true),
|
|
33
68
|
email: z.string().email(),
|
|
@@ -83,7 +118,7 @@ export async function PUT(req: Request) {
|
|
|
83
118
|
}
|
|
84
119
|
try {
|
|
85
120
|
const body = await req.json().catch(() => ({}))
|
|
86
|
-
const parsed =
|
|
121
|
+
const parsed = buildUpdateSchema(translate).safeParse(body)
|
|
87
122
|
if (!parsed.success) {
|
|
88
123
|
return NextResponse.json(
|
|
89
124
|
{
|
|
@@ -94,6 +129,35 @@ export async function PUT(req: Request) {
|
|
|
94
129
|
)
|
|
95
130
|
}
|
|
96
131
|
const container = await createRequestContainer()
|
|
132
|
+
const em = (container.resolve('em') as EntityManager)
|
|
133
|
+
const authService = container.resolve('authService') as AuthService
|
|
134
|
+
if (parsed.data.password) {
|
|
135
|
+
const user = await findOneWithDecryption(
|
|
136
|
+
em,
|
|
137
|
+
User,
|
|
138
|
+
{ id: auth.sub, deletedAt: null },
|
|
139
|
+
undefined,
|
|
140
|
+
{ tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },
|
|
141
|
+
)
|
|
142
|
+
if (!user) {
|
|
143
|
+
return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })
|
|
144
|
+
}
|
|
145
|
+
const currentPassword = parsed.data.currentPassword?.trim() ?? ''
|
|
146
|
+
const isCurrentPasswordValid = await authService.verifyPassword(user, currentPassword)
|
|
147
|
+
if (!isCurrentPasswordValid) {
|
|
148
|
+
const message = translate(
|
|
149
|
+
'auth.profile.form.errors.currentPasswordInvalid',
|
|
150
|
+
'Current password is incorrect.',
|
|
151
|
+
)
|
|
152
|
+
return NextResponse.json(
|
|
153
|
+
{
|
|
154
|
+
error: message,
|
|
155
|
+
issues: [{ path: ['currentPassword'], message }],
|
|
156
|
+
},
|
|
157
|
+
{ status: 400 },
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
97
161
|
const commandBus = (container.resolve('commandBus') as CommandBus)
|
|
98
162
|
const ctx = buildCommandContext(container, auth, req)
|
|
99
163
|
const { result } = await commandBus.execute<{ id: string; email?: string; password?: string }, User>(
|
|
@@ -107,7 +171,6 @@ export async function PUT(req: Request) {
|
|
|
107
171
|
ctx,
|
|
108
172
|
},
|
|
109
173
|
)
|
|
110
|
-
const authService = container.resolve('authService') as AuthService
|
|
111
174
|
const roles = await authService.getUserRoles(result, result.tenantId ? String(result.tenantId) : null)
|
|
112
175
|
const jwt = signJwt({
|
|
113
176
|
sub: String(result.id),
|
|
@@ -24,6 +24,7 @@ type ProfileUpdateResponse = {
|
|
|
24
24
|
|
|
25
25
|
type ProfileFormValues = {
|
|
26
26
|
email: string
|
|
27
|
+
currentPassword?: string
|
|
27
28
|
password?: string
|
|
28
29
|
confirmPassword?: string
|
|
29
30
|
}
|
|
@@ -70,13 +71,22 @@ export default function AuthProfilePage() {
|
|
|
70
71
|
|
|
71
72
|
const fields = React.useMemo<CrudField[]>(() => [
|
|
72
73
|
{ id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },
|
|
74
|
+
{
|
|
75
|
+
id: 'currentPassword',
|
|
76
|
+
label: t('auth.profile.form.currentPassword', 'Current password'),
|
|
77
|
+
type: 'password',
|
|
78
|
+
},
|
|
73
79
|
{
|
|
74
80
|
id: 'password',
|
|
75
81
|
label: t('auth.profile.form.password', 'New password'),
|
|
76
|
-
type: '
|
|
82
|
+
type: 'password',
|
|
77
83
|
description: passwordDescription,
|
|
78
84
|
},
|
|
79
|
-
{
|
|
85
|
+
{
|
|
86
|
+
id: 'confirmPassword',
|
|
87
|
+
label: t('auth.profile.form.confirmPassword', 'Confirm new password'),
|
|
88
|
+
type: 'password',
|
|
89
|
+
},
|
|
80
90
|
], [passwordDescription, t])
|
|
81
91
|
|
|
82
92
|
const schema = React.useMemo(() => {
|
|
@@ -87,12 +97,37 @@ export default function AuthProfilePage() {
|
|
|
87
97
|
const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()
|
|
88
98
|
return z.object({
|
|
89
99
|
email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),
|
|
100
|
+
currentPassword: z.string().optional(),
|
|
90
101
|
password: optionalPasswordSchema,
|
|
91
102
|
confirmPassword: z.string().optional(),
|
|
92
103
|
}).superRefine((values, ctx) => {
|
|
104
|
+
const currentPassword = values.currentPassword?.trim() ?? ''
|
|
93
105
|
const password = values.password?.trim() ?? ''
|
|
94
106
|
const confirmPassword = values.confirmPassword?.trim() ?? ''
|
|
95
|
-
|
|
107
|
+
const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)
|
|
108
|
+
|
|
109
|
+
if (hasPasswordIntent && !currentPassword) {
|
|
110
|
+
ctx.addIssue({
|
|
111
|
+
code: z.ZodIssueCode.custom,
|
|
112
|
+
message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),
|
|
113
|
+
path: ['currentPassword'],
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
if (hasPasswordIntent && !password) {
|
|
117
|
+
ctx.addIssue({
|
|
118
|
+
code: z.ZodIssueCode.custom,
|
|
119
|
+
message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),
|
|
120
|
+
path: ['password'],
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
if (hasPasswordIntent && !confirmPassword) {
|
|
124
|
+
ctx.addIssue({
|
|
125
|
+
code: z.ZodIssueCode.custom,
|
|
126
|
+
message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),
|
|
127
|
+
path: ['confirmPassword'],
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
if (password && confirmPassword && password !== confirmPassword) {
|
|
96
131
|
ctx.addIssue({
|
|
97
132
|
code: z.ZodIssueCode.custom,
|
|
98
133
|
message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),
|
|
@@ -104,14 +139,16 @@ export default function AuthProfilePage() {
|
|
|
104
139
|
|
|
105
140
|
const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {
|
|
106
141
|
const nextEmail = values.email?.trim() ?? ''
|
|
142
|
+
const currentPassword = values.currentPassword?.trim() ?? ''
|
|
107
143
|
const password = values.password?.trim() ?? ''
|
|
108
144
|
|
|
109
145
|
if (!password && nextEmail === email) {
|
|
110
146
|
throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))
|
|
111
147
|
}
|
|
112
148
|
|
|
113
|
-
const payload: { email: string; password?: string } = { email: nextEmail }
|
|
149
|
+
const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }
|
|
114
150
|
if (password) payload.password = password
|
|
151
|
+
if (password) payload.currentPassword = currentPassword
|
|
115
152
|
|
|
116
153
|
const result = await readApiResultOrThrow<ProfileUpdateResponse>(
|
|
117
154
|
'/api/auth/profile',
|
|
@@ -158,6 +195,7 @@ export default function AuthProfilePage() {
|
|
|
158
195
|
fields={fields}
|
|
159
196
|
initialValues={{
|
|
160
197
|
email,
|
|
198
|
+
currentPassword: '',
|
|
161
199
|
password: '',
|
|
162
200
|
confirmPassword: '',
|
|
163
201
|
}}
|
|
@@ -23,6 +23,7 @@ type ProfileUpdateResponse = {
|
|
|
23
23
|
|
|
24
24
|
type ProfileFormValues = {
|
|
25
25
|
email: string
|
|
26
|
+
currentPassword?: string
|
|
26
27
|
password?: string
|
|
27
28
|
confirmPassword?: string
|
|
28
29
|
}
|
|
@@ -69,13 +70,22 @@ export default function ProfileChangePasswordPage() {
|
|
|
69
70
|
|
|
70
71
|
const fields = React.useMemo<CrudField[]>(() => [
|
|
71
72
|
{ id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },
|
|
73
|
+
{
|
|
74
|
+
id: 'currentPassword',
|
|
75
|
+
label: t('auth.profile.form.currentPassword', 'Current password'),
|
|
76
|
+
type: 'password',
|
|
77
|
+
},
|
|
72
78
|
{
|
|
73
79
|
id: 'password',
|
|
74
80
|
label: t('auth.profile.form.password', 'New password'),
|
|
75
|
-
type: '
|
|
81
|
+
type: 'password',
|
|
76
82
|
description: passwordDescription,
|
|
77
83
|
},
|
|
78
|
-
{
|
|
84
|
+
{
|
|
85
|
+
id: 'confirmPassword',
|
|
86
|
+
label: t('auth.profile.form.confirmPassword', 'Confirm new password'),
|
|
87
|
+
type: 'password',
|
|
88
|
+
},
|
|
79
89
|
], [passwordDescription, t])
|
|
80
90
|
|
|
81
91
|
const schema = React.useMemo(() => {
|
|
@@ -86,12 +96,37 @@ export default function ProfileChangePasswordPage() {
|
|
|
86
96
|
const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()
|
|
87
97
|
return z.object({
|
|
88
98
|
email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),
|
|
99
|
+
currentPassword: z.string().optional(),
|
|
89
100
|
password: optionalPasswordSchema,
|
|
90
101
|
confirmPassword: z.string().optional(),
|
|
91
102
|
}).superRefine((values, ctx) => {
|
|
103
|
+
const currentPassword = values.currentPassword?.trim() ?? ''
|
|
92
104
|
const password = values.password?.trim() ?? ''
|
|
93
105
|
const confirmPassword = values.confirmPassword?.trim() ?? ''
|
|
94
|
-
|
|
106
|
+
const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)
|
|
107
|
+
|
|
108
|
+
if (hasPasswordIntent && !currentPassword) {
|
|
109
|
+
ctx.addIssue({
|
|
110
|
+
code: z.ZodIssueCode.custom,
|
|
111
|
+
message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),
|
|
112
|
+
path: ['currentPassword'],
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
if (hasPasswordIntent && !password) {
|
|
116
|
+
ctx.addIssue({
|
|
117
|
+
code: z.ZodIssueCode.custom,
|
|
118
|
+
message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),
|
|
119
|
+
path: ['password'],
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
if (hasPasswordIntent && !confirmPassword) {
|
|
123
|
+
ctx.addIssue({
|
|
124
|
+
code: z.ZodIssueCode.custom,
|
|
125
|
+
message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),
|
|
126
|
+
path: ['confirmPassword'],
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
if (password && confirmPassword && password !== confirmPassword) {
|
|
95
130
|
ctx.addIssue({
|
|
96
131
|
code: z.ZodIssueCode.custom,
|
|
97
132
|
message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),
|
|
@@ -103,14 +138,16 @@ export default function ProfileChangePasswordPage() {
|
|
|
103
138
|
|
|
104
139
|
const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {
|
|
105
140
|
const nextEmail = values.email?.trim() ?? ''
|
|
141
|
+
const currentPassword = values.currentPassword?.trim() ?? ''
|
|
106
142
|
const password = values.password?.trim() ?? ''
|
|
107
143
|
|
|
108
144
|
if (!password && nextEmail === email) {
|
|
109
145
|
throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))
|
|
110
146
|
}
|
|
111
147
|
|
|
112
|
-
const payload: { email: string; password?: string } = { email: nextEmail }
|
|
148
|
+
const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }
|
|
113
149
|
if (password) payload.password = password
|
|
150
|
+
if (password) payload.currentPassword = currentPassword
|
|
114
151
|
|
|
115
152
|
const result = await readApiResultOrThrow<ProfileUpdateResponse>(
|
|
116
153
|
'/api/auth/profile',
|
|
@@ -158,6 +195,7 @@ export default function ProfileChangePasswordPage() {
|
|
|
158
195
|
fields={fields}
|
|
159
196
|
initialValues={{
|
|
160
197
|
email,
|
|
198
|
+
currentPassword: '',
|
|
161
199
|
password: '',
|
|
162
200
|
confirmPassword: '',
|
|
163
201
|
}}
|
|
@@ -65,10 +65,16 @@
|
|
|
65
65
|
"auth.password.requirements.special": "Ein Sonderzeichen",
|
|
66
66
|
"auth.password.requirements.uppercase": "Ein Großbuchstabe",
|
|
67
67
|
"auth.profile.form.confirmPassword": "Neues Passwort bestätigen",
|
|
68
|
+
"auth.profile.form.currentPassword": "Aktuelles Passwort",
|
|
68
69
|
"auth.profile.form.email": "E-Mail",
|
|
70
|
+
"auth.profile.form.errors.confirmPasswordRequired": "Bitte bestätige das neue Passwort.",
|
|
71
|
+
"auth.profile.form.errors.currentPasswordInvalid": "Das aktuelle Passwort ist falsch.",
|
|
72
|
+
"auth.profile.form.errors.currentPasswordRequired": "Das aktuelle Passwort ist erforderlich.",
|
|
73
|
+
"auth.profile.form.errors.emailOrPasswordRequired": "Gib eine E-Mail-Adresse oder ein Passwort an.",
|
|
69
74
|
"auth.profile.form.errors.emailRequired": "E-Mail ist erforderlich.",
|
|
70
75
|
"auth.profile.form.errors.invalid": "Ungültige Profilaktualisierung.",
|
|
71
76
|
"auth.profile.form.errors.load": "Profil konnte nicht geladen werden.",
|
|
77
|
+
"auth.profile.form.errors.newPasswordRequired": "Neues Passwort ist erforderlich.",
|
|
72
78
|
"auth.profile.form.errors.noChanges": "Keine Änderungen zu speichern.",
|
|
73
79
|
"auth.profile.form.errors.passwordMismatch": "Die Passwörter stimmen nicht überein.",
|
|
74
80
|
"auth.profile.form.errors.passwordRequirements": "Das Passwort muss die Anforderungen erfüllen.",
|
|
@@ -65,10 +65,16 @@
|
|
|
65
65
|
"auth.password.requirements.special": "One special character",
|
|
66
66
|
"auth.password.requirements.uppercase": "One uppercase letter",
|
|
67
67
|
"auth.profile.form.confirmPassword": "Confirm new password",
|
|
68
|
+
"auth.profile.form.currentPassword": "Current password",
|
|
68
69
|
"auth.profile.form.email": "Email",
|
|
70
|
+
"auth.profile.form.errors.confirmPasswordRequired": "Please confirm the new password.",
|
|
71
|
+
"auth.profile.form.errors.currentPasswordInvalid": "Current password is incorrect.",
|
|
72
|
+
"auth.profile.form.errors.currentPasswordRequired": "Current password is required.",
|
|
73
|
+
"auth.profile.form.errors.emailOrPasswordRequired": "Provide an email or password.",
|
|
69
74
|
"auth.profile.form.errors.emailRequired": "Email is required.",
|
|
70
75
|
"auth.profile.form.errors.invalid": "Invalid profile update.",
|
|
71
76
|
"auth.profile.form.errors.load": "Failed to load profile.",
|
|
77
|
+
"auth.profile.form.errors.newPasswordRequired": "New password is required.",
|
|
72
78
|
"auth.profile.form.errors.noChanges": "No changes to save.",
|
|
73
79
|
"auth.profile.form.errors.passwordMismatch": "Passwords do not match.",
|
|
74
80
|
"auth.profile.form.errors.passwordRequirements": "Password must meet the requirements.",
|
|
@@ -65,10 +65,16 @@
|
|
|
65
65
|
"auth.password.requirements.special": "Un carácter especial",
|
|
66
66
|
"auth.password.requirements.uppercase": "Una letra mayúscula",
|
|
67
67
|
"auth.profile.form.confirmPassword": "Confirmar nueva contraseña",
|
|
68
|
+
"auth.profile.form.currentPassword": "Contraseña actual",
|
|
68
69
|
"auth.profile.form.email": "Correo electrónico",
|
|
70
|
+
"auth.profile.form.errors.confirmPasswordRequired": "Confirma la nueva contraseña.",
|
|
71
|
+
"auth.profile.form.errors.currentPasswordInvalid": "La contraseña actual es incorrecta.",
|
|
72
|
+
"auth.profile.form.errors.currentPasswordRequired": "La contraseña actual es obligatoria.",
|
|
73
|
+
"auth.profile.form.errors.emailOrPasswordRequired": "Proporciona un correo electrónico o una contraseña.",
|
|
69
74
|
"auth.profile.form.errors.emailRequired": "El correo electrónico es obligatorio.",
|
|
70
75
|
"auth.profile.form.errors.invalid": "Actualización de perfil inválida.",
|
|
71
76
|
"auth.profile.form.errors.load": "No se pudo cargar el perfil.",
|
|
77
|
+
"auth.profile.form.errors.newPasswordRequired": "La nueva contraseña es obligatoria.",
|
|
72
78
|
"auth.profile.form.errors.noChanges": "No hay cambios para guardar.",
|
|
73
79
|
"auth.profile.form.errors.passwordMismatch": "Las contraseñas no coinciden.",
|
|
74
80
|
"auth.profile.form.errors.passwordRequirements": "La contraseña debe cumplir los requisitos.",
|
|
@@ -65,10 +65,16 @@
|
|
|
65
65
|
"auth.password.requirements.special": "Jeden znak specjalny",
|
|
66
66
|
"auth.password.requirements.uppercase": "Jedna wielka litera",
|
|
67
67
|
"auth.profile.form.confirmPassword": "Potwierdź nowe hasło",
|
|
68
|
+
"auth.profile.form.currentPassword": "Obecne hasło",
|
|
68
69
|
"auth.profile.form.email": "Email",
|
|
70
|
+
"auth.profile.form.errors.confirmPasswordRequired": "Potwierdź nowe hasło.",
|
|
71
|
+
"auth.profile.form.errors.currentPasswordInvalid": "Obecne hasło jest nieprawidłowe.",
|
|
72
|
+
"auth.profile.form.errors.currentPasswordRequired": "Obecne hasło jest wymagane.",
|
|
73
|
+
"auth.profile.form.errors.emailOrPasswordRequired": "Podaj adres e-mail lub hasło.",
|
|
69
74
|
"auth.profile.form.errors.emailRequired": "Email jest wymagany.",
|
|
70
75
|
"auth.profile.form.errors.invalid": "Nieprawidłowa aktualizacja profilu.",
|
|
71
76
|
"auth.profile.form.errors.load": "Nie udało się wczytać profilu.",
|
|
77
|
+
"auth.profile.form.errors.newPasswordRequired": "Nowe hasło jest wymagane.",
|
|
72
78
|
"auth.profile.form.errors.noChanges": "Brak zmian do zapisania.",
|
|
73
79
|
"auth.profile.form.errors.passwordMismatch": "Hasła nie są zgodne.",
|
|
74
80
|
"auth.profile.form.errors.passwordRequirements": "Hasło musi spełniać wymagania.",
|