Removes fulfillment service creation entirely. Fetches the store's primary location ID via locations(first:1) query. Requires read_locations scope in SHOPIFY_SCOPES env var. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
3.2 KiB
JavaScript
93 lines
3.2 KiB
JavaScript
const express = require("express");
|
|
const axios = require("axios");
|
|
const { log } = require("./logger");
|
|
const { saveToken, getToken, deleteToken } = require("./tokenStore");
|
|
|
|
const router = express.Router();
|
|
|
|
const CLIENT_ID = process.env.SHOPIFY_CLIENT_ID;
|
|
const CLIENT_SECRET = process.env.SHOPIFY_CLIENT_SECRET;
|
|
|
|
router.get("/auth/login", (req, res) => {
|
|
const { shop } = req.query;
|
|
|
|
if (!shop) {
|
|
return res.status(400).json({ error: "Missing shop query parameter." });
|
|
}
|
|
|
|
const redirectUri = process.env.SHOPIFY_REDIRECT_URI || `${process.env.APP_URL || "http://localhost:3002"}/auth/callback`;
|
|
const scopes = process.env.SHOPIFY_SCOPES || "write_products,write_files,write_inventory,write_publications";
|
|
const installUrl = `https://${shop}/admin/oauth/authorize?client_id=${CLIENT_ID}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
|
|
return res.redirect(installUrl);
|
|
});
|
|
|
|
router.get("/auth/callback", async (req, res) => {
|
|
const { shop, code } = req.query;
|
|
|
|
if (!shop || !code) {
|
|
log("general", `Missing shop or code in callback: ${JSON.stringify(req.query)}`);
|
|
return res.status(400).send("Missing shop or code parameter.");
|
|
}
|
|
|
|
try {
|
|
log(shop, "Exchanging OAuth code for access token");
|
|
const resp = await axios.post(
|
|
`https://${shop}/admin/oauth/access_token`,
|
|
{
|
|
client_id: CLIENT_ID,
|
|
client_secret: CLIENT_SECRET,
|
|
code,
|
|
},
|
|
{
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
|
|
const { access_token, scope } = resp.data;
|
|
saveToken(shop, access_token, scope);
|
|
log(shop, "Token saved to data/tokens.json");
|
|
|
|
// Fetch the store's primary location ID (requires read_locations scope)
|
|
try {
|
|
const locResp = await axios.post(
|
|
`https://${shop}/admin/api/${process.env.SHOPIFY_API_VERSION || "2025-10"}/graphql.json`,
|
|
{ query: "{ locations(first: 1) { nodes { id name } } }" },
|
|
{ headers: { "X-Shopify-Access-Token": access_token, "Content-Type": "application/json" } }
|
|
);
|
|
const node = locResp.data?.data?.locations?.nodes?.[0];
|
|
if (node?.id) {
|
|
saveToken(shop, access_token, scope, null, node.id);
|
|
log(shop, `Primary location saved: ${node.name} (${node.id})`);
|
|
} else {
|
|
log(shop, `No location returned — add read_locations to SHOPIFY_SCOPES and re-auth.`);
|
|
}
|
|
} catch (locErr) {
|
|
log(shop, `Could not fetch primary location: ${locErr.message}`);
|
|
}
|
|
|
|
const finalToken = getToken(shop);
|
|
log(shop, `Auth complete. Stored token:\n${JSON.stringify(finalToken, null, 2)}`);
|
|
|
|
const redirectTarget = process.env.SHOPIFY_AFTER_AUTH_REDIRECT || "https://admin.shopify.com";
|
|
return res.redirect(redirectTarget);
|
|
} catch (err) {
|
|
const errMsg = err.response?.data || err.message;
|
|
log(shop, `OAuth error: ${JSON.stringify(errMsg)}`);
|
|
return res.status(500).send("Failed to get access token");
|
|
}
|
|
});
|
|
|
|
router.post("/auth/logout", (req, res) => {
|
|
const { shop } = req.body || {};
|
|
if (!shop) {
|
|
return res.status(400).json({ error: "Missing shop." });
|
|
}
|
|
|
|
deleteToken(shop);
|
|
log(shop, "Shop token removed");
|
|
return res.json({ ok: true, shop });
|
|
});
|
|
|
|
module.exports = router;
|