feat: add GitHub OAuth2 login and protect product mutations with JWT
- add /auth/login and /auth/callback (rewritten to pages/api/auth/*) for the GitHub Authorization Code flow, with a state cookie against login CSRF - issue a 1h JWT after the code exchange and verify it (HS256) in the Apollo context; createProduct, updateProduct and deleteProduct require it - set products.created_by from the logged-in GitHub user (upsert into users) - preset the Authorization header in the embedded Sandbox - add jsonwebtoken, drop unused cors, rename package to week-7
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Lab 07 memakai URL /auth/login dan /auth/callback (tanpa prefix /api).
|
||||||
|
rewrites() {
|
||||||
|
return [
|
||||||
|
{ source: '/auth/login', destination: '/api/auth/login' },
|
||||||
|
{ source: '/auth/callback', destination: '/api/auth/callback' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
Generated
+103
-4
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "week-4",
|
"name": "week-7",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "week-4",
|
"name": "week-7",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^5.5.1",
|
"@apollo/server": "^5.5.1",
|
||||||
"@as-integrations/next": "^4.1.0",
|
"@as-integrations/next": "^4.1.0",
|
||||||
"cors": "^2.8.6",
|
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"graphql": "^16.14.2",
|
"graphql": "^16.14.2",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"next": "^16.3.4",
|
"next": "^16.3.4",
|
||||||
"pg": "^8.23.0",
|
"pg": "^8.23.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
@@ -1198,6 +1198,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -1397,6 +1403,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -1677,6 +1692,91 @@
|
|||||||
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.sortby": {
|
"node_modules/lodash.sortby": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||||
@@ -2175,7 +2275,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "week-4",
|
"name": "week-7",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^5.5.1",
|
"@apollo/server": "^5.5.1",
|
||||||
"@as-integrations/next": "^4.1.0",
|
"@as-integrations/next": "^4.1.0",
|
||||||
"cors": "^2.8.6",
|
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"graphql": "^16.14.2",
|
"graphql": "^16.14.2",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"next": "^16.3.4",
|
"next": "^16.3.4",
|
||||||
"pg": "^8.23.0",
|
"pg": "^8.23.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
// Langkah 3-5 OAuth2: tukar code dengan access token, ambil profil, terbitkan JWT sendiri.
|
||||||
|
module.exports = async function handler(req, res) {
|
||||||
|
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, JWT_SECRET } = process.env;
|
||||||
|
if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET || !JWT_SECRET) {
|
||||||
|
res.status(500).json({ error: 'GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / JWT_SECRET belum diatur di environment' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, state } = req.query;
|
||||||
|
|
||||||
|
// state sekali pakai: hapus cookie apa pun hasilnya.
|
||||||
|
const expectedState = req.cookies.oauth_state;
|
||||||
|
res.setHeader('Set-Cookie', 'oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0');
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
res.status(400).json({ error: 'Parameter code tidak ada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state || !expectedState || state !== expectedState) {
|
||||||
|
res.status(400).json({ error: 'State tidak valid, ulangi login lewat /auth/login' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: GITHUB_CLIENT_ID,
|
||||||
|
client_secret: GITHUB_CLIENT_SECRET,
|
||||||
|
code,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const { access_token, error_description } = await tokenRes.json();
|
||||||
|
if (!access_token) {
|
||||||
|
res.status(401).json({ error: error_description || 'Gagal menukar code dengan access token' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRes = await fetch('https://api.github.com/user', {
|
||||||
|
headers: { Authorization: `Bearer ${access_token}`, Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
if (!userRes.ok) {
|
||||||
|
res.status(502).json({ error: 'Gagal mengambil profil dari GitHub' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const githubUser = await userRes.json();
|
||||||
|
|
||||||
|
const token = jwt.sign({ username: githubUser.login }, JWT_SECRET, { expiresIn: '1h' });
|
||||||
|
res.json({ token });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[OAuth error] /auth/callback:', err);
|
||||||
|
res.status(500).json({ error: 'Terjadi kesalahan pada server, silakan coba lagi' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
// Langkah 1 OAuth2: arahkan pengguna ke halaman login GitHub.
|
||||||
|
module.exports = function handler(req, res) {
|
||||||
|
const { GITHUB_CLIENT_ID, CALLBACK_URL } = process.env;
|
||||||
|
if (!GITHUB_CLIENT_ID || !CALLBACK_URL) {
|
||||||
|
res.status(500).json({ error: 'GITHUB_CLIENT_ID / CALLBACK_URL belum diatur di environment' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookie state hanya terkirim ke host yang sama dengan CALLBACK_URL, jadi login harus dimulai di host itu.
|
||||||
|
const callbackUrl = new URL(CALLBACK_URL);
|
||||||
|
if (req.headers.host !== callbackUrl.host) {
|
||||||
|
res.redirect(`${callbackUrl.origin}/auth/login`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// state acak disimpan di cookie lalu dicocokkan di /auth/callback (anti login-CSRF).
|
||||||
|
const state = crypto.randomBytes(16).toString('hex');
|
||||||
|
const secure = process.env.NODE_ENV === 'production' ? '; Secure' : '';
|
||||||
|
res.setHeader(
|
||||||
|
'Set-Cookie',
|
||||||
|
`oauth_state=${state}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600${secure}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: GITHUB_CLIENT_ID,
|
||||||
|
redirect_uri: CALLBACK_URL,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
res.redirect(`https://github.com/login/oauth/authorize?${params}`);
|
||||||
|
};
|
||||||
+44
-6
@@ -1,6 +1,7 @@
|
|||||||
const { ApolloServer } = require('@apollo/server');
|
const { ApolloServer } = require('@apollo/server');
|
||||||
const { startServerAndCreateNextHandler } = require('@as-integrations/next');
|
const { startServerAndCreateNextHandler } = require('@as-integrations/next');
|
||||||
const { GraphQLError } = require('graphql');
|
const { GraphQLError } = require('graphql');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
// 1. Inisialisasi Pool PostgreSQL untuk Serverless
|
// 1. Inisialisasi Pool PostgreSQL untuk Serverless
|
||||||
@@ -114,6 +115,37 @@ function validatePrice(price) {
|
|||||||
return price;
|
return price;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ambil user dari header "Authorization: Bearer <jwt>". Token hilang/salah/kedaluwarsa -> null.
|
||||||
|
function getUserFromRequest(req) {
|
||||||
|
const token = (req.headers.authorization || '').replace('Bearer ', '');
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dipakai di resolver mutation: tolak request tanpa JWT valid.
|
||||||
|
function requireAuth(contextValue) {
|
||||||
|
if (!contextValue.user) {
|
||||||
|
throw new GraphQLError('Unauthorized: silakan login terlebih dahulu', {
|
||||||
|
extensions: { code: 'UNAUTHENTICATED' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cari (atau buat) baris users untuk pengguna GitHub, supaya products.created_by bisa terisi.
|
||||||
|
// users.email wajib unik & NOT NULL, sedangkan email GitHub bisa privat -> pakai alamat noreply.
|
||||||
|
async function getOrCreateUserId(username) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO users (username, email) VALUES ($1, $2)
|
||||||
|
ON CONFLICT (username) DO UPDATE SET username = EXCLUDED.username
|
||||||
|
RETURNING id`,
|
||||||
|
[username, `${username}@users.noreply.github.com`]
|
||||||
|
);
|
||||||
|
return result.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
// Bungkus error tak terduga dari DB supaya tidak bocor ke client,
|
// Bungkus error tak terduga dari DB supaya tidak bocor ke client,
|
||||||
// tapi biarkan GraphQLError yang sudah kita buat sendiri lewat apa adanya.
|
// tapi biarkan GraphQLError yang sudah kita buat sendiri lewat apa adanya.
|
||||||
function handleUnexpectedError(err, context) {
|
function handleUnexpectedError(err, context) {
|
||||||
@@ -180,7 +212,8 @@ const resolvers = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createProduct: async (_, { input }) => {
|
createProduct: async (_, { input }, contextValue) => {
|
||||||
|
requireAuth(contextValue);
|
||||||
try {
|
try {
|
||||||
const title = validateTitle(input.title);
|
const title = validateTitle(input.title);
|
||||||
const price = validatePrice(input.price);
|
const price = validatePrice(input.price);
|
||||||
@@ -194,9 +227,11 @@ const resolvers = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userId = await getOrCreateUserId(contextValue.user.username);
|
||||||
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
'INSERT INTO products (title, price, category_id) VALUES ($1, $2, $3) RETURNING id, title, price::float, category_id, created_by',
|
'INSERT INTO products (title, price, category_id, created_by) VALUES ($1, $2, $3, $4) RETURNING id, title, price::float, category_id, created_by',
|
||||||
[title, price, categoryId]
|
[title, price, categoryId, userId]
|
||||||
);
|
);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -210,7 +245,8 @@ const resolvers = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProduct: async (_, { id, input }) => {
|
updateProduct: async (_, { id, input }, contextValue) => {
|
||||||
|
requireAuth(contextValue);
|
||||||
try {
|
try {
|
||||||
const productId = parsePositiveIntId(id, 'id');
|
const productId = parsePositiveIntId(id, 'id');
|
||||||
|
|
||||||
@@ -246,7 +282,8 @@ const resolvers = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteProduct: async (_, { id }) => {
|
deleteProduct: async (_, { id }, contextValue) => {
|
||||||
|
requireAuth(contextValue);
|
||||||
try {
|
try {
|
||||||
const productId = parsePositiveIntId(id, 'id');
|
const productId = parsePositiveIntId(id, 'id');
|
||||||
const result = await pool.query('DELETE FROM products WHERE id = $1 RETURNING id', [productId]);
|
const result = await pool.query('DELETE FROM products WHERE id = $1 RETURNING id', [productId]);
|
||||||
@@ -304,7 +341,8 @@ const server = new ApolloServer({
|
|||||||
|
|
||||||
// 6. Inisialisasi Handler dengan Context Per-Request
|
// 6. Inisialisasi Handler dengan Context Per-Request
|
||||||
const apolloHandler = startServerAndCreateNextHandler(server, {
|
const apolloHandler = startServerAndCreateNextHandler(server, {
|
||||||
context: async () => ({
|
context: async (req) => ({
|
||||||
|
user: getUserFromRequest(req),
|
||||||
metrics: {
|
metrics: {
|
||||||
categoryProductsCalls: 0,
|
categoryProductsCalls: 0,
|
||||||
callLogs: [],
|
callLogs: [],
|
||||||
|
|||||||
+3
-1
@@ -15,6 +15,8 @@ function Home() {
|
|||||||
target: '#embedded-sandbox',
|
target: '#embedded-sandbox',
|
||||||
initialEndpoint: `${window.location.origin}/api/graphql`,
|
initialEndpoint: `${window.location.origin}/api/graphql`,
|
||||||
initialState: {
|
initialState: {
|
||||||
|
// Ganti <paste-token-here> dengan JWT dari /auth/login supaya mutation diizinkan.
|
||||||
|
headers: { Authorization: 'Bearer <paste-token-here>' },
|
||||||
document: `query TestCategoriesWithProducts {
|
document: `query TestCategoriesWithProducts {
|
||||||
categories {
|
categories {
|
||||||
id
|
id
|
||||||
@@ -44,7 +46,7 @@ function Home() {
|
|||||||
React.createElement(
|
React.createElement(
|
||||||
Head,
|
Head,
|
||||||
null,
|
null,
|
||||||
React.createElement('title', null, 'GraphQL Sandbox - Lab 04')
|
React.createElement('title', null, 'GraphQL Sandbox')
|
||||||
),
|
),
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
id: 'embedded-sandbox',
|
id: 'embedded-sandbox',
|
||||||
|
|||||||
Reference in New Issue
Block a user