diff --git a/Dockerfile.production b/Dockerfile.production new file mode 100644 index 0000000..08dacef --- /dev/null +++ b/Dockerfile.production @@ -0,0 +1,17 @@ +FROM node:22.12.0-alpine + +WORKDIR /app + +# パッケージファイルをコピーしてインストール +COPY package*.json ./ +RUN npm install --production + +# ソースコードをコピーしてビルド +COPY . . +RUN npm run build + +COPY wait-for-it.sh /wait-for-it.sh +RUN chmod +x /wait-for-it.sh + +# シェルスクリプトを使って、MySQL サーバーの起動を待機 +CMD ["/bin/sh", "/wait-for-it.sh", "db", "3306", "--", "npm", "run", "start"] diff --git a/docker-compose.override.yml b/docker-compose.override.yml index ae6d64c..4ad4b21 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,5 +1,17 @@ services: app: + build: + dockerfile: Dockerfile volumes: - ./public/css:/app/public/css - ./views:/app/views + ports: + - "3000:3000" + environment: + NODE_ENV: development + db: + volumes: + - db-store:/var/lib/mysql + +volumes: + db-store: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 232bc1e..662f206 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,13 +2,13 @@ services: app: build: context: . - dockerfile: Dockerfile + dockerfile: Dockerfile.prodcution ports: - - "3000:3000" + - "80:3000" depends_on: - db environment: - NODE_ENV: development + NODE_ENV: production DB_NAME: ${DB_NAME} DB_USER: ${DB_USER} DB_PASS: ${DB_PASS} @@ -16,11 +16,12 @@ services: TZ: Asia/Tokyo volumes: - ./public/thumbnails:/app/public/thumbnails + - ./logs/app:/app/logs db: image: mysql:8.0 volumes: - - db-store:/var/lib/mysql - - ./logs:/var/log/mysql + - db-store-prodcution:/var/lib/mysql + - ./logs/db:/var/log/mysql - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf environment: MYSQL_DATABASE: ${DB_NAME} @@ -32,5 +33,4 @@ services: - ${DB_PORT}:3306 volumes: - db-store: - + db-store-prodcution: diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..eee849b Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/app.ts b/src/app.ts index 364c936..7fc97b1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { WEB_PORT, BASIC_AUTH_USER, BASIC_AUTH_PASS, SESSION_SECRET } from "./config/env"; +import { WEB_PORT, BASIC_AUTH_ID, BASIC_AUTH_PASS, SESSION_SECRET, CRON_SCHEDULE, ADMIN_ID, ADMIN_PASS, SESSION_EXPIRATION } from "./config/env"; import cron from "node-cron"; import express from "express"; import session from "express-session"; @@ -9,9 +9,6 @@ import { runMigrations } from "./utils/migrate"; import methodOverride from "method-override"; import { updateAllLiveSchedules } from "./services/updateLiveSchedules"; -// セッションとベーシック認証の有効期限 -const SESSION_EXPIRATION = 60 * 1 * 1000; // テスト目的で1分に設定 - const app = express(); const startServer = async () => { @@ -19,66 +16,13 @@ const startServer = async () => { // マイグレーション実行 await runMigrations(); - // 開発中なので定期実行タスクをコメントアウト - // cron.schedule("0 * * * *", async () => { - // console.log("Updating live schedules..."); - // await updateAllLiveSchedules(); - // }); - - // セッション設定 - app.use( - session({ - secret: SESSION_SECRET, - resave: false, - saveUninitialized: false, - rolling: false, - cookie: { maxAge: SESSION_EXPIRATION, secure: false, httpOnly: true }, - }) - ); - - // キャッシュを抑止 - app.use((req, res, next) => { - res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); - res.setHeader("Pragma", "no-cache"); - res.setHeader("Expires", "0"); - next(); + // 定期実行タスクを設定 + cron.schedule(CRON_SCHEDULE, async () => { + console.log("Updating live schedules..."); + await updateAllLiveSchedules(); }); - /** - * 管理画面へのアクセス制限 - * 1. セッションが有効期限内かどうかチェック - * 2. 期限切れなら 401 を返す → ブラウザは再度Basic認証を要求 - * 3. 成功したらセッションを再生成する - */ - app.use("/hyAdmin", (req, res, next) => { - const sessionExpires = req.session.cookie.expires; - - // セッションが有効期限内の場合はそのまま進む - if (req.session.isAuthenticated && sessionExpires && sessionExpires > new Date()) { - return next(); - } - - // セッションが切れた(または存在しない)場合、 - // Basic認証を実行して認証に成功すればセッション再生成、失敗すれば401を返す - basicAuth({ - users: { [BASIC_AUTH_USER]: BASIC_AUTH_PASS }, - challenge: true, // 401時にWWW-Authenticateヘッダーを付加 - unauthorizedResponse: "Unauthorized", - })(req, res, (authError) => { - // authError があればそのままエラーに - if (authError) { - return next(authError); - } - - // 認証が通ったのでセッション再生成 - req.session.isAuthenticated = true; - req.session.cookie.expires = new Date(Date.now() + SESSION_EXPIRATION); - - return next(); - }); - }); - - // 管理画面ビューの設定 + // ビューの設定 app.set("view engine", "ejs"); app.set("views", "./views"); @@ -89,6 +33,45 @@ const startServer = async () => { app.use(express.json()); app.use(express.urlencoded({ extended: true })); + // セッション設定 + app.use( + session({ + secret: SESSION_SECRET, + resave: false, + saveUninitialized: false, + rolling: true, + cookie: { maxAge: SESSION_EXPIRATION, secure: false, httpOnly: true }, + }) + ); + + // 管理画面ベーシック認証 + app.use( + "/hyAdmin", + basicAuth({ + users: { [BASIC_AUTH_ID]: BASIC_AUTH_PASS }, + challenge: true, + }) + ); + + // 管理画面認証ミドルウェア + app.use("/hyAdmin", (req, res, next) => { + if (req.session.isAuthenticated) { + return next(); + } + + if (req.method === "POST" && req.url === "/login") { + const { username, password } = req.body; + if (username === ADMIN_ID && password === ADMIN_PASS) { + req.session.isAuthenticated = true; + req.session.cookie.expires = new Date(Date.now() + SESSION_EXPIRATION); + return res.redirect("/hyAdmin"); + } + return res.render("admin/login", { error: "Invalid credentials" }); + } + + res.render("admin/login"); + }); + // 管理画面ルート app.use("/hyAdmin", adminRoutes); @@ -99,6 +82,7 @@ const startServer = async () => { app.use("/thumbnails", express.static("public/thumbnails")); app.use("/css", express.static("public/css")); app.use("/js", express.static("public/js")); + app.use("/favicon.ico", express.static("public/favicon.ico")); // 404エラーハンドリング app.use((req, res, next) => { diff --git a/src/config/env.ts b/src/config/env.ts index 39897c1..2f5e8f1 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -8,11 +8,14 @@ const requiredEnvVars = [ "DB_USER", "DB_PASS", "DB_HOST", - "BASIC_AUTH_USER", + "BASIC_AUTH_ID", "BASIC_AUTH_PASS", "WEB_PORT", "YOUTUBE_API_KEY", "SESSION_SECRET", + "CRON_INTERVAL_MINUTES", + "ADMIN_ID", + "ADMIN_PASS", ]; requiredEnvVars.forEach((varName) => { @@ -21,12 +24,42 @@ requiredEnvVars.forEach((varName) => { } }); +export const SESSION_EXPIRATION = 60 * 1 * 1000; + export const DB_NAME = process.env.DB_NAME as string; export const DB_USER = process.env.DB_USER as string; export const DB_PASS = process.env.DB_PASS as string; export const DB_HOST = process.env.DB_HOST as string; -export const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER as string; +export const BASIC_AUTH_ID = process.env.BASIC_AUTH_ID as string; export const BASIC_AUTH_PASS = process.env.BASIC_AUTH_PASS as string; export const WEB_PORT = parseInt(process.env.WEB_PORT as string, 10); export const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY as string; export const SESSION_SECRET = process.env.SESSION_SECRET as string; +export const ADMIN_ID = process.env.ADMIN_ID as string; +export const ADMIN_PASS = process.env.ADMIN_PASS as string; + +// cron のスケジュールを計算 +const intervalMinutes = parseInt(process.env.CRON_INTERVAL_MINUTES || "60", 10); +if (isNaN(intervalMinutes) || intervalMinutes <= 0) { + throw new Error("Invalid CRON_INTERVAL_MINUTES value in .env file. Must be a positive number."); +} + +let cronSchedule = ""; +if (intervalMinutes <= 60) { + // 60分以下の場合、分単位で設定 + cronSchedule = `*/${intervalMinutes} * * * *`; +} else { + // 60分を超える場合、時間と分に分割 + const hours = Math.floor(intervalMinutes / 60); + const minutes = intervalMinutes % 60; + + if (minutes === 0) { + // 分が0の場合 + cronSchedule = `0 */${hours} * * *`; + } else { + // 分が0以外の場合 + cronSchedule = `${minutes} */${hours} * * *`; + } +} + +export const CRON_SCHEDULE = cronSchedule; diff --git a/src/routes/admin.ts b/src/routes/admin.ts index ceee321..61cf81c 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -7,6 +7,13 @@ import { updateLiveSchedulesForChannel } from "../services/liveScheduleService"; const router = Router(); +// ログアウト処理 +router.get("/logout", (req, res) => { + req.session.destroy(() => { + res.redirect("/hyAdmin"); + }); +}); + // 管理画面トップページ router.get("/", (req, res) => { res.render("admin/index"); @@ -214,7 +221,7 @@ router.put("/schedule/:id", async (req, res) => { if (!schedule) { return res.status(404).send("Schedule not found"); - } + } await schedule.update({ channel_id, diff --git a/views/admin/channel-edit.ejs b/views/admin/channel-edit.ejs index 0399622..6a9e086 100644 --- a/views/admin/channel-edit.ejs +++ b/views/admin/channel-edit.ejs @@ -7,7 +7,7 @@ <%- include("../partials/head") %> - <%- include("../partials/hyAdmin/header") %> + <%- include("../partials/admin/header") %>

