管理画面のヘッダーを更新し、タイトルを「神椿ライブスケジュール」に変更。ログアウト機能を追加し、Docker設定を修正。新しいログインページを作成し、環境変数を更新。
This commit is contained in:
parent
62d1e72ae9
commit
72031bba37
17
Dockerfile.production
Normal file
17
Dockerfile.production
Normal 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"]
|
@ -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:
|
@ -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:
|
||||
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 178 KiB |
108
src/app.ts
108
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) => {
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
@ -7,7 +7,7 @@
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<%- include("../partials/admin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>チャンネルを編集</h1>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<%- include("../partials/admin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>新しいチャンネルを追加</h1>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<%- include("../partials/admin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>チャンネル管理</h1>
|
||||
|
@ -6,13 +6,15 @@
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<%- include("../partials/admin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>管理画面</h1>
|
||||
<div class="admin-menu">
|
||||
<a href="/hyAdmin/channel" class="btn">チャンネル管理</a><br>
|
||||
<a href="/hyAdmin/schedule" class="btn">配信スケジュール管理</a>
|
||||
<br>
|
||||
<a href="/hyAdmin/logout" class="btn">ログアウト</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
29
views/admin/login.ejs
Normal file
29
views/admin/login.ejs
Normal 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>
|
@ -7,7 +7,7 @@
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<%- include("../partials/admin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>スケジュール編集</h1>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<%- include("../partials/admin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>配信スケジュール管理</h1>
|
||||
|
@ -3,7 +3,7 @@
|
||||
</head>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<a href="/hyAdmin">神椿配信スケジュール(管理画面)</a>
|
||||
<a href="/hyAdmin">神椿ライブスケジュール(管理画面)</a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a href="/" class="header-link">一般ページへ</a>
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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/errors.css">
|
||||
</head>
|
||||
|
@ -1,8 +1,9 @@
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<span>神椿配信スケジュール</span>
|
||||
<span>神椿ライブスケジュール</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a href="/terms" class="header-link">利用規約</a>
|
||||
<a href="https://forms.gle/uP7DsKR6HxzU9TKH7" class="header-link" target="_blank">お問い合わせ</a>
|
||||
</div>
|
||||
</header>
|
||||
|
Loading…
Reference in New Issue
Block a user