@revenexx/cover 0.1.13 → 0.1.15
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/app/components/auth/AuthForgotPasswordPanel.vue +11 -17
- package/app/components/auth/AuthLoginPanel.vue +18 -26
- package/app/components/auth/AuthResetPasswordPanel.vue +18 -25
- package/app/interfaces/auth.ts +6 -0
- package/app/plugins/auth-init.client.ts +7 -0
- package/app/plugins/auth-session.server.ts +24 -0
- package/package.json +1 -1
- package/server/api/account/orders.get.ts +10 -2
- package/server/api/orders/index.post.ts +10 -2
- package/server/services/ApiAuthService.ts +2 -0
- package/app/composables/useFormValidation.ts +0 -29
- package/app/interfaces/validation.ts +0 -14
- package/app/validations/emailFormat.ts +0 -10
- package/app/validations/formValidationConfig.ts +0 -20
- package/app/validations/minLength.ts +0 -9
- package/app/validations/passwordsMatch.ts +0 -5
- package/app/validations/required.ts +0 -11
- package/app/validations/types.ts +0 -10
|
@@ -19,8 +19,6 @@ const state = reactive<ForgotPasswordFormState>({
|
|
|
19
19
|
email: "",
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
const { validate } = useFormValidation<ForgotPasswordFormState>("forgotPassword");
|
|
23
|
-
|
|
24
22
|
async function onSubmit(): Promise<void> {
|
|
25
23
|
try {
|
|
26
24
|
const url = new URL(localePath("/reset-password"), window.location.origin).toString();
|
|
@@ -39,25 +37,21 @@ async function onSubmit(): Promise<void> {
|
|
|
39
37
|
{{ t('auth.forgotPassword.title') }}
|
|
40
38
|
</h2>
|
|
41
39
|
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
:
|
|
40
|
+
<FormKit
|
|
41
|
+
type="form"
|
|
42
|
+
:actions="false"
|
|
45
43
|
class="space-y-4 pt-6"
|
|
46
44
|
@submit="onSubmit"
|
|
47
45
|
>
|
|
48
|
-
<
|
|
46
|
+
<FormKit
|
|
47
|
+
v-model="state.email"
|
|
48
|
+
type="email"
|
|
49
49
|
:label="t('auth.forgotPassword.fields.email')"
|
|
50
50
|
name="email"
|
|
51
|
-
required
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
type="email"
|
|
56
|
-
autocomplete="email"
|
|
57
|
-
class="w-full"
|
|
58
|
-
:placeholder="t('auth.forgotPassword.fields.emailPlaceholder')"
|
|
59
|
-
/>
|
|
60
|
-
</UFormField>
|
|
51
|
+
validation="required|email"
|
|
52
|
+
autocomplete="email"
|
|
53
|
+
:placeholder="t('auth.forgotPassword.fields.emailPlaceholder')"
|
|
54
|
+
/>
|
|
61
55
|
|
|
62
56
|
<UAlert
|
|
63
57
|
v-if="error"
|
|
@@ -83,6 +77,6 @@ async function onSubmit(): Promise<void> {
|
|
|
83
77
|
{{ t('auth.forgotPassword.backToLogin') }}
|
|
84
78
|
</NuxtLink>
|
|
85
79
|
</div>
|
|
86
|
-
</
|
|
80
|
+
</FormKit>
|
|
87
81
|
</div>
|
|
88
82
|
</template>
|
|
@@ -20,8 +20,6 @@ const state = reactive<LoginFormState>({
|
|
|
20
20
|
password: "",
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
const { validate } = useFormValidation<LoginFormState>("login");
|
|
24
|
-
|
|
25
23
|
async function onSubmit(): Promise<void> {
|
|
26
24
|
try {
|
|
27
25
|
await auth.login({
|
|
@@ -42,37 +40,31 @@ async function onSubmit(): Promise<void> {
|
|
|
42
40
|
{{ t('auth.login.existingCustomer') }}
|
|
43
41
|
</h2>
|
|
44
42
|
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
:
|
|
43
|
+
<FormKit
|
|
44
|
+
type="form"
|
|
45
|
+
:actions="false"
|
|
48
46
|
class="space-y-4 pt-6"
|
|
49
47
|
@submit="onSubmit"
|
|
50
48
|
>
|
|
51
|
-
<
|
|
49
|
+
<FormKit
|
|
50
|
+
v-model="state.email"
|
|
51
|
+
type="email"
|
|
52
52
|
:label="t('auth.login.fields.email')"
|
|
53
53
|
name="email"
|
|
54
|
-
required
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
type="email"
|
|
59
|
-
autocomplete="email"
|
|
60
|
-
class="w-full"
|
|
61
|
-
:placeholder="t('auth.login.fields.emailPlaceholder')"
|
|
62
|
-
/>
|
|
63
|
-
</UFormField>
|
|
54
|
+
validation="required|email"
|
|
55
|
+
autocomplete="email"
|
|
56
|
+
:placeholder="t('auth.login.fields.emailPlaceholder')"
|
|
57
|
+
/>
|
|
64
58
|
|
|
65
|
-
<
|
|
59
|
+
<FormKit
|
|
60
|
+
v-model="state.password"
|
|
61
|
+
type="password"
|
|
66
62
|
:label="t('auth.login.fields.password')"
|
|
67
63
|
name="password"
|
|
68
|
-
required
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
autocomplete="current-password"
|
|
73
|
-
:placeholder="t('auth.login.fields.passwordPlaceholder')"
|
|
74
|
-
/>
|
|
75
|
-
</UFormField>
|
|
64
|
+
validation="required|length:8"
|
|
65
|
+
autocomplete="current-password"
|
|
66
|
+
:placeholder="t('auth.login.fields.passwordPlaceholder')"
|
|
67
|
+
/>
|
|
76
68
|
|
|
77
69
|
<UAlert
|
|
78
70
|
v-if="error"
|
|
@@ -98,6 +90,6 @@ async function onSubmit(): Promise<void> {
|
|
|
98
90
|
{{ t('auth.login.forgotPassword') }}
|
|
99
91
|
</NuxtLink>
|
|
100
92
|
</div>
|
|
101
|
-
</
|
|
93
|
+
</FormKit>
|
|
102
94
|
</div>
|
|
103
95
|
</template>
|
|
@@ -27,8 +27,6 @@ const state = reactive<ResetPasswordFormState>({
|
|
|
27
27
|
passwordConfirm: "",
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
const { validate } = useFormValidation<ResetPasswordFormState>("resetPassword");
|
|
31
|
-
|
|
32
30
|
async function onSubmit(): Promise<void> {
|
|
33
31
|
try {
|
|
34
32
|
await auth.confirmRecovery(props.userId, props.secret, state.password);
|
|
@@ -54,37 +52,32 @@ async function onSubmit(): Promise<void> {
|
|
|
54
52
|
:title="t('auth.resetPassword.errors.invalidLink')"
|
|
55
53
|
/>
|
|
56
54
|
|
|
57
|
-
<
|
|
55
|
+
<FormKit
|
|
58
56
|
v-else
|
|
59
|
-
|
|
60
|
-
:
|
|
57
|
+
type="form"
|
|
58
|
+
:actions="false"
|
|
61
59
|
class="space-y-4 pt-6"
|
|
62
60
|
@submit="onSubmit"
|
|
63
61
|
>
|
|
64
|
-
<
|
|
62
|
+
<FormKit
|
|
63
|
+
v-model="state.password"
|
|
64
|
+
type="password"
|
|
65
65
|
:label="t('auth.resetPassword.fields.password')"
|
|
66
66
|
name="password"
|
|
67
|
-
required
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
autocomplete="new-password"
|
|
72
|
-
:placeholder="t('auth.resetPassword.fields.passwordPlaceholder')"
|
|
73
|
-
show-strength
|
|
74
|
-
/>
|
|
75
|
-
</UFormField>
|
|
67
|
+
validation="required|length:8"
|
|
68
|
+
autocomplete="new-password"
|
|
69
|
+
:placeholder="t('auth.resetPassword.fields.passwordPlaceholder')"
|
|
70
|
+
/>
|
|
76
71
|
|
|
77
|
-
<
|
|
72
|
+
<FormKit
|
|
73
|
+
v-model="state.passwordConfirm"
|
|
74
|
+
type="password"
|
|
78
75
|
:label="t('auth.resetPassword.fields.passwordConfirm')"
|
|
79
76
|
name="passwordConfirm"
|
|
80
|
-
required
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
autocomplete="new-password"
|
|
85
|
-
:placeholder="t('auth.resetPassword.fields.passwordConfirmPlaceholder')"
|
|
86
|
-
/>
|
|
87
|
-
</UFormField>
|
|
77
|
+
validation="required|confirm:password"
|
|
78
|
+
autocomplete="new-password"
|
|
79
|
+
:placeholder="t('auth.resetPassword.fields.passwordConfirmPlaceholder')"
|
|
80
|
+
/>
|
|
88
81
|
|
|
89
82
|
<UAlert
|
|
90
83
|
v-if="error"
|
|
@@ -110,6 +103,6 @@ async function onSubmit(): Promise<void> {
|
|
|
110
103
|
{{ t('auth.forgotPassword.backToLogin') }}
|
|
111
104
|
</NuxtLink>
|
|
112
105
|
</div>
|
|
113
|
-
</
|
|
106
|
+
</FormKit>
|
|
114
107
|
</div>
|
|
115
108
|
</template>
|
package/app/interfaces/auth.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface AuthUser {
|
|
|
10
10
|
name: string;
|
|
11
11
|
email: string;
|
|
12
12
|
role?: AuthRole;
|
|
13
|
+
/** The customers-app contact (system of record for orders/cart ownership).
|
|
14
|
+
* Lets server routes attribute actions to the contact even when the session
|
|
15
|
+
* cookie predates the contact link. */
|
|
16
|
+
contactId?: string;
|
|
17
|
+
/** The contact's B2B organization, when one applies. */
|
|
18
|
+
organizationId?: string;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default defineNuxtPlugin(() => {
|
|
2
|
+
const auth = useAuthStore();
|
|
3
|
+
const cart = useCartStore();
|
|
4
|
+
// auth.init() must complete first: it detects auth-session expiry and clears
|
|
5
|
+
// the cart (case 2) before cart.initFromServer() tries to restore from server.
|
|
6
|
+
void auth.init().then(() => cart.initFromServer());
|
|
7
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getCookie } from "h3";
|
|
2
|
+
|
|
3
|
+
import type { StoredSession } from "../interfaces/auth";
|
|
4
|
+
|
|
5
|
+
export default defineNuxtPlugin(() => {
|
|
6
|
+
const event = useRequestEvent();
|
|
7
|
+
if (!event) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const raw = getCookie(event, "cover-session");
|
|
12
|
+
if (!raw) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(raw) as StoredSession;
|
|
18
|
+
const auth = useAuthStore();
|
|
19
|
+
auth.session = parsed;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Invalid cookie value — ignore
|
|
23
|
+
}
|
|
24
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/cover",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Cover \u2014 revenexx design system for Nuxt. Distributed as a Nuxt layer: generic UI components, theming tokens and stores shared by the demo shop, custom storefronts and the Blokkli theme.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -6,7 +6,15 @@ const LIVE_HISTORY_LIMIT = 20;
|
|
|
6
6
|
export default defineEventHandler(async (event): Promise<AccountOrderListResponse> => {
|
|
7
7
|
if (resolveOrderServiceKey(event) === "api") {
|
|
8
8
|
const refs = sessionOrderRefs(event);
|
|
9
|
-
|
|
9
|
+
// Mirror the order-placement fallback: when the session cookie carries no
|
|
10
|
+
// contact link, resolve it from the authenticated user — otherwise a
|
|
11
|
+
// logged-in customer with a late-linked contact sees an empty history.
|
|
12
|
+
let contactId = refs.contact_id;
|
|
13
|
+
if (!contactId) {
|
|
14
|
+
const user = await getAuthService(event).me(event);
|
|
15
|
+
contactId = user?.contactId;
|
|
16
|
+
}
|
|
17
|
+
if (!contactId) {
|
|
10
18
|
return { orders: [] };
|
|
11
19
|
}
|
|
12
20
|
try {
|
|
@@ -14,7 +22,7 @@ export default defineEventHandler(async (event): Promise<AccountOrderListRespons
|
|
|
14
22
|
// Raw call: orders.list does not declare its query parameters in
|
|
15
23
|
// the contract yet, so ordersList() cannot filter by contact.
|
|
16
24
|
const { items } = await sdk.call<{ items: LiveOrder[] }>("GET", "/v1/orders", {
|
|
17
|
-
query: { contact_id:
|
|
25
|
+
query: { contact_id: contactId, limit: LIVE_HISTORY_LIMIT },
|
|
18
26
|
});
|
|
19
27
|
// The list rows carry no positions — load the aggregates (the
|
|
20
28
|
// history is capped, so this stays a bounded fan-out).
|
|
@@ -205,9 +205,17 @@ export default defineEventHandler(async (event) => {
|
|
|
205
205
|
const shippingRow = calculation.totals.find(row => row.key === "shipping");
|
|
206
206
|
try {
|
|
207
207
|
const refs = sessionOrderRefs(event);
|
|
208
|
+
// Attribute the order to the acting contact. The session cookie is
|
|
209
|
+
// the fast path, but it can lack the contact link (cookie predates
|
|
210
|
+
// the contact, or login resolved it late) — fall back to the
|
|
211
|
+
// authenticated user's resolved contact so a logged-in order is
|
|
212
|
+
// never stored contactless (which would orphan it from the account
|
|
213
|
+
// order history, filtered strictly by contact_id).
|
|
214
|
+
const contactId = refs.contact_id ?? user?.contactId;
|
|
215
|
+
const organizationId = refs.organization_id ?? user?.organizationId;
|
|
208
216
|
const placed = await useRevenexxSdk().orders.ordersPlace({
|
|
209
|
-
...(
|
|
210
|
-
...(
|
|
217
|
+
...(contactId ? { contactId } : {}),
|
|
218
|
+
...(organizationId ? { organizationId } : {}),
|
|
211
219
|
currency: calculation.currency,
|
|
212
220
|
...(body.orderNumber ? { customerOrderNumber: body.orderNumber } : {}),
|
|
213
221
|
buyer: user ? { name: user.name, email: user.email } : { email: body.contactEmail ?? null },
|
|
@@ -106,6 +106,8 @@ export class ApiAuthService implements IAuthService {
|
|
|
106
106
|
name: contactName(contact, user.name ?? user.email),
|
|
107
107
|
email: user.email,
|
|
108
108
|
role: toAuthRole(contact?.role),
|
|
109
|
+
...(contact?.id ? { contactId: contact.id } : {}),
|
|
110
|
+
...(contact?.organization_id ? { organizationId: contact.organization_id } : {}),
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
113
|
catch (err) {
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { FormError } from "@nuxt/ui";
|
|
2
|
-
|
|
3
|
-
import type { FormKey } from "../interfaces/validation";
|
|
4
|
-
import { formValidationConfig } from "../validations/formValidationConfig";
|
|
5
|
-
|
|
6
|
-
export function useFormValidation<TState extends object>(formKey: FormKey) {
|
|
7
|
-
const { t } = useI18n();
|
|
8
|
-
const config = formValidationConfig[formKey];
|
|
9
|
-
|
|
10
|
-
function validate(state: TState): FormError[] {
|
|
11
|
-
const errors: FormError[] = [];
|
|
12
|
-
const stateRecord = state as Record<string, unknown>;
|
|
13
|
-
for (const fieldConfig of config) {
|
|
14
|
-
const value = stateRecord[fieldConfig.field];
|
|
15
|
-
for (const rule of fieldConfig.rules) {
|
|
16
|
-
const key = rule.kind === "cross"
|
|
17
|
-
? rule.fn(value, stateRecord, rule.options)
|
|
18
|
-
: rule.fn(value, rule.options);
|
|
19
|
-
if (key !== null) {
|
|
20
|
-
errors.push({ name: fieldConfig.field, message: t(key, rule.options ?? {}) });
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return errors;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return { validate };
|
|
29
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { CrossFieldValidator, SimpleValidator } from "../validations/types";
|
|
2
|
-
|
|
3
|
-
export type FormKey = "login" | "forgotPassword" | "resetPassword";
|
|
4
|
-
|
|
5
|
-
export interface FieldValidationRule {
|
|
6
|
-
readonly kind: "simple" | "cross";
|
|
7
|
-
readonly fn: SimpleValidator | CrossFieldValidator;
|
|
8
|
-
readonly options?: Readonly<Record<string, unknown>>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface FieldConfig {
|
|
12
|
-
readonly field: string;
|
|
13
|
-
readonly rules: ReadonlyArray<FieldValidationRule>;
|
|
14
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { EMAIL_VALIDATION_REGEX } from "../shared/constants";
|
|
2
|
-
|
|
3
|
-
import type { SimpleValidator } from "./types";
|
|
4
|
-
|
|
5
|
-
export const emailFormat: SimpleValidator = (value) => {
|
|
6
|
-
if (typeof value !== "string" || !EMAIL_VALIDATION_REGEX.test(value)) {
|
|
7
|
-
return "auth.register.errors.emailFormat";
|
|
8
|
-
}
|
|
9
|
-
return null;
|
|
10
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { FieldConfig, FormKey } from "../interfaces/validation";
|
|
2
|
-
|
|
3
|
-
import { emailFormat } from "./emailFormat";
|
|
4
|
-
import { minLength } from "./minLength";
|
|
5
|
-
import { passwordsMatch } from "./passwordsMatch";
|
|
6
|
-
import { required } from "./required";
|
|
7
|
-
|
|
8
|
-
export const formValidationConfig: Record<FormKey, FieldConfig[]> = {
|
|
9
|
-
login: [
|
|
10
|
-
{ field: "email", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: emailFormat }] },
|
|
11
|
-
{ field: "password", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 8 } }] },
|
|
12
|
-
],
|
|
13
|
-
forgotPassword: [
|
|
14
|
-
{ field: "email", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: emailFormat }] },
|
|
15
|
-
],
|
|
16
|
-
resetPassword: [
|
|
17
|
-
{ field: "password", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 8 } }] },
|
|
18
|
-
{ field: "passwordConfirm", rules: [{ kind: "cross", fn: passwordsMatch }] },
|
|
19
|
-
],
|
|
20
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
export const required: SimpleValidator = (value) => {
|
|
4
|
-
if (typeof value === "boolean") {
|
|
5
|
-
return value === false ? "required" : null;
|
|
6
|
-
}
|
|
7
|
-
if (typeof value === "string") {
|
|
8
|
-
return value.trim() === "" ? "required" : null;
|
|
9
|
-
}
|
|
10
|
-
return value === null || value === undefined ? "required" : null;
|
|
11
|
-
};
|
package/app/validations/types.ts
DELETED