チャンネルを編集

diff --git a/views/admin/channel-new.ejs b/views/admin/channel-new.ejs index bc6c05e..82bf7b1 100644 --- a/views/admin/channel-new.ejs +++ b/views/admin/channel-new.ejs @@ -7,7 +7,7 @@ <%- include("../partials/head") %> - <%- include("../partials/hyAdmin/header") %> + <%- include("../partials/admin/header") %>

新しいチャンネルを追加

diff --git a/views/admin/channel.ejs b/views/admin/channel.ejs index 7eb5b51..d7d2f18 100644 --- a/views/admin/channel.ejs +++ b/views/admin/channel.ejs @@ -7,7 +7,7 @@ <%- include("../partials/head") %> - <%- include("../partials/hyAdmin/header") %> + <%- include("../partials/admin/header") %>

チャンネル管理

diff --git a/views/admin/index.ejs b/views/admin/index.ejs index 598dc30..fbb9be1 100644 --- a/views/admin/index.ejs +++ b/views/admin/index.ejs @@ -6,13 +6,15 @@ <%- include("../partials/head") %> - <%- include("../partials/hyAdmin/header") %> + <%- include("../partials/admin/header") %>

管理画面

diff --git a/views/admin/login.ejs b/views/admin/login.ejs new file mode 100644 index 0000000..dfb1c8e --- /dev/null +++ b/views/admin/login.ejs @@ -0,0 +1,29 @@ + + + + + + + <%- include("../partials/head") %> + + <%- include("../partials/admin/header") %> +
+
+

