Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,918 changes: 2,918 additions & 0 deletions IMPLEMENT_CODE_DUMP.txt

Large diffs are not rendered by default.

1,025 changes: 353 additions & 672 deletions backend/package-lock.json

Large diffs are not rendered by default.

17 changes: 9 additions & 8 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
{
"name": "backoffice-backend",
"name": "backoffice",
"version": "0.0.1",
"description": "",
"main": "src/app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/app.js",
"dev": "nodemon src/app.js"
"start": "nodemon src/app.js"
},
"author": "ejjem",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-s3": "^3.758.0",
"@aws-sdk/s3-request-presigner": "^3.758.0",
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"express-rate-limit": "^7.4.0",
"fs": "^0.0.1-security",
"helmet": "^7.1.0",
"hpp": "^0.2.3",
"mongoose": "^8.12.2",
"ioredis": "^5.7.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.13.0",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"nodemon": "^3.1.4",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"passport-google-oauth20": "^2.0.0",
"process": "^0.11.10",
"rate-limit-redis": "^4.3.0",
"swagger-ui-express": "^5.0.1",
"winston": "^3.17.0"
"winston": "^3.19.0"
}
}
193 changes: 85 additions & 108 deletions backend/src/app.js
Original file line number Diff line number Diff line change
@@ -1,134 +1,89 @@
const express = require("express");
const app = express();
const morgan = require("morgan");
const winston = require("winston");

const cookieParser = require("cookie-parser");
const path = require("path");
const session = require("express-session");
const passport = require("passport");
const helmet = require("helmet");
const hpp = require("hpp");
const cors = require("cors");
const passport = require("passport");

require("dotenv").config({ path: path.resolve(__dirname, "../.env") });
const NODE_ENV = process.env.NODE_ENV;
console.log(`NODE_ENV = ${NODE_ENV}`);
const PORT = process.env.PORT;

const passportConfig = require("../src/passport");
passportConfig();
const app = express();
const NODE_ENV = process.env.NODE_ENV || "development";
const PORT = process.env.PORT || 4000;

[
"COOKIE_SECRET",
"ACCESS_TOKEN_SECRET",
"BO_BACKEND_URL",
"BO_FRONTEND_URL",
"BO_ALLOWED_ORIGINS",
"MONGO_URI",
"GOOGLE_CLIENT_ID",
"GOOGLE_CLIENT_SECRET",
"SERVER_SECRET_SALT",
].forEach((k) => {
if (!process.env[k]) throw new Error(`${k} is required`);
});

const { connectDB } = require("../src/models");
const loginRouter = require("../src/routes/login");
const memberRouter = require("../src/routes/member");
const seminaRouter = require("../src/routes/semina");
const featureRouter = require("../src/routes/feature");
const { connectMongo } = require("./config/mongo");
const { bootstrapSuperAdmin } = require("./services/superAdminBootstrap");
const { getRedisMode } = require("./services/redisClient");
const passportConfig = require("./passport");

const isProdOrTest = NODE_ENV === "production" || NODE_ENV === "test";
const PORT_NUMBER = Number(PORT) || 3001;
const memberRouter = require("./routes/member");
const seminaRouter = require("./routes/semina");
const featureRouter = require("./routes/feature");
const boAuthRouter = require("./routes/boAuth");
const boAdminUsersRouter = require("./routes/boAdminUsers");
const boAdminInvitesRouter = require("./routes/boAdminInvites");

const SESSION_SECRET = process.env.COOKIE_SECRET;
if (!SESSION_SECRET && isProdOrTest) {
throw new Error("COOKIE_SECRET 환경변수가 필요합니다.");
}
const safeSessionSecret = SESSION_SECRET || "dev-only-cookie-secret";

const sessionOption = {
resave: false,
saveUninitialized: false,
secret: safeSessionSecret,
cookie: {
maxAge: 1000 * 60 * 60 * 2,
httpOnly: true,
secure: isProdOrTest,
...(isProdOrTest && { sameSite: "None" }),
},
...(isProdOrTest && { proxy: true }),
};

