@jarwizz/create-jarshop 0.1.1 → 0.1.2
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/README.md +8 -0
- package/dist/generator.js +6 -3
- package/dist/generator.js.map +1 -1
- package/dist/template/CONTEXT.md +10 -0
- package/dist/template/README.md +20 -3
- package/dist/template/docker-compose.yml +5 -0
- package/dist/template/docs/smtp-smoke.md +37 -0
- package/dist/template/env.example.template +18 -0
- package/dist/template/gitignore.template +2 -0
- package/dist/template/package.json +13 -5
- package/dist/template/pnpm-lock.yaml +38 -7
- package/dist/template/src/dev-storefront.ts +58 -0
- package/dist/template/src/dev.ts +67 -4
- package/dist/template/src/email-smoke.ts +27 -0
- package/dist/template/src/environment.d.ts +6 -0
- package/dist/template/src/migrate.ts +16 -0
- package/dist/template/src/migrations/1786024108434-jarshop-checkout-attempt.ts +28 -0
- package/dist/template/src/migrations/1786024108435-jarshop-legal-and-confirmation.ts +60 -0
- package/dist/template/src/migrations/1786024108436-jarshop-order-email-outbox.ts +37 -0
- package/dist/template/src/migrations/1786030000000-jarshop-order-email-delivery-lease.ts +27 -0
- package/dist/template/src/plugins/jarshop-checkout/checkout-attempt.entity.ts +42 -0
- package/dist/template/src/plugins/jarshop-checkout/confirmation-token.entity.ts +31 -0
- package/dist/template/src/plugins/jarshop-checkout/confirmation.ts +26 -0
- package/dist/template/src/plugins/jarshop-checkout/dashboard/index.tsx +107 -0
- package/dist/template/src/plugins/jarshop-checkout/dashboard/order-email-delivery-status.ts +43 -0
- package/dist/template/src/plugins/jarshop-checkout/email-smoke-core.ts +56 -0
- package/dist/template/src/plugins/jarshop-checkout/index.ts +47 -0
- package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +813 -0
- package/dist/template/src/plugins/jarshop-checkout/legal.entity.ts +39 -0
- package/dist/template/src/plugins/jarshop-checkout/order-consent.entity.ts +32 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-core.ts +337 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-status.ts +39 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery.entity.ts +66 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-outbox.ts +108 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-transport.ts +35 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-worker.ts +281 -0
- package/dist/template/src/plugins/jarshop-checkout/shipping.test.ts +41 -0
- package/dist/template/src/plugins/jarshop-checkout/shipping.ts +162 -0
- package/dist/template/src/seed.ts +149 -0
- package/dist/template/src/vendure-config.ts +68 -5
- package/dist/template/static/email/templates/email-verification/body.hbs +4 -4
- package/dist/template/storefront/app/[locale]/[slug]/add-to-cart-form.tsx +44 -0
- package/dist/template/storefront/app/[locale]/[slug]/page.tsx +35 -26
- package/dist/template/storefront/app/[locale]/account/account-mutation-form.tsx +44 -0
- package/dist/template/storefront/app/[locale]/account/change-password-form.tsx +65 -0
- package/dist/template/storefront/app/[locale]/account/login/login-form.tsx +60 -0
- package/dist/template/storefront/app/[locale]/account/login/page.tsx +23 -0
- package/dist/template/storefront/app/[locale]/account/page.tsx +215 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/callback/route.ts +33 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/complete/page.tsx +27 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/page.tsx +37 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/password-reset-forms.tsx +99 -0
- package/dist/template/storefront/app/[locale]/account/register/page.tsx +23 -0
- package/dist/template/storefront/app/[locale]/account/register/register-form.tsx +67 -0
- package/dist/template/storefront/app/[locale]/account/verify/complete/route.ts +33 -0
- package/dist/template/storefront/app/[locale]/account/verify/page.tsx +41 -0
- package/dist/template/storefront/app/[locale]/account/verify/refresh-form.tsx +35 -0
- package/dist/template/storefront/app/[locale]/cart/page.tsx +78 -0
- package/dist/template/storefront/app/[locale]/checkout/checkout-form.tsx +112 -0
- package/dist/template/storefront/app/[locale]/checkout/page.tsx +44 -0
- package/dist/template/storefront/app/[locale]/checkout/success/page.tsx +23 -0
- package/dist/template/storefront/app/[locale]/layout.tsx +48 -0
- package/dist/template/storefront/app/[locale]/page.tsx +10 -11
- package/dist/template/storefront/app/globals.css +372 -4
- package/dist/template/storefront/app/layout.tsx +13 -0
- package/dist/template/storefront/eslint.config.mjs +1 -1
- package/dist/template/storefront/gitignore.template +1 -0
- package/dist/template/storefront/lib/account-security.test.ts +74 -0
- package/dist/template/storefront/lib/account-security.ts +81 -0
- package/dist/template/storefront/lib/account.ts +366 -0
- package/dist/template/storefront/lib/cart-contract.ts +52 -0
- package/dist/template/storefront/lib/cart.test.ts +30 -0
- package/dist/template/storefront/lib/cart.ts +130 -0
- package/dist/template/storefront/lib/checkout-contract.test.ts +20 -0
- package/dist/template/storefront/lib/checkout-contract.ts +16 -0
- package/dist/template/storefront/lib/checkout.ts +143 -0
- package/dist/template/storefront/lib/commerce-session.test.ts +77 -0
- package/dist/template/storefront/lib/commerce-session.ts +85 -0
- package/dist/template/storefront/lib/commerce.ts +122 -1
- package/dist/template/storefront/lib/confirmation-cookie.ts +9 -0
- package/dist/template/storefront/lib/confirmation.ts +47 -0
- package/dist/template/storefront/lib/form-values.test.ts +14 -0
- package/dist/template/storefront/lib/form-values.ts +7 -0
- package/dist/template/storefront/lib/i18n.test.ts +50 -0
- package/dist/template/storefront/lib/i18n.ts +44 -0
- package/dist/template/storefront/lib/password-reset-token.test.ts +31 -0
- package/dist/template/storefront/lib/password-reset-token.ts +17 -0
- package/dist/template/storefront/lib/valkey-rate-limit.ts +72 -0
- package/dist/template/storefront/messages/en.json +89 -0
- package/dist/template/storefront/messages/sk.json +89 -0
- package/dist/template/storefront/next.config.ts +3 -1
- package/dist/template/storefront/package.json +4 -1
- package/dist/template/storefront/pnpm-lock.yaml +756 -2
- package/dist/template/tsconfig.json +2 -1
- package/dist/template/turbo.json +1 -0
- package/dist/template/vite.config.mts +4 -1
- package/dist/template-manifest.json +1 -1
- package/dist/template.js +3 -0
- package/dist/template.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,8 @@ declare global {
|
|
|
5
5
|
interface ProcessEnv {
|
|
6
6
|
APP_ENV?: string;
|
|
7
7
|
PORT?: string;
|
|
8
|
+
JARSHOP_STOREFRONT_PORT?: string;
|
|
9
|
+
JARSHOP_COMMERCE_ENDPOINT?: string;
|
|
8
10
|
COOKIE_SECRET?: string;
|
|
9
11
|
SUPERADMIN_USERNAME?: string;
|
|
10
12
|
SUPERADMIN_PASSWORD?: string;
|
|
@@ -14,6 +16,10 @@ declare global {
|
|
|
14
16
|
DB_USERNAME?: string;
|
|
15
17
|
DB_PASSWORD?: string;
|
|
16
18
|
DB_SCHEMA?: string;
|
|
19
|
+
JARSHOP_ORDER_EMAIL_ENCRYPTION_KEY?: string;
|
|
20
|
+
JARSHOP_EMAIL_TRANSPORT?: string;
|
|
21
|
+
JARSHOP_EMAIL_FILE_TRANSPORT_PATH?: string;
|
|
22
|
+
JARSHOP_STOREFRONT_URL?: string;
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { runMigrations } from "@vendure/core";
|
|
2
|
+
|
|
3
|
+
import { config } from "./vendure-config.js";
|
|
4
|
+
|
|
5
|
+
runMigrations(config)
|
|
6
|
+
.then((migrations) => {
|
|
7
|
+
if (migrations.length === 0) {
|
|
8
|
+
console.log("Migrations are up to date.");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
console.log(`Applied migrations: ${migrations.join(", ")}`);
|
|
12
|
+
})
|
|
13
|
+
.catch((error: unknown) => {
|
|
14
|
+
console.error(error);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class JarShopCheckoutAttempt1786024108434 implements MigrationInterface {
|
|
4
|
+
name = "JarShopCheckoutAttempt1786024108434";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`
|
|
8
|
+
CREATE TABLE "jarshop_checkout_attempt" (
|
|
9
|
+
"id" SERIAL NOT NULL,
|
|
10
|
+
"attemptId" character varying(36) NOT NULL,
|
|
11
|
+
"marketCode" character varying(64) NOT NULL,
|
|
12
|
+
"languageCode" character varying(32) NOT NULL,
|
|
13
|
+
"status" character varying(16) NOT NULL,
|
|
14
|
+
"inputHash" character varying(128) NOT NULL,
|
|
15
|
+
"orderId" integer,
|
|
16
|
+
"errorCode" character varying(64),
|
|
17
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
18
|
+
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
19
|
+
CONSTRAINT "PK_jarshop_checkout_attempt" PRIMARY KEY ("id"),
|
|
20
|
+
CONSTRAINT "UQ_jarshop_checkout_attempt_attempt_id" UNIQUE ("attemptId")
|
|
21
|
+
)
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
26
|
+
await queryRunner.query(`DROP TABLE "jarshop_checkout_attempt"`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class JarShopLegalAndConfirmation1786024108435 implements MigrationInterface {
|
|
4
|
+
name = "JarShopLegalAndConfirmation1786024108435";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`
|
|
8
|
+
CREATE TABLE "jarshop_legal_configuration" (
|
|
9
|
+
"id" SERIAL NOT NULL,
|
|
10
|
+
"channelId" integer NOT NULL,
|
|
11
|
+
"languageCode" character varying(10) NOT NULL,
|
|
12
|
+
"termsVersion" character varying(64) NOT NULL,
|
|
13
|
+
"termsUrl" character varying(2048) NOT NULL,
|
|
14
|
+
"privacyVersion" character varying(64) NOT NULL,
|
|
15
|
+
"privacyUrl" character varying(2048) NOT NULL,
|
|
16
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
17
|
+
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
18
|
+
CONSTRAINT "PK_jarshop_legal_configuration" PRIMARY KEY ("id"),
|
|
19
|
+
CONSTRAINT "UQ_jarshop_legal_channel_language" UNIQUE ("channelId", "languageCode")
|
|
20
|
+
)
|
|
21
|
+
`);
|
|
22
|
+
await queryRunner.query(`
|
|
23
|
+
CREATE TABLE "jarshop_order_consent" (
|
|
24
|
+
"id" SERIAL NOT NULL,
|
|
25
|
+
"orderId" integer NOT NULL,
|
|
26
|
+
"termsVersion" character varying(64) NOT NULL,
|
|
27
|
+
"termsUrl" character varying(2048) NOT NULL,
|
|
28
|
+
"privacyUrl" character varying(2048) NOT NULL,
|
|
29
|
+
"acceptedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
30
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
31
|
+
CONSTRAINT "PK_jarshop_order_consent" PRIMARY KEY ("id"),
|
|
32
|
+
CONSTRAINT "UQ_jarshop_order_consent_order" UNIQUE ("orderId")
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
await queryRunner.query(`
|
|
36
|
+
CREATE TABLE "jarshop_confirmation_token" (
|
|
37
|
+
"id" SERIAL NOT NULL,
|
|
38
|
+
"tokenHash" character varying(128) NOT NULL,
|
|
39
|
+
"orderId" integer NOT NULL,
|
|
40
|
+
"expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
41
|
+
"consumedAt" TIMESTAMP WITH TIME ZONE,
|
|
42
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
43
|
+
CONSTRAINT "PK_jarshop_confirmation_token" PRIMARY KEY ("id"),
|
|
44
|
+
CONSTRAINT "UQ_jarshop_confirmation_token_hash" UNIQUE ("tokenHash")
|
|
45
|
+
)
|
|
46
|
+
`);
|
|
47
|
+
await queryRunner.query(
|
|
48
|
+
`CREATE INDEX "IDX_jarshop_confirmation_token_order" ON "jarshop_confirmation_token" ("orderId")`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
53
|
+
await queryRunner.query(
|
|
54
|
+
`DROP INDEX "IDX_jarshop_confirmation_token_order"`,
|
|
55
|
+
);
|
|
56
|
+
await queryRunner.query(`DROP TABLE "jarshop_confirmation_token"`);
|
|
57
|
+
await queryRunner.query(`DROP TABLE "jarshop_order_consent"`);
|
|
58
|
+
await queryRunner.query(`DROP TABLE "jarshop_legal_configuration"`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class JarShopOrderEmailOutbox1786024108436 implements MigrationInterface {
|
|
4
|
+
name = "JarShopOrderEmailOutbox1786024108436";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`
|
|
8
|
+
CREATE TABLE "jarshop_order_email_delivery" (
|
|
9
|
+
"id" SERIAL NOT NULL,
|
|
10
|
+
"orderId" integer NOT NULL,
|
|
11
|
+
"checkoutAttemptId" character varying(36) NOT NULL,
|
|
12
|
+
"kind" character varying(64) NOT NULL,
|
|
13
|
+
"languageCode" character varying(10) NOT NULL,
|
|
14
|
+
"status" character varying(16) NOT NULL,
|
|
15
|
+
"payloadCiphertext" text NOT NULL,
|
|
16
|
+
"attemptCount" integer NOT NULL DEFAULT 0,
|
|
17
|
+
"nextAttemptAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
18
|
+
"lastErrorCode" character varying(64),
|
|
19
|
+
"sentAt" TIMESTAMP WITH TIME ZONE,
|
|
20
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
21
|
+
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
22
|
+
CONSTRAINT "PK_jarshop_order_email_delivery" PRIMARY KEY ("id"),
|
|
23
|
+
CONSTRAINT "UQ_jarshop_order_email_delivery_order_kind" UNIQUE ("orderId", "kind")
|
|
24
|
+
)
|
|
25
|
+
`);
|
|
26
|
+
await queryRunner.query(
|
|
27
|
+
`CREATE INDEX "IDX_jarshop_order_email_delivery_pending" ON "jarshop_order_email_delivery" ("status", "nextAttemptAt")`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
32
|
+
await queryRunner.query(
|
|
33
|
+
`DROP INDEX "IDX_jarshop_order_email_delivery_pending"`,
|
|
34
|
+
);
|
|
35
|
+
await queryRunner.query(`DROP TABLE "jarshop_order_email_delivery"`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This is additive and nullable: existing pending/sent deliveries remain valid
|
|
5
|
+
* while application code rolls forward. It must not delete delivery intent.
|
|
6
|
+
*/
|
|
7
|
+
export class JarShopOrderEmailDeliveryLease1786030000000 implements MigrationInterface {
|
|
8
|
+
name = "JarShopOrderEmailDeliveryLease1786030000000";
|
|
9
|
+
|
|
10
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
11
|
+
await queryRunner.query(
|
|
12
|
+
`ALTER TABLE "jarshop_order_email_delivery" ADD "leaseToken" character varying(36)`,
|
|
13
|
+
);
|
|
14
|
+
await queryRunner.query(
|
|
15
|
+
`ALTER TABLE "jarshop_order_email_delivery" ADD "leaseExpiresAt" TIMESTAMP WITH TIME ZONE`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
20
|
+
await queryRunner.query(
|
|
21
|
+
`ALTER TABLE "jarshop_order_email_delivery" DROP COLUMN "leaseExpiresAt"`,
|
|
22
|
+
);
|
|
23
|
+
await queryRunner.query(
|
|
24
|
+
`ALTER TABLE "jarshop_order_email_delivery" DROP COLUMN "leaseToken"`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Column,
|
|
3
|
+
CreateDateColumn,
|
|
4
|
+
Entity,
|
|
5
|
+
PrimaryGeneratedColumn,
|
|
6
|
+
UpdateDateColumn,
|
|
7
|
+
} from "typeorm";
|
|
8
|
+
|
|
9
|
+
export type CheckoutAttemptStatus = "processing" | "completed" | "failed";
|
|
10
|
+
|
|
11
|
+
@Entity({ name: "jarshop_checkout_attempt" })
|
|
12
|
+
export class CheckoutAttempt {
|
|
13
|
+
@PrimaryGeneratedColumn()
|
|
14
|
+
id!: number;
|
|
15
|
+
|
|
16
|
+
@Column({ type: "varchar", length: 36, unique: true })
|
|
17
|
+
attemptId!: string;
|
|
18
|
+
|
|
19
|
+
@Column({ type: "varchar", length: 64 })
|
|
20
|
+
marketCode!: string;
|
|
21
|
+
|
|
22
|
+
@Column({ type: "varchar", length: 32 })
|
|
23
|
+
languageCode!: string;
|
|
24
|
+
|
|
25
|
+
@Column({ type: "varchar", length: 16 })
|
|
26
|
+
status!: CheckoutAttemptStatus;
|
|
27
|
+
|
|
28
|
+
@Column({ type: "varchar", length: 128 })
|
|
29
|
+
inputHash!: string;
|
|
30
|
+
|
|
31
|
+
@Column({ type: "int", nullable: true })
|
|
32
|
+
orderId!: number | null;
|
|
33
|
+
|
|
34
|
+
@Column({ type: "varchar", length: 64, nullable: true })
|
|
35
|
+
errorCode!: string | null;
|
|
36
|
+
|
|
37
|
+
@CreateDateColumn()
|
|
38
|
+
createdAt!: Date;
|
|
39
|
+
|
|
40
|
+
@UpdateDateColumn()
|
|
41
|
+
updatedAt!: Date;
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Column,
|
|
3
|
+
CreateDateColumn,
|
|
4
|
+
Entity,
|
|
5
|
+
Index,
|
|
6
|
+
PrimaryGeneratedColumn,
|
|
7
|
+
Unique,
|
|
8
|
+
} from "typeorm";
|
|
9
|
+
|
|
10
|
+
@Entity({ name: "jarshop_confirmation_token" })
|
|
11
|
+
export class JarShopConfirmationToken {
|
|
12
|
+
@PrimaryGeneratedColumn()
|
|
13
|
+
id!: number;
|
|
14
|
+
|
|
15
|
+
@Unique("UQ_jarshop_confirmation_token_hash", ["tokenHash"])
|
|
16
|
+
@Column({ type: "varchar", length: 128 })
|
|
17
|
+
tokenHash!: string;
|
|
18
|
+
|
|
19
|
+
@Index("IDX_jarshop_confirmation_token_order")
|
|
20
|
+
@Column({ type: "int" })
|
|
21
|
+
orderId!: number;
|
|
22
|
+
|
|
23
|
+
@Column({ type: "timestamptz" })
|
|
24
|
+
expiresAt!: Date;
|
|
25
|
+
|
|
26
|
+
@Column({ type: "timestamptz", nullable: true })
|
|
27
|
+
consumedAt!: Date | null;
|
|
28
|
+
|
|
29
|
+
@CreateDateColumn()
|
|
30
|
+
createdAt!: Date;
|
|
31
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const CONFIRMATION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
export function hashConfirmationToken(token: string): string {
|
|
6
|
+
return createHash("sha256").update(token).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createConfirmationToken(now = Date.now()) {
|
|
10
|
+
const token = randomBytes(32).toString("base64url");
|
|
11
|
+
return {
|
|
12
|
+
token,
|
|
13
|
+
tokenHash: hashConfirmationToken(token),
|
|
14
|
+
expiresAt: new Date(now + CONFIRMATION_TTL_MS),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function canUseConfirmationToken(input: {
|
|
19
|
+
expiresAt: Date;
|
|
20
|
+
consumedAt: Date | null;
|
|
21
|
+
cookieMatches: boolean;
|
|
22
|
+
now?: number;
|
|
23
|
+
}): boolean {
|
|
24
|
+
if (input.expiresAt.getTime() <= (input.now ?? Date.now())) return false;
|
|
25
|
+
return input.consumedAt === null || input.cookieMatches;
|
|
26
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { api, defineDashboardExtension, useQuery } from "@vendure/dashboard";
|
|
2
|
+
import { graphql } from "@/gql";
|
|
3
|
+
import {
|
|
4
|
+
getDashboardOrderId,
|
|
5
|
+
toOrderEmailDeliveryStatusPanel,
|
|
6
|
+
} from "./order-email-delivery-status.js";
|
|
7
|
+
|
|
8
|
+
const orderEmailDeliveryStatusDocument = graphql(`
|
|
9
|
+
query JarShopOrderEmailDeliveryStatus($orderId: ID!) {
|
|
10
|
+
jarShopOrderEmailDeliveryStatus(orderId: $orderId) {
|
|
11
|
+
status
|
|
12
|
+
attemptCount
|
|
13
|
+
nextAttemptAt
|
|
14
|
+
sentAt
|
|
15
|
+
lastErrorCode
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
`);
|
|
19
|
+
|
|
20
|
+
function formatDateTime(value: string): string {
|
|
21
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
22
|
+
dateStyle: "medium",
|
|
23
|
+
timeStyle: "short",
|
|
24
|
+
}).format(new Date(value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function OrderEmailDeliveryStatusBlock({ context }: { context: unknown }) {
|
|
28
|
+
const orderId = getDashboardOrderId((context as { entity?: unknown }).entity);
|
|
29
|
+
const query = useQuery({
|
|
30
|
+
queryKey: ["jarshop-order-email-delivery-status", orderId],
|
|
31
|
+
enabled: orderId !== undefined,
|
|
32
|
+
queryFn: () =>
|
|
33
|
+
api.query(orderEmailDeliveryStatusDocument, {
|
|
34
|
+
orderId: orderId ?? "",
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!orderId) return null;
|
|
39
|
+
if (query.isPending) {
|
|
40
|
+
return (
|
|
41
|
+
<p className="text-sm text-muted-foreground">Loading delivery status…</p>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (query.isError) {
|
|
45
|
+
return (
|
|
46
|
+
<p className="text-sm text-destructive">
|
|
47
|
+
Delivery status is currently unavailable.
|
|
48
|
+
</p>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const panel = toOrderEmailDeliveryStatusPanel(
|
|
53
|
+
query.data?.jarShopOrderEmailDeliveryStatus,
|
|
54
|
+
);
|
|
55
|
+
if (panel.kind === "unavailable") {
|
|
56
|
+
return (
|
|
57
|
+
<p className="text-sm text-muted-foreground">
|
|
58
|
+
No JarShop order email is queued for this order.
|
|
59
|
+
</p>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<dl className="grid gap-2 text-sm">
|
|
65
|
+
<div className="flex justify-between gap-4">
|
|
66
|
+
<dt className="text-muted-foreground">Status</dt>
|
|
67
|
+
<dd className="font-medium capitalize">{panel.status}</dd>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex justify-between gap-4">
|
|
70
|
+
<dt className="text-muted-foreground">Attempts</dt>
|
|
71
|
+
<dd>{panel.attemptCount}</dd>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="flex justify-between gap-4">
|
|
74
|
+
<dt className="text-muted-foreground">Next attempt</dt>
|
|
75
|
+
<dd>{formatDateTime(panel.nextAttemptAt)}</dd>
|
|
76
|
+
</div>
|
|
77
|
+
{panel.sentAt ? (
|
|
78
|
+
<div className="flex justify-between gap-4">
|
|
79
|
+
<dt className="text-muted-foreground">Sent</dt>
|
|
80
|
+
<dd>{formatDateTime(panel.sentAt)}</dd>
|
|
81
|
+
</div>
|
|
82
|
+
) : null}
|
|
83
|
+
{panel.lastErrorCode ? (
|
|
84
|
+
<div className="flex justify-between gap-4">
|
|
85
|
+
<dt className="text-muted-foreground">Error code</dt>
|
|
86
|
+
<dd className="font-mono text-xs">{panel.lastErrorCode}</dd>
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
89
|
+
</dl>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
defineDashboardExtension({
|
|
94
|
+
pageBlocks: [
|
|
95
|
+
{
|
|
96
|
+
id: "jarshop-order-email-delivery-status",
|
|
97
|
+
title: "Email delivery",
|
|
98
|
+
location: {
|
|
99
|
+
pageId: "order-detail",
|
|
100
|
+
column: "side",
|
|
101
|
+
position: { blockId: "state", order: "after" },
|
|
102
|
+
},
|
|
103
|
+
component: OrderEmailDeliveryStatusBlock,
|
|
104
|
+
requiresPermission: "ReadOrder",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type OrderEmailDeliveryStatusPanel =
|
|
2
|
+
| { kind: "unavailable" }
|
|
3
|
+
| {
|
|
4
|
+
kind: "ready";
|
|
5
|
+
status: "pending" | "processing" | "sent" | "failed";
|
|
6
|
+
attemptCount: number;
|
|
7
|
+
nextAttemptAt: string;
|
|
8
|
+
sentAt: string | null;
|
|
9
|
+
lastErrorCode: string | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function getDashboardOrderId(entity: unknown): string | undefined {
|
|
13
|
+
if (!entity || typeof entity !== "object") return undefined;
|
|
14
|
+
const id = (entity as { id?: unknown }).id;
|
|
15
|
+
if (typeof id === "string" && id.trim()) return id;
|
|
16
|
+
if (typeof id === "number" && Number.isSafeInteger(id) && id > 0) {
|
|
17
|
+
return String(id);
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function toOrderEmailDeliveryStatusPanel(
|
|
23
|
+
delivery:
|
|
24
|
+
| {
|
|
25
|
+
status: "pending" | "processing" | "sent" | "failed";
|
|
26
|
+
attemptCount: number;
|
|
27
|
+
nextAttemptAt: string;
|
|
28
|
+
sentAt: string | null;
|
|
29
|
+
lastErrorCode: string | null;
|
|
30
|
+
}
|
|
31
|
+
| null
|
|
32
|
+
| undefined,
|
|
33
|
+
): OrderEmailDeliveryStatusPanel {
|
|
34
|
+
if (!delivery) return { kind: "unavailable" };
|
|
35
|
+
return {
|
|
36
|
+
kind: "ready",
|
|
37
|
+
status: delivery.status,
|
|
38
|
+
attemptCount: delivery.attemptCount,
|
|
39
|
+
nextAttemptAt: delivery.nextAttemptAt,
|
|
40
|
+
sentAt: delivery.sentAt,
|
|
41
|
+
lastErrorCode: delivery.lastErrorCode,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readOrderEmailTransportConfiguration,
|
|
3
|
+
type SmtpOrderEmailTransportConfiguration,
|
|
4
|
+
} from "./order-email-delivery-core.js";
|
|
5
|
+
|
|
6
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
7
|
+
|
|
8
|
+
export type SmtpSmokeCommand = {
|
|
9
|
+
recipient: string;
|
|
10
|
+
configuration: SmtpOrderEmailTransportConfiguration;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function parseSmtpSmokeCommand(
|
|
14
|
+
arguments_: readonly string[],
|
|
15
|
+
environment: Environment,
|
|
16
|
+
): SmtpSmokeCommand {
|
|
17
|
+
if (environment.APP_ENV !== "staging") {
|
|
18
|
+
throw new Error("SMTP smoke is allowed only when APP_ENV=staging");
|
|
19
|
+
}
|
|
20
|
+
if (!arguments_.includes("--confirm-external-send")) {
|
|
21
|
+
throw new Error("SMTP smoke requires --confirm-external-send");
|
|
22
|
+
}
|
|
23
|
+
const recipient = readRecipient(arguments_);
|
|
24
|
+
const configuration = readOrderEmailTransportConfiguration(environment);
|
|
25
|
+
if (configuration.kind !== "smtp") {
|
|
26
|
+
throw new Error("SMTP smoke requires JARSHOP_EMAIL_TRANSPORT=smtp");
|
|
27
|
+
}
|
|
28
|
+
return { recipient, configuration };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readRecipient(arguments_: readonly string[]): string {
|
|
32
|
+
const toIndex = arguments_.indexOf("--to");
|
|
33
|
+
const recipient =
|
|
34
|
+
toIndex === -1 ? undefined : arguments_[toIndex + 1]?.trim();
|
|
35
|
+
if (recipient === undefined || !isEmailAddress(recipient)) {
|
|
36
|
+
throw new Error("SMTP smoke requires a valid --to email address");
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
arguments_.some(
|
|
40
|
+
(argument, index) =>
|
|
41
|
+
(argument === "--to" && index !== toIndex) ||
|
|
42
|
+
(argument !== "--to" &&
|
|
43
|
+
argument !== "--confirm-external-send" &&
|
|
44
|
+
index !== toIndex + 1),
|
|
45
|
+
)
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"SMTP smoke supports only --to and --confirm-external-send",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return recipient;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isEmailAddress(value: string): boolean {
|
|
55
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(value);
|
|
56
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export { CheckoutAttempt } from "./checkout-attempt.entity.js";
|
|
2
|
+
export type { CheckoutAttemptStatus } from "./checkout-attempt.entity.js";
|
|
3
|
+
export { JarShopConfirmationToken } from "./confirmation-token.entity.js";
|
|
4
|
+
export {
|
|
5
|
+
CONFIRMATION_TTL_MS,
|
|
6
|
+
canUseConfirmationToken,
|
|
7
|
+
createConfirmationToken,
|
|
8
|
+
hashConfirmationToken,
|
|
9
|
+
} from "./confirmation.js";
|
|
10
|
+
export { JarShopLegalConfiguration } from "./legal.entity.js";
|
|
11
|
+
export { JarShopOrderEmailDelivery } from "./order-email-delivery.entity.js";
|
|
12
|
+
export type { OrderEmailDeliveryStatus } from "./order-email-delivery.entity.js";
|
|
13
|
+
export {
|
|
14
|
+
assertOrderEmailOutboxEncryptionConfiguration,
|
|
15
|
+
createPendingOrderEmailDelivery,
|
|
16
|
+
decryptOrderEmailOutboxPayload,
|
|
17
|
+
encryptOrderEmailOutboxPayload,
|
|
18
|
+
parseOrderEmailEncryptionKey,
|
|
19
|
+
} from "./order-email-outbox.js";
|
|
20
|
+
export {
|
|
21
|
+
assertOrderEmailDeliveryConfiguration,
|
|
22
|
+
createFileOrderEmailTransport,
|
|
23
|
+
createSmtpOrderEmailTransport,
|
|
24
|
+
orderEmailDeliveryPolicy,
|
|
25
|
+
readOrderEmailTransportConfiguration,
|
|
26
|
+
registerUniqueScheduledTask,
|
|
27
|
+
renderOrderConfirmationEmail,
|
|
28
|
+
scheduleOrderEmailRetry,
|
|
29
|
+
sanitizeFileTransportEmail,
|
|
30
|
+
writeSanitizedFileTransport,
|
|
31
|
+
} from "./order-email-delivery-core.js";
|
|
32
|
+
export type {
|
|
33
|
+
OrderConfirmationEmail,
|
|
34
|
+
OrderConfirmationEmailInput,
|
|
35
|
+
OrderEmailLanguageCode,
|
|
36
|
+
OrderEmailTransport,
|
|
37
|
+
OrderEmailTransportConfiguration,
|
|
38
|
+
SmtpOrderEmailTransportConfiguration,
|
|
39
|
+
} from "./order-email-delivery-core.js";
|
|
40
|
+
export { createConfiguredOrderEmailTransport } from "./order-email-transport.js";
|
|
41
|
+
export { parseSmtpSmokeCommand } from "./email-smoke-core.js";
|
|
42
|
+
export {
|
|
43
|
+
jarShopOrderEmailWorkerTask,
|
|
44
|
+
registerJarShopOrderEmailWorkerTask,
|
|
45
|
+
} from "./order-email-worker.js";
|
|
46
|
+
export { JarShopOrderConsent } from "./order-consent.entity.js";
|
|
47
|
+
export { JarShopCheckoutPlugin } from "./jarshop-checkout.plugin.js";
|