管理画面のヘッダーを更新し、タイトルを「神椿ライブスケジュール」に変更。ログアウト機能を追加し、Docker設定を修正。新しいログインページを作成し、環境変数を更新。

This commit is contained in:
ntki72 2025-01-03 21:46:33 +09:00
parent 62d1e72ae9
commit 72031bba37
17 changed files with 166 additions and 81 deletions

17
Dockerfile.production Normal file
View File

@ -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"]

View File

@ -1,5 +1,17 @@
services: services:
app: app:
build:
dockerfile: Dockerfile
volumes: volumes:
- ./public/css:/app/public/css - ./public/css:/app/public/css
- ./views:/app/views - ./views:/app/views
ports:
- "3000:3000"
environment:
NODE_ENV: development
db:
volumes:
- db-store:/var/lib/mysql
volumes:
db-store:

View File

@ -2,13 +2,13 @@ services:
app: app:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile.prodcution
ports: ports:
- "3000:3000" - "80:3000"
depends_on: depends_on:
- db - db
environment: environment:
NODE_ENV: development NODE_ENV: production
DB_NAME: ${DB_NAME} DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER} DB_USER: ${DB_USER}
DB_PASS: ${DB_PASS} DB_PASS: ${DB_PASS}
@ -16,11 +16,12 @@ services:
TZ: Asia/Tokyo TZ: Asia/Tokyo
volumes: volumes:
- ./public/thumbnails:/app/public/thumbnails - ./public/thumbnails:/app/public/thumbnails
- ./logs/app:/app/logs
db: db:
image: mysql:8.0 image: mysql:8.0
volumes: volumes:
- db-store:/var/lib/mysql - db-store-prodcution:/var/lib/mysql
- ./logs:/var/log/mysql - ./logs/db:/var/log/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
environment: environment:
MYSQL_DATABASE: ${DB_NAME} MYSQL_DATABASE: ${DB_NAME}
@ -32,5 +33,4 @@ services:
- ${DB_PORT}:3306 - ${DB_PORT}:3306
volumes: volumes:
db-store: db-store-prodcution:

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@ -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 cron from "node-cron";
import express from "express"; import express from "express";
import session from "express-session"; import session from "express-session";
@ -9,9 +9,6 @@ import { runMigrations } from "./utils/migrate";
import methodOverride from "method-override"; import methodOverride from "method-override";
import { updateAllLiveSchedules } from "./services/updateLiveSchedules"; import { updateAllLiveSchedules } from "./services/updateLiveSchedules";
// セッションとベーシック認証の有効期限
const SESSION_EXPIRATION = 60 * 1 * 1000; // テスト目的で1分に設定
const app = express(); const app = express();
const startServer = async () => { const startServer = async () => {
@ -19,66 +16,13 @@ const startServer = async () => {
// マイグレーション実行 // マイグレーション実行
await runMigrations(); await runMigrations();
// 開発中なので定期実行タスクをコメントアウト // 定期実行タスクを設定
// cron.schedule("0 * * * *", async () => { cron.schedule(CRON_SCHEDULE, async () => {
// console.log("Updating live schedules..."); console.log("Updating live schedules...");
// await updateAllLiveSchedules(); 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();
}); });
/** // ビューの設定
*
* 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("view engine", "ejs");
app.set("views", "./views"); app.set("views", "./views");
@ -89,6 +33,45 @@ const startServer = async () => {
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); 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); app.use("/hyAdmin", adminRoutes);
@ -99,6 +82,7 @@ const startServer = async () => {
app.use("/thumbnails", express.static("public/thumbnails")); app.use("/thumbnails", express.static("public/thumbnails"));
app.use("/css", express.static("public/css")); app.use("/css", express.static("public/css"));
app.use("/js", express.static("public/js")); app.use("/js", express.static("public/js"));
app.use("/favicon.ico", express.static("public/favicon.ico"));
// 404エラーハンドリング // 404エラーハンドリング
app.use((req, res, next) => { app.use((req, res, next) => {

View File

@ -8,11 +8,14 @@ const requiredEnvVars = [
"DB_USER", "DB_USER",
"DB_PASS", "DB_PASS",
"DB_HOST", "DB_HOST",
"BASIC_AUTH_USER", "BASIC_AUTH_ID",
"BASIC_AUTH_PASS", "BASIC_AUTH_PASS",
"WEB_PORT", "WEB_PORT",
"YOUTUBE_API_KEY", "YOUTUBE_API_KEY",
"SESSION_SECRET", "SESSION_SECRET",
"CRON_INTERVAL_MINUTES",
"ADMIN_ID",
"ADMIN_PASS",
]; ];
requiredEnvVars.forEach((varName) => { 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_NAME = process.env.DB_NAME as string;
export const DB_USER = process.env.DB_USER 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_PASS = process.env.DB_PASS as string;
export const DB_HOST = process.env.DB_HOST 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 BASIC_AUTH_PASS = process.env.BASIC_AUTH_PASS as string;
export const WEB_PORT = parseInt(process.env.WEB_PORT as string, 10); 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 YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY as string;
export const SESSION_SECRET = process.env.SESSION_SECRET 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;

View File

@ -7,6 +7,13 @@ import { updateLiveSchedulesForChannel } from "../services/liveScheduleService";
const router = Router(); const router = Router();
// ログアウト処理
router.get("/logout", (req, res) => {
req.session.destroy(() => {
res.redirect("/hyAdmin");
});
});
// 管理画面トップページ // 管理画面トップページ
router.get("/", (req, res) => { router.get("/", (req, res) => {
res.render("admin/index"); res.render("admin/index");
@ -214,7 +221,7 @@ router.put("/schedule/:id", async (req, res) => {
if (!schedule) { if (!schedule) {
return res.status(404).send("Schedule not found"); return res.status(404).send("Schedule not found");
} }
await schedule.update({ await schedule.update({
channel_id, channel_id,

View File

@ -7,7 +7,7 @@
</head> </head>
<%- include("../partials/head") %> <%- include("../partials/head") %>
<body> <body>
<%- include("../partials/hyAdmin/header") %> <%- include("../partials/admin/header") %>
<div class="header-spacer"></div> <div class="header-spacer"></div>
<div class="main-content"> <div class="main-content">
<h1>チャンネルを編集</h1> <h1>チャンネルを編集</h1>

View File

@ -7,7 +7,7 @@
</head> </head>
<%- include("../partials/head") %> <%- include("../partials/head") %>
<body> <body>
<%- include("../partials/hyAdmin/header") %> <%- include("../partials/admin/header") %>
<div class="header-spacer"></div> <div class="header-spacer"></div>
<div class="main-content"> <div class="main-content">
<h1>新しいチャンネルを追加</h1> <h1>新しいチャンネルを追加</h1>

View File

@ -7,7 +7,7 @@
</head> </head>
<%- include("../partials/head") %> <%- include("../partials/head") %>
<body> <body>
<%- include("../partials/hyAdmin/header") %> <%- include("../partials/admin/header") %>
<div class="header-spacer"></div> <div class="header-spacer"></div>
<div class="main-content"> <div class="main-content">
<h1>チャンネル管理</h1> <h1>チャンネル管理</h1>

View File

@ -6,13 +6,15 @@
</head> </head>
<%- include("../partials/head") %> <%- include("../partials/head") %>
<body> <body>
<%- include("../partials/hyAdmin/header") %> <%- include("../partials/admin/header") %>
<div class="header-spacer"></div> <div class="header-spacer"></div>
<div class="main-content"> <div class="main-content">
<h1>管理画面</h1> <h1>管理画面</h1>
<div class="admin-menu"> <div class="admin-menu">
<a href="/hyAdmin/channel" class="btn">チャンネル管理</a><br> <a href="/hyAdmin/channel" class="btn">チャンネル管理</a><br>
<a href="/hyAdmin/schedule" class="btn">配信スケジュール管理</a> <a href="/hyAdmin/schedule" class="btn">配信スケジュール管理</a>
<br>
<a href="/hyAdmin/logout" class="btn">ログアウト</a>
</div> </div>
</body> </body>
</html> </html>

29
views/admin/login.ejs Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<%- include("../partials/head") %>
<body>
<%- include("../partials/admin/header") %>
<div class="header-spacer"></div>
<div class="main-content">
<h1>管理画面ログイン</h1>
<% if (typeof error !== "undefined") { %>
<p class="error"><%= error %></p>
<% } %>
<form action="/hyAdmin/login" method="POST">
<div class="form-group">
<label for="username">ユーザー</label>
<input type="text" id="username" name="username" required />
</div>
<div class="form-group">
<label for="password">パスワード</label>
<input type="password" id="password" name="password" required />
</div>
<button type="submit" class="btn-login">ログイン</button>
</form>
</div>
</body>
</html>

View File

@ -7,7 +7,7 @@
</head> </head>
<%- include("../partials/head") %> <%- include("../partials/head") %>
<body> <body>
<%- include("../partials/hyAdmin/header") %> <%- include("../partials/admin/header") %>
<div class="header-spacer"></div> <div class="header-spacer"></div>
<div class="main-content"> <div class="main-content">
<h1>スケジュール編集</h1> <h1>スケジュール編集</h1>

View File

@ -7,7 +7,7 @@
</head> </head>
<%- include("../partials/head") %> <%- include("../partials/head") %>
<body> <body>
<%- include("../partials/hyAdmin/header") %> <%- include("../partials/admin/header") %>
<div class="header-spacer"></div> <div class="header-spacer"></div>
<div class="main-content"> <div class="main-content">
<h1>配信スケジュール管理</h1> <h1>配信スケジュール管理</h1>

View File

@ -3,7 +3,7 @@
</head> </head>
<header class="header"> <header class="header">
<div class="header-left"> <div class="header-left">
<a href="/hyAdmin">神椿配信スケジュール(管理画面)</a> <a href="/hyAdmin">神椿ライブスケジュール(管理画面)</a>
</div> </div>
<div class="header-right"> <div class="header-right">
<a href="/" class="header-link">一般ページへ</a> <a href="/" class="header-link">一般ページへ</a>

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>神椿配信スケジュール</title> <title>神椿ライブスケジュール</title>
<link rel="stylesheet" href="/css/header.css"> <link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/errors.css"> <link rel="stylesheet" href="/css/errors.css">
</head> </head>

View File

@ -1,8 +1,9 @@
<header class="header"> <header class="header">
<div class="header-left"> <div class="header-left">
<span>神椿配信スケジュール</span> <span>神椿ライブスケジュール</span>
</div> </div>
<div class="header-right"> <div class="header-right">
<a href="/terms" class="header-link">利用規約</a> <a href="/terms" class="header-link">利用規約</a>
<a href="https://forms.gle/uP7DsKR6HxzU9TKH7" class="header-link" target="_blank">お問い合わせ</a>
</div> </div>
</header> </header>