diff --git a/apps/herbatika/src/components/search/search-autocomplete.tsx b/apps/herbatika/src/components/search/search-autocomplete.tsx index 20e577deb..8583368cb 100644 --- a/apps/herbatika/src/components/search/search-autocomplete.tsx +++ b/apps/herbatika/src/components/search/search-autocomplete.tsx @@ -46,7 +46,7 @@ export function SearchAutocomplete({ aria-controls={controller.hasItems ? controller.panelId : undefined} aria-expanded={controller.shouldShowPanel} aria-haspopup="listbox" - className={`${isMobile ? "px-350 text-sm" : "px-400"} border-none h-full font-verdana -outline-offset-1 outline outline-border-search focus-visible:outline-offset-0 focus-visible:outline-search-form-border-focused`} + className={`${isMobile ? "px-350 text-sm" : "px-400"} -outline-offset-1 h-full border-none font-verdana outline outline-border-search focus-visible:outline-search-form-border-focused focus-visible:outline-offset-0`} maxLength={SEARCH_AUTOCOMPLETE_MAX_QUERY_LENGTH} name="q" onFocus={controller.handleFocus} @@ -56,13 +56,13 @@ export function SearchAutocomplete({ /> diff --git a/apps/medusa-symmy-plugin/package.json b/apps/medusa-symmy-plugin/package.json index 237d10fe8..60bc63782 100644 --- a/apps/medusa-symmy-plugin/package.json +++ b/apps/medusa-symmy-plugin/package.json @@ -48,6 +48,9 @@ "publish:local": "medusa plugin:publish", "typecheck": "tsc --noEmit" }, + "dependencies": { + "jose": "^6.2.3" + }, "devDependencies": { "@medusajs/admin-sdk": "^2.16.0", "@medusajs/admin-shared": "^2.16.0", diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/customers/route.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/customers/route.ts new file mode 100644 index 000000000..9c5bd663b --- /dev/null +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/customers/route.ts @@ -0,0 +1,25 @@ +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import type { Query } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + const { data, metadata } = await query.graph({ + entity: "customer", + fields: req.queryConfig.fields, + filters: req.filterableFields, + pagination: req.queryConfig.pagination, + }) + const paginationMetadata = metadata ?? { + count: Array.isArray(data) ? data.length : 0, + skip: 0, + take: Array.isArray(data) ? data.length : 0, + } + + res.json({ + customers: data, + count: paginationMetadata.count, + offset: paginationMetadata.skip, + limit: paginationMetadata.take, + }) +} diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/middlewares.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/middlewares.ts new file mode 100644 index 000000000..5df119570 --- /dev/null +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/middlewares.ts @@ -0,0 +1,138 @@ +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + MiddlewareRoute, +} from "@medusajs/framework/http" +import { + authenticate, + maybeApplyLinkFilter, + validateAndTransformQuery, +} from "@medusajs/framework/http" +import { PolicyOperation } from "@medusajs/framework/utils" +import { + Entities as CustomersEntities, + listTransformQueryConfig as customersListTransformQueryConfig, +} from "@medusajs/medusa/api/admin/customers/query-config" +import { AdminCustomersParams } from "@medusajs/medusa/api/admin/customers/validators" +import { + Entities as OrdersEntities, + listTransformQueryConfig as ordersListTransformQueryConfig, +} from "@medusajs/medusa/api/admin/orders/query-config" +import { AdminGetOrdersParams } from "@medusajs/medusa/api/admin/orders/validators" +import { + listProductQueryConfig, + Entities as ProductsEntities, +} from "@medusajs/medusa/api/admin/products/query-config" +import { maybeApplyPriceListsFilter } from "@medusajs/medusa/api/admin/products/utils/maybe-apply-price-lists-filter" +import { AdminGetProductsParams } from "@medusajs/medusa/api/admin/products/validators" +import { + Entities as RegionsEntities, + listTransformQueryConfig as regionsListTransformQueryConfig, +} from "@medusajs/medusa/api/admin/regions/query-config" +import { AdminGetRegionsParams } from "@medusajs/medusa/api/admin/regions/validators" +import { + Entities as UsersEntities, + retrieveTransformQueryConfig as usersRetrieveTransformQueryConfig, +} from "@medusajs/medusa/api/admin/users/query-config" +import { AdminGetUserParams } from "@medusajs/medusa/api/admin/users/validators" + +export const symmyAdminRoutes: MiddlewareRoute[] = [ + { + methods: ["GET"], + matcher: "/api/symmy/v1/admin/orders", + middlewares: [ + authenticate("user", ["bearer", "session", "api-key"]), + validateAndTransformQuery( + AdminGetOrdersParams, + ordersListTransformQueryConfig + ), + ], + policies: [ + { + resource: OrdersEntities.order, + operation: PolicyOperation.read, + }, + ], + }, + { + methods: ["GET"], + matcher: "/api/symmy/v1/admin/customers", + middlewares: [ + authenticate("user", ["bearer", "session", "api-key"]), + validateAndTransformQuery( + AdminCustomersParams, + customersListTransformQueryConfig + ), + ], + policies: [ + { + resource: CustomersEntities.customer, + operation: PolicyOperation.read, + }, + ], + }, + { + methods: ["GET"], + matcher: "/api/symmy/v1/admin/products", + middlewares: [ + authenticate("user", ["bearer", "session", "api-key"]), + validateAndTransformQuery(AdminGetProductsParams, listProductQueryConfig), + (req: MedusaRequest, res: MedusaResponse, next: MedusaNextFunction) => { + if ( + !req.filterableFields || + Object.keys(req.filterableFields).length === 0 + ) { + return next() + } + + return maybeApplyLinkFilter({ + entryPoint: "product_sales_channel", + resourceId: "product_id", + filterableField: "sales_channel_id", + })(req, res, next) + }, + maybeApplyPriceListsFilter(), + ], + policies: [ + { + resource: ProductsEntities.product, + operation: PolicyOperation.read, + }, + ], + }, + { + methods: ["GET"], + matcher: "/api/symmy/v1/admin/regions", + middlewares: [ + authenticate("user", ["bearer", "session", "api-key"]), + validateAndTransformQuery( + AdminGetRegionsParams, + regionsListTransformQueryConfig + ), + ], + policies: [ + { + resource: RegionsEntities.region, + operation: PolicyOperation.read, + }, + ], + }, + { + methods: ["GET"], + matcher: "/api/symmy/v1/admin/users/me", + middlewares: [ + authenticate("user", ["bearer", "session", "api-key"]), + validateAndTransformQuery( + AdminGetUserParams, + usersRetrieveTransformQueryConfig + ), + ], + policies: [ + { + resource: UsersEntities.user, + operation: PolicyOperation.read, + }, + ], + }, +] diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/orders/route.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/orders/route.ts new file mode 100644 index 000000000..52ea5234c --- /dev/null +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/orders/route.ts @@ -0,0 +1,37 @@ +import { getOrdersListWorkflow } from "@medusajs/core-flows" +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const variables = { + filters: { + ...req.filterableFields, + is_draft_order: false, + }, + ...req.queryConfig.pagination, + } + + const workflow = getOrdersListWorkflow(req.scope) + const { result } = await workflow.run({ + input: { + fields: req.queryConfig.fields, + variables, + }, + }) + + if (Array.isArray(result)) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + "Unexpected orders workflow result" + ) + } + + const { rows: orders, metadata } = result + + res.json({ + orders, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/products/route.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/products/route.ts new file mode 100644 index 000000000..b364c60d0 --- /dev/null +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/products/route.ts @@ -0,0 +1,25 @@ +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { refetchEntities } from "@medusajs/framework/http" +import { + remapKeysForProduct, + remapProductResponse, +} from "@medusajs/medusa/api/admin/products/helpers" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const selectFields = remapKeysForProduct(req.queryConfig.fields ?? []) + const { data: products, metadata } = await refetchEntities({ + entity: "product", + idOrFilter: req.filterableFields, + scope: req.scope, + fields: selectFields, + pagination: req.queryConfig.pagination, + withDeleted: req.queryConfig.withDeleted, + }) + + res.json({ + products: products.map(remapProductResponse), + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/regions/route.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/regions/route.ts new file mode 100644 index 000000000..9a19723f6 --- /dev/null +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/regions/route.ts @@ -0,0 +1,26 @@ +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/framework/utils" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const query = remoteQueryObjectFromString({ + entryPoint: "region", + variables: { + filters: req.filterableFields, + ...req.queryConfig.pagination, + }, + fields: req.queryConfig.fields, + }) + + const { rows: regions, metadata } = await remoteQuery(query) + + res.json({ + regions, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/users/me/route.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/users/me/route.ts new file mode 100644 index 000000000..60beb7811 --- /dev/null +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/admin/users/me/route.ts @@ -0,0 +1,38 @@ +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, + MedusaError, + remoteQueryObjectFromString, +} from "@medusajs/framework/utils" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.auth_context.actor_id + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + if (!id) { + throw new MedusaError(MedusaError.Types.UNAUTHORIZED, "User ID not found") + } + + const query = remoteQueryObjectFromString({ + entryPoint: "user", + variables: { id }, + fields: req.queryConfig.fields, + }) + + const [user] = await remoteQuery(query) + + if (!user) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `User with id: ${id} was not found` + ) + } + + res.status(200).json({ user }) +} diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/auth/user/emailpass/middlewares.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/auth/user/emailpass/middlewares.ts new file mode 100644 index 000000000..8ac9772dc --- /dev/null +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/auth/user/emailpass/middlewares.ts @@ -0,0 +1,11 @@ +import type { MiddlewareRoute } from "@medusajs/framework/http" +import { validateAndTransformBody } from "@medusajs/framework/http" +import { PostSymmyAuthUserEmailPassSchema } from "./validators" + +export const symmyAuthUserEmailPassRoutes: MiddlewareRoute[] = [ + { + methods: ["POST"], + matcher: "/api/symmy/v1/auth/user/emailpass", + middlewares: [validateAndTransformBody(PostSymmyAuthUserEmailPassSchema)], + }, +] diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/auth/user/emailpass/route.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/auth/user/emailpass/route.ts new file mode 100644 index 000000000..95f763afd --- /dev/null +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/auth/user/emailpass/route.ts @@ -0,0 +1,95 @@ +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import type { IAuthModuleService, Query } from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, + MedusaError, + Modules, +} from "@medusajs/framework/utils" +import type { PostSymmyAuthUserEmailPassSchemaType } from "./validators" + +const JWT_TTL_SECONDS = 24 * 60 * 60 + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const authModuleService = req.scope.resolve(Modules.AUTH) + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + const { email, password } = req.validatedBody + + const result = await authModuleService.authenticate("emailpass", { + actor_type: "user", + body: { email, password }, + }) + + if (!(result.success && result.authIdentity)) { + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + typeof result.error === "string" + ? result.error + : "Invalid email or password" + ) + } + + const userId = await resolveUserId(query, result.authIdentity) + if (!userId) { + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + "Invalid email or password" + ) + } + + const jwtSecret = process.env.JWT_SECRET?.trim() + if (!jwtSecret) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "JWT_SECRET is not configured" + ) + } + + const authIdentity = result.authIdentity as typeof result.authIdentity & { + user_metadata?: Record + } + + const { SignJWT } = await import("jose") + const token = await new SignJWT({ + actor_id: userId, + actor_type: "user", + auth_identity_id: authIdentity.id, + auth_provider: "emailpass", + }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt() + .setExpirationTime(`${JWT_TTL_SECONDS}s`) + .sign(new TextEncoder().encode(jwtSecret)) + + res.status(200).json({ token }) +} + +async function resolveUserId( + query: Query, + authIdentity: { + app_metadata?: Record | null + user_metadata?: Record | null + } +) { + const linkedUserId = authIdentity.app_metadata?.user_id + if (typeof linkedUserId === "string" && linkedUserId.length > 0) { + return linkedUserId + } + + const email = authIdentity.user_metadata?.email + if (typeof email !== "string" || !email) { + return + } + + const { data } = await query.graph({ + entity: "user", + fields: ["id"], + filters: { email }, + }) + + return Array.isArray(data) && typeof data[0]?.id === "string" + ? data[0].id + : undefined +} diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/auth/user/emailpass/validators.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/auth/user/emailpass/validators.ts new file mode 100644 index 000000000..af604af22 --- /dev/null +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/auth/user/emailpass/validators.ts @@ -0,0 +1,10 @@ +import { z } from "@medusajs/framework/zod" + +export const PostSymmyAuthUserEmailPassSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), +}) + +export type PostSymmyAuthUserEmailPassSchemaType = z.infer< + typeof PostSymmyAuthUserEmailPassSchema +> diff --git a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/jobs/[id]/route.ts b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/jobs/[id]/route.ts index e8438cb34..569a51eaa 100644 --- a/apps/medusa-symmy-plugin/src/api/api/symmy/v1/jobs/[id]/route.ts +++ b/apps/medusa-symmy-plugin/src/api/api/symmy/v1/jobs/[id]/route.ts @@ -3,7 +3,7 @@ import { SYMMY_IMPORT_JOB_MODULE, type SymmyImportJobDTO, type SymmyImportJobModuleService, -} from "../../../../../../modules/import-job" +} from "../../../../../../modules/import-job/index" const serializeJob = (job: SymmyImportJobDTO) => ({ id: job.id, diff --git a/apps/medusa-symmy-plugin/src/api/middlewares.ts b/apps/medusa-symmy-plugin/src/api/middlewares.ts index 840d3b044..a1aa336ef 100644 --- a/apps/medusa-symmy-plugin/src/api/middlewares.ts +++ b/apps/medusa-symmy-plugin/src/api/middlewares.ts @@ -7,6 +7,8 @@ import { } from "@medusajs/framework/http" import { MedusaError } from "@medusajs/framework/utils" import { adminSymmyWebhookRoutes } from "./admin/symmy-webhooks/middlewares" +import { symmyAdminRoutes } from "./api/symmy/v1/admin/middlewares" +import { symmyAuthUserEmailPassRoutes } from "./api/symmy/v1/auth/user/emailpass/middlewares" import { symmyCustomerGroupCustomersBatchRoutes } from "./api/symmy/v1/customer-groups/[code]/customers/batch/middlewares" import { symmyCustomerGroupsBatchRoutes } from "./api/symmy/v1/customer-groups/batch/middlewares" import { symmyCustomersBatchRoutes } from "./api/symmy/v1/customers/batch/middlewares" @@ -111,6 +113,8 @@ export default defineMiddlewares({ }, routes: [ ...adminSymmyWebhookRoutes, + ...symmyAdminRoutes, + ...symmyAuthUserEmailPassRoutes, ...symmyJobRoutes, ...symmyProductsBatchRoutes, ...symmyInventoryStockBatchRoutes, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aaf2b91b7..e4905aa25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -564,6 +564,10 @@ importers: version: 5.9.3 apps/medusa-symmy-plugin: + dependencies: + jose: + specifier: ^6.2.3 + version: 6.2.3 devDependencies: '@medusajs/admin-sdk': specifier: ^2.16.0