管理画面ログイン

+ <% if (typeof error !== "undefined") { %> +

<%= error %>

+ <% } %> +
+
+ + +
+
+ + +
+ +
+
+ + diff --git a/views/admin/schedule-edit.ejs b/views/admin/schedule-edit.ejs index edf5426..dffead9 100644 --- a/views/admin/schedule-edit.ejs +++ b/views/admin/schedule-edit.ejs @@ -7,7 +7,7 @@ <%- include("../partials/head") %> - <%- include("../partials/hyAdmin/header") %> + <%- include("../partials/admin/header") %>

スケジュール編集

diff --git a/views/admin/schedule.ejs b/views/admin/schedule.ejs index 6ee68c7..d138e7f 100644 --- a/views/admin/schedule.ejs +++ b/views/admin/schedule.ejs @@ -7,7 +7,7 @@ <%- include("../partials/head") %> - <%- include("../partials/hyAdmin/header") %> + <%- include("../partials/admin/header") %>

配信スケジュール管理

diff --git a/views/partials/hyAdmin/header.ejs b/views/partials/admin/header.ejs similarity index 75% rename from views/partials/hyAdmin/header.ejs rename to views/partials/admin/header.ejs index 479a747..a830d80 100644 --- a/views/partials/hyAdmin/header.ejs +++ b/views/partials/admin/header.ejs @@ -3,7 +3,7 @@
一般ページへ diff --git a/views/partials/head.ejs b/views/partials/head.ejs index 61ef705..48448c7 100644 --- a/views/partials/head.ejs +++ b/views/partials/head.ejs @@ -3,7 +3,7 @@ - 神椿配信スケジュール + 神椿ライブスケジュール diff --git a/views/partials/header.ejs b/views/partials/header.ejs index d9f0e90..1362d51 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -1,8 +1,9 @@
- 神椿配信スケジュール + 神椿ライブスケジュール