app.use(cookieParser(safeSessionSecret));
app.use(session(sessionOption));
app.use(passport.initialize());
app.use(passport.session());
const swaggerUi = require("swagger-ui-express");
const swaggerDocument = require("./swagger.json");

// CORS와 CSRF(boAuth.js)가 동일 상수를 공유한다. 파싱 로직은 config/allowedOrigins.js에서 관리한다.
const allowedOrigins = require("./config/allowedOrigins");

app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(passport.initialize());

if (process.env.NODE_ENV === "development") {
app.use(
cors({
origin: process.env.CLIENT_ORIGIN_DEV,
methods: ["GET", "POST", "OPTIONS", "DELETE", "PATCH"],
credentials: true,
})
);
app.use(
cors({
origin: allowedOrigins,
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
credentials: true,
})
);

app.use(
helmet({
contentSecurityPolicy: false,
frameguard: { action: "deny" },
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
noSniff: true,
})
);

if (NODE_ENV === "development") {
app.use(morgan("dev"));
app.use(express.urlencoded({ extended: false }));
} else {
app.use(
cors({
origin: process.env.CLIENT_ORIGIN,
methods: ["GET", "POST", "PATCH", "OPTIONS"],
credentials: true,
})
);
app.enable("trust proxy");
app.use(morgan("combined"));
app.use(hpp());
app.use(express.urlencoded({ extended: false }));
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'none'"],
scriptSrc: ["'none'"],
styleSrc: ["'none'"],
frameSrc: ["'none'"],
},
})
);
app.use(helmet.frameguard({ action: "deny" }));
app.use(helmet.noSniff());
app.use(helmet.dnsPrefetchControl({ allow: false }));
app.use(helmet.hidePoweredBy());
app.use(helmet.referrerPolicy({ policy: "strict-origin-when-cross-origin" }));
}

const swaggerUi = require("swagger-ui-express");
const swaggerDocument = require("./swagger.json");

async function startServer() {
try {
await connectDB();
console.log("[LOG] MongoDB 연결 성공");

app.listen(PORT_NUMBER, () => {
console.log(`PORT: ${PORT_NUMBER}`);
console.log(`swagger: http://localhost:${PORT_NUMBER}/api-docs`);
console.log(`server: http://localhost:${PORT_NUMBER}`);
});
} catch (err) {
console.error("DB 연결 실패:", err);
process.exit(1);
}
}

startServer();

app.get("/", (req, res) => {
res.status(200).json({ message: "backoffice backend is running" });
});

app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" });
});

app.use("/bo/auth", loginRouter);
app.use("/bo/auth", boAuthRouter);
app.use("/bo/admin/users", boAdminUsersRouter);
app.use("/bo/admin/invites", boAdminInvitesRouter);
app.use("/bo/member", memberRouter);
app.use("/bo/semina", seminaRouter);
app.use("/bo/feature", featureRouter);
// 하위호환: 구버전 프론트가 /feature/* 를 호출하는 경우 지원
app.use("/feature", featureRouter);

if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") {
if (NODE_ENV === "development" || NODE_ENV === "test") {
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
}

Expand All @@ -138,15 +93,37 @@ const logger = winston.createLogger({
transports: [new winston.transports.File({ filename: "error.log" })],
});

