docker-composeの設定追加、TypeScriptの型定義更新、管理画面用のヘッダー追加、エラーページの修正、CSSスタイルの調整
This commit is contained in:
parent
29cebe239f
commit
62d1e72ae9
5
docker-compose.override.yml
Normal file
5
docker-compose.override.yml
Normal file
@ -0,0 +1,5 @@
|
||||
services:
|
||||
app:
|
||||
volumes:
|
||||
- ./public/css:/app/public/css
|
||||
- ./views:/app/views
|
@ -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:
|
||||
|
76
package-lock.json
generated
76
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
211
public/css/hyAdmin/common.css
Normal file
211
public/css/hyAdmin/common.css
Normal file
@ -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;
|
||||
}
|
48
public/css/hyAdmin/header.css
Normal file
48
public/css/hyAdmin/header.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ body {
|
||||
|
||||
.terms-container {
|
||||
max-width: 800px;
|
||||
margin: 80px auto; /* ヘッダーの高さを考慮して調整 */
|
||||
margin: 80px auto;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
@ -45,3 +45,25 @@ body {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
73
src/app.ts
73
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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
"<script>alert('スケジュールを取得しました。'); window.location.href = '/hyAdmin/channel';</script>"
|
||||
);
|
||||
} 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(
|
||||
"<script>alert('過去の配信情報を取得しました。'); window.location.href = '/hyAdmin/channel';</script>"
|
||||
);
|
||||
} 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;
|
||||
|
@ -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;
|
7
src/types/express-session.d.ts
vendored
Normal file
7
src/types/express-session.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import "express-session";
|
||||
|
||||
declare module "express-session" {
|
||||
interface SessionData {
|
||||
isAuthenticated?: boolean;
|
||||
}
|
||||
}
|
@ -8,6 +8,6 @@
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"include": ["src/**/*.ts", "src/types/**/*.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
34
views/admin/channel-edit.ejs
Normal file
34
views/admin/channel-edit.ejs
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/hyAdmin/common.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>チャンネルを編集</h1>
|
||||
<form action="/hyAdmin/channel/<%= channel.id %>?_method=PUT" method="POST" class="admin-form">
|
||||
<div class="form-group">
|
||||
<label for="name">チャンネル名:</label>
|
||||
<input type="text" id="name" name="name" value="<%= channel.name %>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="channel_handle">チャンネルハンドル:</label>
|
||||
<input type="text" id="channel_handle" name="channel_handle" value="<%= channel.channel_handle %>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="youtube_id">YouTube ID:</label>
|
||||
<input type="text" id="youtube_id" name="youtube_id" value="<%= channel.youtube_id %>" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-update">チャンネルを更新</button>
|
||||
<a href="/hyAdmin/channel" class="btn btn-back">戻る</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
35
views/admin/channel-new.ejs
Normal file
35
views/admin/channel-new.ejs
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/hyAdmin/common.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>新しいチャンネルを追加</h1>
|
||||
<form action="/hyAdmin/channel/new" method="POST" class="admin-form">
|
||||
<div class="form-group">
|
||||
<label for="name">チャンネル名:</label>
|
||||
<input type="text" id="name" name="name" placeholder="例: Example Channel" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="channel_handle">チャンネルハンドル (例: @example):</label>
|
||||
<input type="text" id="channel_handle" name="channel_handle" placeholder="例: @example">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="youtube_id">YouTube ID (例: UC_xxx...):</label>
|
||||
<input type="text" id="youtube_id" name="youtube_id" placeholder="例: UC_xxx...">
|
||||
</div>
|
||||
<p class="form-note">注意: 「チャンネルハンドル」または「YouTube ID」のいずれかを必ず入力してください。</p>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-add">チャンネルを追加</button>
|
||||
<a href="/hyAdmin/channel" class="btn btn-back">戻る</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
66
views/admin/channel.ejs
Normal file
66
views/admin/channel.ejs
Normal file
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/hyAdmin/common.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>チャンネル管理</h1>
|
||||
<div class="add-channel">
|
||||
<a href="/hyAdmin/channel/new" class="btn">新しいチャンネルを追加</a>
|
||||
</div>
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>チャンネル名</th>
|
||||
<th>ハンドル</th>
|
||||
<th>YouTube ID</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% channels.forEach(channel => { %>
|
||||
<tr>
|
||||
<td><%= channel.id %></td>
|
||||
<td><%= channel.name %></td>
|
||||
<td><%= channel.channel_handle %></td>
|
||||
<td><%= channel.youtube_id %></td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<form action="/hyAdmin/channel/<%= channel.id %>/edit" method="GET">
|
||||
<button type="submit" class="btn btn-edit">編集</button>
|
||||
</form>
|
||||
<form action="/hyAdmin/channel/<%= channel.id %>?_method=DELETE" method="POST">
|
||||
<button type="submit" class="btn btn-delete" onclick="return confirm('本当にこのチャンネルを削除しますか?');">削除</button>
|
||||
</form>
|
||||
<form action="/hyAdmin/channel/<%= channel.id %>/run" method="GET">
|
||||
<button type="submit" class="btn btn-run">スケジュール取得</button>
|
||||
</form>
|
||||
<form action="/hyAdmin/channel/<%= channel.id %>/fetch-history" method="POST" class="history-form">
|
||||
<div class="date-group">
|
||||
<label>
|
||||
開始日:
|
||||
<input type="date" name="startDate" required>
|
||||
</label>
|
||||
<label>
|
||||
終了日:
|
||||
<input type="date" name="endDate" required>
|
||||
</label>
|
||||
<button type="submit" class="btn btn-fetch">履歴取得</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,49 +0,0 @@
|
||||
<!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>
|
||||
<h1>Channel List</h1>
|
||||
<a href="/admin/channels/new">Add New Channel</a>
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Channel Handle</th>
|
||||
<th>YouTube ID</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% channels.forEach(channel => { %>
|
||||
<tr>
|
||||
<td><%= channel.id %></td>
|
||||
<td><%= channel.name %></td>
|
||||
<td><%= channel.channel_handle %></td>
|
||||
<td><%= channel.youtube_id %></td>
|
||||
<td>
|
||||
<form action="/admin/channels/<%= channel.id %>/edit" method="GET" style="display:inline;">
|
||||
<button type="submit">Edit</button>
|
||||
</form>
|
||||
<form action="/admin/channels/<%= channel.id %>?_method=DELETE" method="POST" style="display:inline;">
|
||||
<button type="submit" onclick="return confirm('Are you sure you want to delete this channel?');">Delete</button>
|
||||
</form>
|
||||
<form action="/admin/channels/<%= channel.id %>/run" method="GET" style="display:inline;">
|
||||
<button type="submit">Run</button>
|
||||
</form>
|
||||
<form action="/admin/channels/<%= channel.id %>/fetch-history" method="POST" style="display:inline;">
|
||||
<input type="date" name="startDate" required>
|
||||
<input type="date" name="endDate" required>
|
||||
<button type="submit">Fetch History</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@ -1,23 +0,0 @@
|
||||
<!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>
|
||||
<h1><%= title %></h1>
|
||||
<form action="/admin/channels/<%= channel.id %>?_method=PUT" method="POST">
|
||||
<label for="name">Channel Name:</label>
|
||||
<input type="text" id="name" name="name" value="<%= channel.name %>" required>
|
||||
<br>
|
||||
<label for="channel_handle">Channel Handle:</label>
|
||||
<input type="text" id="channel_handle" name="channel_handle" value="<%= channel.channel_handle %>" required>
|
||||
<br>
|
||||
<label for="youtube_id">YouTube ID:</label>
|
||||
<input type="text" id="youtube_id" name="youtube_id" value="<%= channel.youtube_id %>" required>
|
||||
<br>
|
||||
<button type="submit">Update Channel</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
@ -6,7 +6,13 @@
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<h1><%= title %></h1>
|
||||
<p>Welcome to the Admin Dashboard!</p>
|
||||
<%- include("../partials/hyAdmin/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>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,25 +0,0 @@
|
||||
<!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>
|
||||
<h1>Add New Channel</h1>
|
||||
<form action="/admin/channels/new" method="POST">
|
||||
<label for="name">Channel Name (Optional):</label>
|
||||
<input type="text" id="name" name="name">
|
||||
<br>
|
||||
<label for="channel_handle">Channel Handle (e.g., @example):</label>
|
||||
<input type="text" id="channel_handle" name="channel_handle">
|
||||
<br>
|
||||
<label for="youtube_id">YouTube ID (e.g., UC_xxx...):</label>
|
||||
<input type="text" id="youtube_id" name="youtube_id">
|
||||
<br>
|
||||
<p>Note: Either 'Channel Handle' or 'YouTube ID' must be provided.</p>
|
||||
<button type="submit">Add Channel</button>
|
||||
</form>
|
||||
<a href="/admin/channels">Back to Channel List</a>
|
||||
</body>
|
||||
</html>
|
38
views/admin/schedule-edit.ejs
Normal file
38
views/admin/schedule-edit.ejs
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/hyAdmin/common.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>スケジュール編集</h1>
|
||||
<form action="/hyAdmin/schedule/<%= schedule.id %>?_method=PUT" method="POST" class="admin-form">
|
||||
<div class="form-group">
|
||||
<label for="channel_id">チャンネル名:</label>
|
||||
<select id="channel_id" name="channel_id" required>
|
||||
<% channels.forEach(channel => { %>
|
||||
<option value="<%= channel.id %>" <%= schedule.channel_id === channel.id ? "selected" : "" %>><%= channel.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="title">タイトル:</label>
|
||||
<input type="text" id="title" name="title" value="<%= schedule.title %>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="start_time">配信開始時刻:</label>
|
||||
<input type="datetime-local" id="start_time" name="start_time" value="<%= dayjs(schedule.start_time).tz("Asia/Tokyo").format("YYYY-MM-DDTHH:mm") %>" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-update">更新</button>
|
||||
<a href="/hyAdmin/schedule" class="btn btn-back">キャンセル</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
45
views/admin/schedule.ejs
Normal file
45
views/admin/schedule.ejs
Normal file
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/hyAdmin/common.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/hyAdmin/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<h1>配信スケジュール管理</h1>
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>チャンネル名</th>
|
||||
<th>タイトル</th>
|
||||
<th>配信開始時刻</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% liveSchedules.forEach(schedule => { %>
|
||||
<tr>
|
||||
<td><%= schedule.id %></td>
|
||||
<td><%= schedule.Channel ? schedule.Channel.name : "N/A" %></td>
|
||||
<td><%= schedule.title %></td>
|
||||
<td><%= dayjs(schedule.start_time).tz("Asia/Tokyo").format("YYYY-MM-DD HH:mm") %></td>
|
||||
<td>
|
||||
<form action="/hyAdmin/schedule/<%= schedule.id %>/edit" method="GET" style="display:inline;">
|
||||
<button class="btn btn-edit">編集</button>
|
||||
</form>
|
||||
<form action="/hyAdmin/schedule/<%= schedule.id %>?_method=DELETE" method="POST" style="display:inline;">
|
||||
<button class="btn btn-delete" onclick="return confirm('本当に削除しますか?');">削除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,27 +0,0 @@
|
||||
<!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>
|
||||
<h1><%= title %></h1>
|
||||
<h2>Channel: <%= channel.name %> (<%= channel.youtube_id %>)</h2>
|
||||
<h3>Live Streams</h3>
|
||||
<% if (liveStreams.length === 0) { %>
|
||||
<p>No upcoming live streams found.</p>
|
||||
<% } else { %>
|
||||
<ul>
|
||||
<% liveStreams.forEach(stream => { %>
|
||||
<li>
|
||||
<strong>Title:</strong> <%= stream.title %><br>
|
||||
<strong>Start Time:</strong> <%= new Date(stream.start_time).toLocaleString() %><br>
|
||||
<strong>Thumbnail:</strong> <img src="<%= stream.thumbnail_url %>" alt="Thumbnail" width="200">
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } %>
|
||||
<a href="/admin/channels">Back to Channel List</a>
|
||||
</body>
|
||||
</html>
|
@ -3,14 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/errors.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<div class="error-container">
|
||||
<h1>400</h1>
|
||||
<p>送信されたリクエストに問題がありました。</p>
|
||||
<a href="/" class="home-link">トップページへ戻る</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,14 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/errors.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<div class="error-container">
|
||||
<h1>403</h1>
|
||||
<p>このページへのアクセスが拒否されました。</p>
|
||||
<a href="/" class="home-link">トップページへ戻る</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,14 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/errors.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<div class="error-container">
|
||||
<h1>404</h1>
|
||||
<p>お探しのページが見つかりませんでした。</p>
|
||||
<a href="/" class="home-link">トップページへ戻る</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,14 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/errors.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<div class="error-container">
|
||||
<h1>500</h1>
|
||||
<p>予期せぬエラーが発生しました。少し時間をおいて再度お試しください。</p>
|
||||
<a href="/" class="home-link">トップページへ戻る</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,14 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/errors.css">
|
||||
</head>
|
||||
<%- include("../partials/head") %>
|
||||
<body>
|
||||
<%- include("../partials/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<div class="error-container">
|
||||
<h1>503</h1>
|
||||
<p>現在サーバーが一時的に利用できません。時間をおいて再度お試しください。</p>
|
||||
<a href="/" class="home-link">トップページへ戻る</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<a href="/">神椿配信スケジュール</a>
|
||||
<span>神椿配信スケジュール</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a href="/terms" class="header-link">利用規約</a>
|
||||
|
11
views/partials/hyAdmin/header.ejs
Normal file
11
views/partials/hyAdmin/header.ejs
Normal file
@ -0,0 +1,11 @@
|
||||
<head>
|
||||
<link rel="stylesheet" href="/css/hyAdmin/header.css">
|
||||
</head>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<a href="/hyAdmin">神椿配信スケジュール(管理画面)</a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a href="/" class="header-link">一般ページへ</a>
|
||||
</div>
|
||||
</header>
|
@ -4,7 +4,6 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/schedule.css">
|
||||
<link rel="stylesheet" href="/css/header.css">
|
||||
</head>
|
||||
<%- include("partials/head") %>
|
||||
<body>
|
||||
@ -19,7 +18,13 @@
|
||||
</div>
|
||||
<div class="banners">
|
||||
<% groupedSchedules[date].forEach(schedule => { %>
|
||||
<a href="https://www.youtube.com/watch?v=<%= schedule.video_id %>" class="banner" target="_blank">
|
||||
<%
|
||||
const timeDifference = dayjs(schedule.start_time).diff(dayjs(), 'minute');
|
||||
const isLive = timeDifference < 5 && timeDifference >= -60; // 配信開始5時間前~1時間後
|
||||
%>
|
||||
<a href="https://www.youtube.com/watch?v=<%= schedule.video_id %>"
|
||||
class="banner <%= isLive ? 'highlight-live' : '' %>"
|
||||
target="_blank">
|
||||
<div class="thumbnail">
|
||||
<img src="<%= schedule.thumbnail_url || '/default-thumbnail.jpg' %>" alt="Thumbnail">
|
||||
</div>
|
||||
|
@ -3,12 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/header.css">
|
||||
<link rel="stylesheet" href="/css/terms.css">
|
||||
</head>
|
||||
<%- include("partials/head") %>
|
||||
<body>
|
||||
<%- include("partials/header") %>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="main-content">
|
||||
<div class="terms-container">
|
||||
<h1>利用規約</h1>
|
||||
<section>
|
||||
@ -41,6 +42,10 @@
|
||||
本サイトに関するお問い合わせは、管理者宛てにご連絡ください。ただし、返信や対応を保証するものではありません。
|
||||
</p>
|
||||
</section>
|
||||
<div class="back-to-top">
|
||||
<a href="/" class="btn-back">トップページに戻る</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user