diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..ae6d64c --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,5 @@ +services: + app: + volumes: + - ./public/css:/app/public/css + - ./views:/app/views diff --git a/docker-compose.yml b/docker-compose.yml index 8a615aa..232bc1e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,6 @@ services: TZ: Asia/Tokyo volumes: - ./public/thumbnails:/app/public/thumbnails - - ./public/css:/app/public/css db: image: mysql:8.0 volumes: diff --git a/package-lock.json b/package-lock.json index c0d4d35..fe70278 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "ejs": "^3.1.10", "express": "^4.18.2", "express-basic-auth": "^1.2.0", + "express-session": "^1.18.1", "method-override": "^3.0.0", "mysql2": "^3.3.3", "node-cron": "^3.0.3", @@ -22,6 +23,7 @@ }, "devDependencies": { "@types/express": "^4.17.17", + "@types/express-session": "^1.18.1", "@types/method-override": "^3.0.0", "@types/node": "^20.4.2", "@types/node-cron": "^3.0.11", @@ -117,6 +119,16 @@ "@types/send": "*" } }, + "node_modules/@types/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -977,6 +989,40 @@ "basic-auth": "^2.0.1" } }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -1753,6 +1799,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -1857,6 +1912,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2439,6 +2503,18 @@ "node": ">=14.17" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", diff --git a/package.json b/package.json index a765f22..7794875 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "ejs": "^3.1.10", "express": "^4.18.2", "express-basic-auth": "^1.2.0", + "express-session": "^1.18.1", "method-override": "^3.0.0", "mysql2": "^3.3.3", "node-cron": "^3.0.3", @@ -21,6 +22,7 @@ }, "devDependencies": { "@types/express": "^4.17.17", + "@types/express-session": "^1.18.1", "@types/method-override": "^3.0.0", "@types/node": "^20.4.2", "@types/node-cron": "^3.0.11", diff --git a/public/css/header.css b/public/css/header.css index 65a76e3..ca9f16f 100644 --- a/public/css/header.css +++ b/public/css/header.css @@ -4,45 +4,36 @@ left: 0; width: 100%; height: 60px; - background-color: #333; + background-color: #1a1a1a; color: white; display: flex; justify-content: space-between; align-items: center; padding: 0 20px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); z-index: 1000; box-sizing: border-box; } -.header-left a { +.header-left span { color: white; - font-size: 1.5em; + font-size: 1.6em; font-weight: bold; text-decoration: none; - white-space: nowrap; } .header-right { display: flex; - gap: 20px; - margin-right: 10px; - white-space: nowrap; + gap: 15px; } .header-link { - color: white; + color: #ddd; text-decoration: none; font-size: 1em; transition: color 0.2s; } .header-link:hover { - color: #ddd; -} - -@media (max-width: 768px) { - .header-right { - margin-right: 5px; - } + color: white; } diff --git a/public/css/hyAdmin/common.css b/public/css/hyAdmin/common.css new file mode 100644 index 0000000..66d5691 --- /dev/null +++ b/public/css/hyAdmin/common.css @@ -0,0 +1,211 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; +} + +.main-content { + padding: 20px; +} + +h1 { + text-align: center; + margin-bottom: 20px; +} + +.add-channel { + text-align: center; + margin-bottom: 20px; +} + +.add-channel .btn { + display: inline-block; + padding: 10px 20px; + background-color: #007BFF; + color: #fff; + text-decoration: none; + border-radius: 5px; + transition: background-color 0.3s ease; +} + +.add-channel .btn:hover { + background-color: #0056b3; +} + +.admin-table { + width: 100%; + border-collapse: collapse; + margin: 0 auto; +} + +.admin-table th, .admin-table td { + border: 1px solid #ddd; + padding: 10px; + text-align: center; +} + +.admin-table th { + background-color: #f4f4f4; +} + +.action-buttons { + display: flex; + flex-direction: column; + gap: 10px; +} + +.btn { + display: block; + padding: 5px 10px; + text-align: center; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.btn-edit { + background-color: #FFC107; + color: #fff; +} + +.btn-edit:hover { + background-color: #d39e00; +} + +.btn-delete { + background-color: #DC3545; + color: #fff; +} + +.btn-delete:hover { + background-color: #a71d2a; +} + +.btn-run { + background-color: #28A745; + color: #fff; +} + +.btn-run:hover { + background-color: #1e7e34; +} + +.btn-fetch { + background-color: #17A2B8; + color: #fff; +} + +.btn-fetch:hover { + background-color: #117a8b; +} + +form label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +.date-group { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 10px; +} + +.date-group label { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +form input { + margin-top: 5px; + padding: 5px; + width: 100%; + box-sizing: border-box; +} + +.admin-form { + max-width: 500px; + margin: 0 auto; + padding: 20px; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #f9f9f9; +} + +.admin-form .form-group { + margin-bottom: 15px; +} + +.admin-form .form-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +.admin-form .form-group input { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +.admin-form .form-note { + font-size: 0.9em; + color: #666; + margin-bottom: 15px; + text-align: center; +} + +.admin-form .form-actions { + display: flex; + justify-content: space-between; +} + +.admin-form .btn { + display: inline-block; + padding: 10px 15px; + text-align: center; + text-decoration: none; + border-radius: 4px; + font-weight: bold; + cursor: pointer; +} + +.admin-form .btn-add { + background-color: #28A745; + color: #fff; +} + +.admin-form .btn-add:hover { + background-color: #218838; +} + +.admin-form .btn-update { + background-color: #007BFF; + color: #fff; +} + +.admin-form .btn-update:hover { + background-color: #0056b3; +} + +.admin-form .btn-back { + background-color: #6C757D; + color: #fff; +} + +.admin-form .btn-back:hover { + background-color: #5A6268; +} + +.header-spacer { + height: 60px; +} + +.main-content { + margin-top: 20px; +} diff --git a/public/css/hyAdmin/header.css b/public/css/hyAdmin/header.css new file mode 100644 index 0000000..a9516b5 --- /dev/null +++ b/public/css/hyAdmin/header.css @@ -0,0 +1,48 @@ +.header { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 60px; + background-color: #ff4b4b; + color: white; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + z-index: 1000; + box-sizing: border-box; +} + +.header-left a { + color: white; + font-size: 1.5em; + font-weight: bold; + text-decoration: none; + white-space: nowrap; +} + +.header-right { + display: flex; + gap: 20px; + margin-right: 10px; + white-space: nowrap; +} + +.header-link { + color: white; + text-decoration: none; + font-size: 1em; + transition: color 0.2s; +} + +.header-link:hover { + color: #ddd; +} + +@media (max-width: 768px) { + .header-right { + margin-right: 5px; + } +} diff --git a/public/css/schedule.css b/public/css/schedule.css index 4048a79..3a69824 100644 --- a/public/css/schedule.css +++ b/public/css/schedule.css @@ -1,13 +1,28 @@ +body { + font-family: 'Noto Sans JP', Arial, sans-serif; + background-color: #f0f0f0; + color: #333; + margin: 0; + padding: 0; +} + .schedule-container { - width: 80%; + width: 100%; + max-width: 1200px; margin: 0 auto; + padding: 20px; + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .date-header { - font-size: 1.5em; - margin: 20px 0; + font-size: 1.8em; font-weight: bold; - min-width: 550px; + color: #222; + margin: 20px 0; + border-bottom: 2px solid #333; + padding-bottom: 10px; } .banners { @@ -15,19 +30,20 @@ flex-wrap: wrap; gap: 20px; } + .banner { display: flex; flex-direction: row; align-items: center; text-decoration: none; color: black; - background-color: #f9f9f9; + background-color: #ffffff; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; flex: 1 1 calc(50% - 13px); max-width: calc(50% - 13px); - min-width: 550px; + min-width: 550px; /* デフォルトはPC向け */ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: transform 0.2s, box-shadow 0.2s; } @@ -35,12 +51,25 @@ .banner:hover { transform: scale(1.02); box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); + border-color: #444; } +/* 1時間以内のバナーを強調表示 */ +.banner.highlight-live { + background-color: #ffe5e5; /* 明るい赤系 */ + border-color: #ff4d4d; /* 濃い赤 */ +} + +.banner.highlight-live .details { + color: #a10000; /* 赤系の文字色 */ +} + +/* サムネイル部分 */ .thumbnail img { width: 150px; height: auto; object-fit: cover; + border-right: 1px solid #ddd; } .details { @@ -57,7 +86,7 @@ .title { font-size: 1.2em; font-weight: bold; - color: #555; + color: #222; } .header-spacer { @@ -65,5 +94,14 @@ } .main-content { - margin-top: 20px; + background-color: #f9f9f9; +} + +/* スマホや縦長画面の場合 */ +@media (max-width: 768px) { + .banner { + flex: 1 1 100%; /* 横幅を画面いっぱいにする */ + max-width: 100%; + min-width: 100%; + } } diff --git a/public/css/terms.css b/public/css/terms.css index 73b0359..f35cd21 100644 --- a/public/css/terms.css +++ b/public/css/terms.css @@ -1,47 +1,69 @@ body { - font-family: Arial, sans-serif; - margin: 0; - padding: 0; - } - - .terms-container { - max-width: 800px; - margin: 80px auto; /* ヘッダーの高さを考慮して調整 */ - padding: 20px; - background-color: #ffffff; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - } - - h1 { - font-size: 2em; - margin-bottom: 20px; - text-align: center; - color: #333333; - } - - h2 { - font-size: 1.5em; - margin-top: 20px; - margin-bottom: 10px; - color: #444444; - border-bottom: 2px solid #ddd; - padding-bottom: 5px; - } - - p { - font-size: 1em; - line-height: 1.6; - margin-bottom: 15px; - color: #555555; - } - - a { - color: #0066cc; - text-decoration: none; - } - - a:hover { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; +} + +.terms-container { + max-width: 800px; + margin: 80px auto; + padding: 20px; + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +h1 { + font-size: 2em; + margin-bottom: 20px; + text-align: center; + color: #333333; +} + +h2 { + font-size: 1.5em; + margin-top: 20px; + margin-bottom: 10px; + color: #444444; + border-bottom: 2px solid #ddd; + padding-bottom: 5px; +} + +p { + font-size: 1em; + line-height: 1.6; + margin-bottom: 15px; + color: #555555; +} + +a { + color: #0066cc; + text-decoration: none; +} + +a:hover { text-decoration: underline; } - \ No newline at end of file + +.back-to-top { + margin-top: 30px; + text-align: center; +} + +.btn-back { + display: inline-block; + padding: 10px 20px; + background-color: #444; + color: #fff; + text-decoration: none; + border-radius: 5px; + font-size: 1em; + transition: background-color 0.3s ease, transform 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.btn-back:hover { + background-color: #222; + transform: scale(1.05); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); +} diff --git a/src/app.ts b/src/app.ts index af761d1..364c936 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,14 +1,16 @@ -import { WEB_PORT, BASIC_AUTH_USER, BASIC_AUTH_PASS } from "./config/env"; +import { WEB_PORT, BASIC_AUTH_USER, BASIC_AUTH_PASS, SESSION_SECRET } from "./config/env"; import cron from "node-cron"; import express from "express"; -import apiRoutes from "./routes/api"; +import session from "express-session"; import adminRoutes from "./routes/admin"; import userRoutes from "./routes/user"; import basicAuth from "express-basic-auth"; import { runMigrations } from "./utils/migrate"; import methodOverride from "method-override"; import { updateAllLiveSchedules } from "./services/updateLiveSchedules"; -import scheduleRoutes from "./routes/schedule"; + +// セッションとベーシック認証の有効期限 +const SESSION_EXPIRATION = 60 * 1 * 1000; // テスト目的で1分に設定 const app = express(); @@ -17,22 +19,65 @@ const startServer = async () => { // マイグレーション実行 await runMigrations(); - // // 定期実行タスクを設定(1時間ごと) + // 開発中なので定期実行タスクをコメントアウト // cron.schedule("0 * * * *", async () => { // console.log("Updating live schedules..."); // await updateAllLiveSchedules(); // }); - - // ベーシック認証 + // セッション設定 app.use( - "/admin", - basicAuth({ - users: { [BASIC_AUTH_USER]: BASIC_AUTH_PASS }, - challenge: true, + 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("views", "./views"); @@ -45,24 +90,22 @@ const startServer = async () => { app.use(express.urlencoded({ extended: true })); // 管理画面ルート - app.use("/admin", adminRoutes); + app.use("/hyAdmin", adminRoutes); // ユーザー向けルート app.use("/", userRoutes); - // 管理画面 API - app.use("/admin/api", apiRoutes); - // 静的ファイルの提供 app.use("/thumbnails", express.static("public/thumbnails")); app.use("/css", express.static("public/css")); app.use("/js", express.static("public/js")); - // エラーハンドリング + // 404エラーハンドリング app.use((req, res, next) => { res.status(404).render("errors/404"); }); + // その他エラーハンドリング app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error("Unhandled error:", err); diff --git a/src/config/env.ts b/src/config/env.ts index 331ebe9..39897c1 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -12,6 +12,7 @@ const requiredEnvVars = [ "BASIC_AUTH_PASS", "WEB_PORT", "YOUTUBE_API_KEY", + "SESSION_SECRET", ]; requiredEnvVars.forEach((varName) => { @@ -28,3 +29,4 @@ export const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER 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; diff --git a/src/routes/admin.ts b/src/routes/admin.ts index a714aee..ceee321 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -1,17 +1,22 @@ +import dayjs from "dayjs"; import { Router } from "express"; -import { Channel } from "../models"; +import { Channel, LiveSchedule } from "../models"; import { getChannelIdByHandle, fetchPastLiveStreams } from "../utils/youtube"; import { saveLiveSchedules } from "../services/liveScheduleService"; import { updateLiveSchedulesForChannel } from "../services/liveScheduleService"; - const router = Router(); +// 管理画面トップページ +router.get("/", (req, res) => { + res.render("admin/index"); +}); + // チャンネル一覧表示 -router.get("/channels", async (req, res) => { +router.get("/channel", async (req, res) => { try { const channels = await Channel.findAll(); - res.render("admin/channels", { title: "Channels", channels }); + res.render("admin/channel", { channels }); } catch (error) { console.error(error); res.status(500).send("Failed to load channels"); @@ -19,12 +24,12 @@ router.get("/channels", async (req, res) => { }); // チャンネル登録フォームの表示 -router.get("/channels/new", (req, res) => { - res.render("admin/new-channel", { title: "Add New Channel" }); +router.get("/channel/new", (req, res) => { + res.render("admin/channel-new"); }); // チャンネル登録処理 -router.post("/channels/new", async (req, res) => { +router.post("/channel/new", async (req, res) => { try { const { name, channel_handle, youtube_id } = req.body; @@ -58,7 +63,7 @@ router.post("/channels/new", async (req, res) => { youtube_id: resolvedYoutubeId, }); - res.redirect("/admin/channels"); + res.redirect("/hyAdmin/channel"); } catch (error) { console.error("Failed to create channel:", error); res.status(500).send("An error occurred while creating the channel."); @@ -67,7 +72,7 @@ router.post("/channels/new", async (req, res) => { // チャンネル編集フォームの表示 -router.get("/channels/:id/edit", async (req, res) => { +router.get("/channel/:id/edit", async (req, res) => { try { const { id } = req.params; const channel = await Channel.findByPk(id); @@ -76,7 +81,7 @@ router.get("/channels/:id/edit", async (req, res) => { return res.status(404).send("Channel not found"); } - res.render("admin/edit-channel", { title: "Edit Channel", channel }); + res.render("admin/channel-edit", { channel }); } catch (error) { console.error(error); res.status(500).send("Failed to load channel for editing"); @@ -84,7 +89,7 @@ router.get("/channels/:id/edit", async (req, res) => { }); // チャンネル更新処理 -router.put("/channels/:id", async (req, res) => { +router.put("/channel/:id", async (req, res) => { try { const { id } = req.params; const { name, youtube_id } = req.body; @@ -98,7 +103,7 @@ router.put("/channels/:id", async (req, res) => { channel.youtube_id = youtube_id; await channel.save(); - res.redirect("/admin/channels"); + res.redirect("/hyAdmin/channel"); } catch (error) { console.error(error); res.status(500).send("Failed to update channel"); @@ -106,7 +111,7 @@ router.put("/channels/:id", async (req, res) => { }); // チャンネル削除処理 -router.delete("/channels/:id", async (req, res) => { +router.delete("/channel/:id", async (req, res) => { try { const { id } = req.params; const channel = await Channel.findByPk(id); @@ -116,7 +121,7 @@ router.delete("/channels/:id", async (req, res) => { } await channel.destroy(); - res.redirect("/admin/channels"); + res.redirect("/hyAdmin/channel"); } catch (error) { console.error(error); res.status(500).send("Failed to delete channel"); @@ -124,7 +129,7 @@ router.delete("/channels/:id", async (req, res) => { }); // チャンネルの配信スケジュールを更新 -router.get("/channels/:id/run", async (req, res) => { +router.get("/channel/:id/run", async (req, res) => { try { const channel = await Channel.findByPk(req.params.id); @@ -134,7 +139,9 @@ router.get("/channels/:id/run", async (req, res) => { await updateLiveSchedulesForChannel(channel); - res.send(`Live schedules successfully updated for channel: ${channel.name}`); + res.send( + "" + ); } catch (error) { console.error("Error updating live schedules for channel:", error); res.status(500).send("Failed to update live schedules for this channel."); @@ -142,7 +149,7 @@ router.get("/channels/:id/run", async (req, res) => { }); // 配信履歴取得 -router.post("/channels/:id/fetch-history", async (req, res) => { +router.post("/channel/:id/fetch-history", async (req, res) => { try { const channelId = req.params.id; const { startDate, endDate } = req.body; @@ -159,11 +166,84 @@ router.post("/channels/:id/fetch-history", async (req, res) => { const historyStreams = await fetchPastLiveStreams(channel.youtube_id, startDate, endDate); await saveLiveSchedules(channel.id!, historyStreams); - res.json({ message: "History fetched and saved successfully." }); + res.send( + "" + ); } catch (error) { console.error("Failed to fetch history:", error); res.status(500).json({ error: "Failed to fetch history." }); } }); +// LiveSchedules 一覧表示 +router.get("/schedule", async (req, res) => { + try { + const liveSchedules = await LiveSchedule.findAll({ + include: [{ model: Channel }], + order: [["start_time", "ASC"]], + }); + res.render("admin/schedule", { liveSchedules, dayjs }); + } catch (error) { + console.error("Failed to fetch live schedules:", error); + res.status(500).send("Internal Server Error"); + } +}); + +// LiveSchedule 編集画面 +router.get("/schedule/:id/edit", async (req, res) => { + try { + const schedule = await LiveSchedule.findByPk(req.params.id); + const channels = await Channel.findAll(); + + if (!schedule) { + return res.status(404).send("Schedule not found"); + } + + res.render("admin/schedule-edit", { schedule, channels, dayjs }); + } catch (error) { + console.error("Failed to fetch schedule:", error); + res.status(500).send("Internal Server Error"); + } +}); + +// LiveSchedule 更新処理 +router.put("/schedule/:id", async (req, res) => { + try { + const { channel_id, title, start_time } = req.body; + const schedule = await LiveSchedule.findByPk(req.params.id); + + if (!schedule) { + return res.status(404).send("Schedule not found"); + } + + await schedule.update({ + channel_id, + title, + start_time: new Date(start_time), + }); + + res.redirect("/hyAdmin/schedule"); + } catch (error) { + console.error("Failed to update schedule:", error); + res.status(500).send("Internal Server Error"); + } +}); + +// LiveSchedule 削除 +router.delete("/schedule/:id", async (req, res) => { + try { + const schedule = await LiveSchedule.findByPk(req.params.id); + + if (!schedule) { + return res.status(404).send("Schedule not found"); + } + + await schedule.destroy(); + res.redirect("/hyAdmin/schedule"); + } catch (error) { + console.error("Failed to delete schedule:", error); + res.status(500).send("Internal Server Error"); + } +}); + export default router; diff --git a/src/routes/api.ts b/src/routes/api.ts deleted file mode 100644 index 9ea17ef..0000000 --- a/src/routes/api.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Router } from "express"; -import { Channel, LiveSchedule } from "../models"; // Sequelize モデルをインポート - -const router = Router(); - -// チャンネル一覧を取得 -router.get("/channels", async (req, res) => { - try { - const channels = await Channel.findAll(); - res.json(channels); - } catch (error) { - res.status(500).json({ error: "Failed to fetch channels" }); - } -}); - -// // チャンネルを登録 -// router.post("/channels", async (req, res) => { -// try { -// const { name, youtube_id } = req.body; -// const newChannel = await Channel.create({ name, youtube_id }); -// res.status(201).json(newChannel); -// } catch (error) { -// res.status(400).json({ error: "Failed to create channel" }); -// } -// }); - -// // チャンネルを更新 -// router.put("/channels/:id", async (req, res) => { -// try { -// const { id } = req.params; -// const { name, youtube_id } = req.body; -// const channel = await Channel.findByPk(id); - -// if (!channel) { -// return res.status(404).json({ error: "Channel not found" }); -// } - -// channel.name = name; -// channel.youtube_id = youtube_id; -// await channel.save(); - -// res.json(channel); -// } catch (error) { -// res.status(400).json({ error: "Failed to update channel" }); -// } -// }); - -// // チャンネルを削除 -// router.delete("/channels/:id", async (req, res) => { -// try { -// const { id } = req.params; -// const channel = await Channel.findByPk(id); - -// if (!channel) { -// return res.status(404).json({ error: "Channel not found" }); -// } - -// await channel.destroy(); -// res.status(204).send(); -// } catch (error) { -// res.status(500).json({ error: "Failed to delete channel" }); -// } -// }); - -// ライブスケジュール一覧を取得 -router.get("/live-schedules", async (req, res) => { - try { - const schedules = await LiveSchedule.findAll({ include: [Channel] }); - res.json(schedules); - } catch (error) { - res.status(500).json({ error: "Failed to fetch live schedules" }); - } -}); - -export default router; diff --git a/src/types/express-session.d.ts b/src/types/express-session.d.ts new file mode 100644 index 0000000..33d36e6 --- /dev/null +++ b/src/types/express-session.d.ts @@ -0,0 +1,7 @@ +import "express-session"; + +declare module "express-session" { + interface SessionData { + isAuthenticated?: boolean; + } +} diff --git a/tsconfig.json b/tsconfig.json index 7e67e14..46b752d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,6 @@ "esModuleInterop": true, "skipLibCheck": true }, - "include": ["src/**/*"], + "include": ["src/**/*.ts", "src/types/**/*.d.ts"], "exclude": ["node_modules"] } diff --git a/views/admin/channel-edit.ejs b/views/admin/channel-edit.ejs new file mode 100644 index 0000000..0399622 --- /dev/null +++ b/views/admin/channel-edit.ejs @@ -0,0 +1,34 @@ + + + + + + + + <%- include("../partials/head") %> + + <%- include("../partials/hyAdmin/header") %> +
+
+

チャンネルを編集

+
+
+ + +
+
+ + +
+
+ + +
+
+ + 戻る +
+
+
+ + diff --git a/views/admin/channel-new.ejs b/views/admin/channel-new.ejs new file mode 100644 index 0000000..bc6c05e --- /dev/null +++ b/views/admin/channel-new.ejs @@ -0,0 +1,35 @@ + + + + + + + + <%- include("../partials/head") %> + + <%- include("../partials/hyAdmin/header") %> +
+
+

新しいチャンネルを追加

+
+
+ + +
+
+ + +
+
+ + +
+

注意: 「チャンネルハンドル」または「YouTube ID」のいずれかを必ず入力してください。

+
+ + 戻る +
+
+
+ + diff --git a/views/admin/channel.ejs b/views/admin/channel.ejs new file mode 100644 index 0000000..7eb5b51 --- /dev/null +++ b/views/admin/channel.ejs @@ -0,0 +1,66 @@ + + + + + + + + <%- include("../partials/head") %> + + <%- include("../partials/hyAdmin/header") %> +
+
+

チャンネル管理

+
+ 新しいチャンネルを追加 +
+ + + + + + + + + + + + <% channels.forEach(channel => { %> + + + + + + + + <% }) %> + +
IDチャンネル名ハンドルYouTube ID操作
<%= channel.id %><%= channel.name %><%= channel.channel_handle %><%= channel.youtube_id %> +
+
+ +
+
+ +
+
+ +
+
+
+ + + +
+
+
+
+
+ + diff --git a/views/admin/channels.ejs b/views/admin/channels.ejs deleted file mode 100644 index 25d8c4b..0000000 --- a/views/admin/channels.ejs +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - <%- include("../partials/head") %> - -

Channel List

- Add New Channel - - - - - - - - - - - - <% channels.forEach(channel => { %> - - - - - - - - <% }) %> - -
IDNameChannel HandleYouTube IDActions
<%= channel.id %><%= channel.name %><%= channel.channel_handle %><%= channel.youtube_id %> -
- -
-
- -
-
- -
-
- - - -
-
- - diff --git a/views/admin/edit-channel.ejs b/views/admin/edit-channel.ejs deleted file mode 100644 index e82a2f2..0000000 --- a/views/admin/edit-channel.ejs +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - <%- include("../partials/head") %> - -

<%= title %>

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

<%= title %>

-

Welcome to the Admin Dashboard!

+ <%- include("../partials/hyAdmin/header") %> +
+
+

管理画面

+
+ チャンネル管理
+ 配信スケジュール管理 +
diff --git a/views/admin/new-channel.ejs b/views/admin/new-channel.ejs deleted file mode 100644 index 23a2674..0000000 --- a/views/admin/new-channel.ejs +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - <%- include("../partials/head") %> - -

Add New Channel

-
- - -
- - -
- - -
-

Note: Either 'Channel Handle' or 'YouTube ID' must be provided.

- -
- Back to Channel List - - diff --git a/views/admin/schedule-edit.ejs b/views/admin/schedule-edit.ejs new file mode 100644 index 0000000..edf5426 --- /dev/null +++ b/views/admin/schedule-edit.ejs @@ -0,0 +1,38 @@ + + + + + + + + <%- include("../partials/head") %> + + <%- include("../partials/hyAdmin/header") %> +
+
+

スケジュール編集

+
+
+ + +
+
+ + +
+
+ + " required> +
+
+ + キャンセル +
+
+
+ + diff --git a/views/admin/schedule.ejs b/views/admin/schedule.ejs new file mode 100644 index 0000000..6ee68c7 --- /dev/null +++ b/views/admin/schedule.ejs @@ -0,0 +1,45 @@ + + + + + + + + <%- include("../partials/head") %> + + <%- include("../partials/hyAdmin/header") %> +
+
+

配信スケジュール管理

+ + + + + + + + + + + + <% liveSchedules.forEach(schedule => { %> + + + + + + + + <% }) %> + +
IDチャンネル名タイトル配信開始時刻操作
<%= schedule.id %><%= schedule.Channel ? schedule.Channel.name : "N/A" %><%= schedule.title %><%= dayjs(schedule.start_time).tz("Asia/Tokyo").format("YYYY-MM-DD HH:mm") %> +
+ +
+
+ +
+
+
+ + diff --git a/views/admin/test-result.ejs b/views/admin/test-result.ejs deleted file mode 100644 index c3c3b06..0000000 --- a/views/admin/test-result.ejs +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - <%- include("../partials/head") %> - -

<%= title %>

-

Channel: <%= channel.name %> (<%= channel.youtube_id %>)

-

Live Streams

- <% if (liveStreams.length === 0) { %> -

No upcoming live streams found.

- <% } else { %> - - <% } %> - Back to Channel List - - diff --git a/views/errors/400.ejs b/views/errors/400.ejs index 004cb13..05e3641 100644 --- a/views/errors/400.ejs +++ b/views/errors/400.ejs @@ -3,14 +3,17 @@ - <%- include("../partials/head") %> -
-

400

-

送信されたリクエストに問題がありました。

- トップページへ戻る + <%- include("../partials/header") %> +
+
+
+

400

+

送信されたリクエストに問題がありました。

+ トップページへ戻る +
diff --git a/views/errors/403.ejs b/views/errors/403.ejs index c1cd6cd..443ef2c 100644 --- a/views/errors/403.ejs +++ b/views/errors/403.ejs @@ -3,14 +3,17 @@ - <%- include("../partials/head") %> -
-

403

-

このページへのアクセスが拒否されました。

- トップページへ戻る + <%- include("../partials/header") %> +
+
+
+

403

+

このページへのアクセスが拒否されました。

+ トップページへ戻る +
diff --git a/views/errors/404.ejs b/views/errors/404.ejs index 1236b83..6617865 100644 --- a/views/errors/404.ejs +++ b/views/errors/404.ejs @@ -3,14 +3,17 @@ - <%- include("../partials/head") %> -
-

404

-

お探しのページが見つかりませんでした。

- トップページへ戻る + <%- include("../partials/header") %> +
+
+
+

404

+

お探しのページが見つかりませんでした。

+ トップページへ戻る +
diff --git a/views/errors/500.ejs b/views/errors/500.ejs index 48e45c3..a4ad784 100644 --- a/views/errors/500.ejs +++ b/views/errors/500.ejs @@ -3,14 +3,17 @@ - <%- include("../partials/head") %> -
-

500

-

予期せぬエラーが発生しました。少し時間をおいて再度お試しください。

- トップページへ戻る + <%- include("../partials/header") %> +
+
+
+

500

+

予期せぬエラーが発生しました。少し時間をおいて再度お試しください。

+ トップページへ戻る +
diff --git a/views/errors/503.ejs b/views/errors/503.ejs index 73c2c58..466a65e 100644 --- a/views/errors/503.ejs +++ b/views/errors/503.ejs @@ -3,14 +3,17 @@ - <%- include("../partials/head") %> -
-

503

-

現在サーバーが一時的に利用できません。時間をおいて再度お試しください。

- トップページへ戻る + <%- include("../partials/header") %> +
+
+
+

503

+

現在サーバーが一時的に利用できません。時間をおいて再度お試しください。

+ トップページへ戻る +
diff --git a/views/partials/header.ejs b/views/partials/header.ejs index 072e833..d9f0e90 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -1,6 +1,6 @@
- 神椿配信スケジュール + 神椿配信スケジュール
利用規約 diff --git a/views/partials/hyAdmin/header.ejs b/views/partials/hyAdmin/header.ejs new file mode 100644 index 0000000..479a747 --- /dev/null +++ b/views/partials/hyAdmin/header.ejs @@ -0,0 +1,11 @@ + + + +
+ + +
diff --git a/views/schedule.ejs b/views/schedule.ejs index 510b78d..5408491 100644 --- a/views/schedule.ejs +++ b/views/schedule.ejs @@ -4,7 +4,6 @@ - <%- include("partials/head") %> @@ -19,7 +18,13 @@
<% groupedSchedules[date].forEach(schedule => { %> -