app.use((err, req, res, next) => {
if (process.env.NODE_ENV === "development") {
console.log("[ERROR] error handler 동작");
app.use((err, _req, res, _next) => {
if (NODE_ENV === "development" || NODE_ENV === "test") {
console.error(err.stack || err);
} else {
logger.error(err.message || "Unexpected error");
}

res.status(err.status || 500).json({
error: { message: "Internal Server Error" },
code: "INTERNAL_ERROR",
message: "Internal Server Error",
});
});

async function startServer() {
try {
passportConfig();

await connectMongo();
await bootstrapSuperAdmin();

const rateLimiterMode = getRedisMode();
console.log(`[LOG] Rate limiter mode: ${rateLimiterMode}`);

app.listen(PORT, () => {
console.log(`PORT: ${PORT}`);
console.log(`server: http://localhost:${PORT}`);
});
} catch (err) {
console.error("Server start failed:", err);
process.exit(1);
}
}

startServer();
9 changes: 9 additions & 0 deletions backend/src/config/allowedOrigins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// BO_ALLOWED_ORIGINS는 서버 기동 시 한 번만 파싱하여 모듈 싱글턴으로 캐싱한다.
// CORS(app.js)와 CSRF(boAuth.js) 두 곳에서 동일 env를 각자 파싱하는 중복을 제거하고,
// 환경변수 이름이나 파싱 로직 변경 시 이 파일 한 곳만 수정하면 되도록 한다.
const ALLOWED_ORIGINS = (process.env.BO_ALLOWED_ORIGINS || "")
.split(",")
.map((v) => v.trim())
.filter(Boolean);

module.exports = ALLOWED_ORIGINS;
14 changes: 14 additions & 0 deletions backend/src/config/mongo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const mongoose = require("mongoose");

async function connectMongo() {
const uri = process.env.MONGO_URI;
if (!uri) throw new Error("MONGO_URI is required");

await mongoose.connect(uri, {
dbName: process.env.MONGO_DB_NAME || "quipu_backoffice",
});

console.log("[LOG] MongoDB connected");
}

module.exports = { connectMongo };
13 changes: 13 additions & 0 deletions backend/src/config/permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const Permission = {
READ: 1 << 0,
WRITE_ACTIVITY: 1 << 1,
WRITE_RECRUIT_FORM: 1 << 2,
WRITE_CLUB_INFO: 1 << 3,
};

const WRITE_ALL_MASK =
Permission.WRITE_ACTIVITY |
Permission.WRITE_RECRUIT_FORM |
Permission.WRITE_CLUB_INFO;

module.exports = { Permission, WRITE_ALL_MASK };
41 changes: 4 additions & 37 deletions backend/src/controllers/auth.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,4 @@
const passport = require('passport');

exports.login = (req, res, next) => {
passport.authenticate('local', (authError, user, info) => {
if (authError) {
console.error(authError);
return next(authError);
}
if (!user) {
return res
.status(401)
.send(`${info.message}`);
}
return req.login(user, (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res
.status(200)
.send('로그인 성공');
})
})(req, res, next);
}

exports.logout = (req, res) => {
req.logout((err) => {
if (err) {
return res
.status(500)
.json({message: '로그아웃 중 오류 발생'});
}
res
.status(200)
.json({message: '로그아웃 완료'});
});
}
// DEPRECATED: passport-local 기반 로그인/로그아웃 컨트롤러.
// Google OAuth + Authorization Bearer Token 방식으로 전환 완료.
// 이 파일은 더 이상 사용되지 않으며 삭제 예정입니다.
// 인증 관련 처리는 src/routes/boAuth.js 를 참조하세요.
30 changes: 16 additions & 14 deletions backend/src/controllers/getdata.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
const getData = (model) => async (req, res) => {
try {
const data = await model.find({}).lean();
try {
const data = await model.find({}).lean();
const rows = data.map(({ _id, __v, ...rest }) => rest);

const indexedData = data.map((item, index) => {
const { _id, ...rest } = item;
return {
index: index + 1,
...rest,
};
});
const indexedData = rows.map((item, index) => ({
index: index + 1,
...item
}));

res.status(200).json(indexedData);
} catch (err) {
console.log(err);
res.status(500).send("Server Error");
}
res
.status(200)
.json(indexedData);
} catch (err) {
console.log(err);
res
.status(500)
.send('Server Error');
}
};

module.exports = getData;
Loading