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:
+44
-6
@@ -1,6 +1,7 @@
|
||||
const { ApolloServer } = require('@apollo/server');
|
||||
const { startServerAndCreateNextHandler } = require('@as-integrations/next');
|
||||
const { GraphQLError } = require('graphql');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { Pool } = require('pg');
|
||||
|
||||
// 1. Inisialisasi Pool PostgreSQL untuk Serverless
|
||||
@@ -114,6 +115,37 @@ function validatePrice(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,
|
||||
// tapi biarkan GraphQLError yang sudah kita buat sendiri lewat apa adanya.
|
||||
function handleUnexpectedError(err, context) {
|
||||
@@ -180,7 +212,8 @@ const resolvers = {
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
createProduct: async (_, { input }) => {
|
||||
createProduct: async (_, { input }, contextValue) => {
|
||||
requireAuth(contextValue);
|
||||
try {
|
||||
const title = validateTitle(input.title);
|
||||
const price = validatePrice(input.price);
|
||||
@@ -194,9 +227,11 @@ const resolvers = {
|
||||
});
|
||||
}
|
||||
|
||||
const userId = await getOrCreateUserId(contextValue.user.username);
|
||||
|
||||
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',
|
||||
[title, price, categoryId]
|
||||
'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, userId]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (err) {
|
||||
@@ -210,7 +245,8 @@ const resolvers = {
|
||||
}
|
||||
},
|
||||
|
||||
updateProduct: async (_, { id, input }) => {
|
||||
updateProduct: async (_, { id, input }, contextValue) => {
|
||||
requireAuth(contextValue);
|
||||
try {
|
||||
const productId = parsePositiveIntId(id, 'id');
|
||||
|
||||
@@ -246,7 +282,8 @@ const resolvers = {
|
||||
}
|
||||
},
|
||||
|
||||
deleteProduct: async (_, { id }) => {
|
||||
deleteProduct: async (_, { id }, contextValue) => {
|
||||
requireAuth(contextValue);
|
||||
try {
|
||||
const productId = parsePositiveIntId(id, 'id');
|
||||
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
|
||||
const apolloHandler = startServerAndCreateNextHandler(server, {
|
||||
context: async () => ({
|
||||
context: async (req) => ({
|
||||
user: getUserFromRequest(req),
|
||||
metrics: {
|
||||
categoryProductsCalls: 0,
|
||||
callLogs: [],
|
||||
|
||||
Reference in New Issue
Block a user