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