管理画面のヘッダーを更新し、タイトルを「神椿ライブスケジュール」に変更。ログアウト機能を追加し、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:
|
services:
|
||||||
app:
|
app:
|
||||||
|
build:
|
||||||
|
dockerfile: Dockerfile
|
||||||
volumes:
|
volumes:
|
||||||
- ./public/css:/app/public/css
|
- ./public/css:/app/public/css
|
||||||
- ./views:/app/views
|
- ./views:/app/views
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
db:
|
||||||
|
volumes:
|
||||||
|
- db-store:/var/lib/mysql
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-store:
|
@ -2,13 +2,13 @@ services:
|
|||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.prodcution
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "80:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: development
|
NODE_ENV: production
|
||||||
DB_NAME: ${DB_NAME}
|
DB_NAME: ${DB_NAME}
|
||||||
DB_USER: ${DB_USER}
|
DB_USER: ${DB_USER}
|
||||||
DB_PASS: ${DB_PASS}
|
DB_PASS: ${DB_PASS}
|
||||||
@ -16,11 +16,12 @@ services:
|
|||||||
TZ: Asia/Tokyo
|
TZ: Asia/Tokyo
|
||||||
volumes:
|
volumes:
|
||||||
- ./public/thumbnails:/app/public/thumbnails
|
- ./public/thumbnails:/app/public/thumbnails
|
||||||
|
- ./logs/app:/app/logs
|
||||||
db:
|
db:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
volumes:
|
volumes:
|
||||||
- db-store:/var/lib/mysql
|
- db-store-prodcution:/var/lib/mysql
|
||||||
- ./logs:/var/log/mysql
|
- ./logs/db:/var/log/mysql
|
||||||
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
|
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: ${DB_NAME}
|
MYSQL_DATABASE: ${DB_NAME}
|
||||||
@ -32,5 +33,4 @@ services:
|
|||||||
- ${DB_PORT}:3306
|
- ${DB_PORT}:3306
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-store:
|
db-store-prodcution:
|
||||||
|
|
||||||
|
BIN
public/favicon.ico
Normal file
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 cron from "node-cron";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
@ -9,9 +9,6 @@ import { runMigrations } from "./utils/migrate";
|
|||||||
import methodOverride from "method-override";
|
import methodOverride from "method-override";
|
||||||
import { updateAllLiveSchedules } from "./services/updateLiveSchedules";
|
import { updateAllLiveSchedules } from "./services/updateLiveSchedules";
|
||||||
|
|
||||||
// セッションとベーシック認証の有効期限
|
|
||||||
const SESSION_EXPIRATION = 60 * 1 * 1000; // テスト目的で1分に設定
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const startServer = async () => {
|
const startServer = async () => {
|
||||||
@ -19,66 +16,13 @@ const startServer = async () => {
|
|||||||
// マイグレーション実行
|
// マイグレーション実行
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
|
|
||||||
// 開発中なので定期実行タスクをコメントアウト
|
// 定期実行タスクを設定
|
||||||
// cron.schedule("0 * * * *", async () => {
|
cron.schedule(CRON_SCHEDULE, async () => {
|
||||||
// console.log("Updating live schedules...");
|
console.log("Updating live schedules...");
|
||||||
// await updateAllLiveSchedules();
|
await updateAllLiveSchedules();
|
||||||
// });
|
|
||||||
|
|
||||||
// セッション設定
|
|
||||||
app.use(
|
|
||||||
session({
|
|
||||||
secret: SESSION_SECRET,
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: false,
|
|
||||||
rolling: false,
|
|
||||||
cookie: { maxAge: SESSION_EXPIRATION, secure: false, httpOnly: true },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// キャッシュを抑止
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
||||||
res.setHeader("Pragma", "no-cache");
|
|
||||||
res.setHeader("Expires", "0");
|
|
||||||
next();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
// ビューの設定
|
||||||
* 管理画面へのアクセス制限
|
|
||||||
* 1. セッションが有効期限内かどうかチェック
|
|
||||||
* 2. 期限切れなら 401 を返す → ブラウザは再度Basic認証を要求
|
|
||||||
* 3. 成功したらセッションを再生成する
|
|
||||||
*/
|
|
||||||
app.use("/hyAdmin", (req, res, next) => {
|
|
||||||
const sessionExpires = req.session.cookie.expires;
|
|
||||||
|
|
||||||
// セッションが有効期限内の場合はそのまま進む
|
|
||||||
if (req.session.isAuthenticated && sessionExpires && sessionExpires > new Date()) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// セッションが切れた(または存在しない)場合、
|
|
||||||
// Basic認証を実行して認証に成功すればセッション再生成、失敗すれば401を返す
|
|
||||||
basicAuth({
|
|
||||||
users: { [BASIC_AUTH_USER]: BASIC_AUTH_PASS },
|
|
||||||
challenge: true, // 401時にWWW-Authenticateヘッダーを付加
|
|
||||||
unauthorizedResponse: "Unauthorized",
|
|
||||||
})(req, res, (authError) => {
|
|
||||||
// authError があればそのままエラーに
|
|
||||||
if (authError) {
|
|
||||||
return next(authError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 認証が通ったのでセッション再生成
|
|
||||||
req.session.isAuthenticated = true;
|
|
||||||
req.session.cookie.expires = new Date(Date.now() + SESSION_EXPIRATION);
|
|
||||||
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 管理画面ビューの設定
|
|
||||||
app.set("view engine", "ejs");
|
app.set("view engine", "ejs");
|
||||||
app.set("views", "./views");
|
app.set("views", "./views");
|
||||||
|
|
||||||
@ -89,6 +33,45 @@ const startServer = async () => {
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// セッション設定
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: SESSION_SECRET,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
rolling: true,
|
||||||
|
cookie: { maxAge: SESSION_EXPIRATION, secure: false, httpOnly: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 管理画面ベーシック認証
|
||||||
|
app.use(
|
||||||
|
"/hyAdmin",
|
||||||
|
basicAuth({
|
||||||
|
users: { [BASIC_AUTH_ID]: BASIC_AUTH_PASS },
|
||||||
|
challenge: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 管理画面認証ミドルウェア
|
||||||
|
app.use("/hyAdmin", (req, res, next) => {
|
||||||
|
if (req.session.isAuthenticated) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && req.url === "/login") {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
if (username === ADMIN_ID && password === ADMIN_PASS) {
|
||||||
|
req.session.isAuthenticated = true;
|
||||||
|
req.session.cookie.expires = new Date(Date.now() + SESSION_EXPIRATION);
|
||||||
|
return res.redirect("/hyAdmin");
|
||||||
|
}
|
||||||
|
return res.render("admin/login", { error: "Invalid credentials" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render("admin/login");
|
||||||
|
});
|
||||||
|
|
||||||
// 管理画面ルート
|
// 管理画面ルート
|
||||||
app.use("/hyAdmin", adminRoutes);
|
app.use("/hyAdmin", adminRoutes);
|
||||||
|
|
||||||
@ -99,6 +82,7 @@ const startServer = async () => {
|
|||||||
app.use("/thumbnails", express.static("public/thumbnails"));
|
app.use("/thumbnails", express.static("public/thumbnails"));
|
||||||
app.use("/css", express.static("public/css"));
|
app.use("/css", express.static("public/css"));
|
||||||
app.use("/js", express.static("public/js"));
|
app.use("/js", express.static("public/js"));
|
||||||
|
app.use("/favicon.ico", express.static("public/favicon.ico"));
|
||||||
|
|
||||||
// 404エラーハンドリング
|
// 404エラーハンドリング
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
|
@ -8,11 +8,14 @@ const requiredEnvVars = [
|
|||||||
"DB_USER",
|
"DB_USER",
|
||||||
"DB_PASS",
|
"DB_PASS",
|
||||||
"DB_HOST",
|
"DB_HOST",
|
||||||
"BASIC_AUTH_USER",
|
"BASIC_AUTH_ID",
|
||||||
"BASIC_AUTH_PASS",
|
"BASIC_AUTH_PASS",
|
||||||
"WEB_PORT",
|
"WEB_PORT",
|
||||||
"YOUTUBE_API_KEY",
|
"YOUTUBE_API_KEY",
|
||||||
"SESSION_SECRET",
|
"SESSION_SECRET",
|
||||||
|
"CRON_INTERVAL_MINUTES",
|
||||||
|
"ADMIN_ID",
|
||||||
|
"ADMIN_PASS",
|
||||||
];
|
];
|
||||||
|
|
||||||
requiredEnvVars.forEach((varName) => {
|
requiredEnvVars.forEach((varName) => {
|
||||||
@ -21,12 +24,42 @@ requiredEnvVars.forEach((varName) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SESSION_EXPIRATION = 60 * 1 * 1000;
|
||||||
|
|
||||||
export const DB_NAME = process.env.DB_NAME as string;
|
export const DB_NAME = process.env.DB_NAME as string;
|
||||||
export const DB_USER = process.env.DB_USER as string;
|
export const DB_USER = process.env.DB_USER as string;
|
||||||
export const DB_PASS = process.env.DB_PASS as string;
|
export const DB_PASS = process.env.DB_PASS as string;
|
||||||
export const DB_HOST = process.env.DB_HOST as string;
|
export const DB_HOST = process.env.DB_HOST as string;
|
||||||
export const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER as string;
|
export const BASIC_AUTH_ID = process.env.BASIC_AUTH_ID as string;
|
||||||
export const BASIC_AUTH_PASS = process.env.BASIC_AUTH_PASS as string;
|
export const BASIC_AUTH_PASS = process.env.BASIC_AUTH_PASS as string;
|
||||||
export const WEB_PORT = parseInt(process.env.WEB_PORT as string, 10);
|
export const WEB_PORT = parseInt(process.env.WEB_PORT as string, 10);
|
||||||
export const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY as string;
|
export const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY as string;
|
||||||
export const SESSION_SECRET = process.env.SESSION_SECRET as string;
|
export const SESSION_SECRET = process.env.SESSION_SECRET as string;
|
||||||
|
export const ADMIN_ID = process.env.ADMIN_ID as string;
|
||||||
|
export const ADMIN_PASS = process.env.ADMIN_PASS as string;
|
||||||
|
|
||||||
|
// cron のスケジュールを計算
|
||||||
|
const intervalMinutes = parseInt(process.env.CRON_INTERVAL_MINUTES || "60", 10);
|
||||||
|
if (isNaN(intervalMinutes) || intervalMinutes <= 0) {
|
||||||
|
throw new Error("Invalid CRON_INTERVAL_MINUTES value in .env file. Must be a positive number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let cronSchedule = "";
|
||||||
|
if (intervalMinutes <= 60) {
|
||||||
|
// 60分以下の場合、分単位で設定
|
||||||
|
cronSchedule = `*/${intervalMinutes} * * * *`;
|
||||||
|
} else {
|
||||||
|
// 60分を超える場合、時間と分に分割
|
||||||
|
const hours = Math.floor(intervalMinutes / 60);
|
||||||
|
const minutes = intervalMinutes % 60;
|
||||||
|
|
||||||
|
if (minutes === 0) {
|
||||||
|
// 分が0の場合
|
||||||
|
cronSchedule = `0 */${hours} * * *`;
|
||||||
|
} else {
|
||||||
|
// 分が0以外の場合
|
||||||
|
cronSchedule = `${minutes} */${hours} * * *`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CRON_SCHEDULE = cronSchedule;
|
||||||
|
@ -7,6 +7,13 @@ import { updateLiveSchedulesForChannel } from "../services/liveScheduleService";
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// ログアウト処理
|
||||||
|
router.get("/logout", (req, res) => {
|
||||||
|
req.session.destroy(() => {
|
||||||
|
res.redirect("/hyAdmin");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 管理画面トップページ
|
// 管理画面トップページ
|
||||||
router.get("/", (req, res) => {
|
router.get("/", (req, res) => {
|
||||||
res.render("admin/index");
|
res.render("admin/index");
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<%- include("../partials/head") %>
|
<%- include("../partials/head") %>
|
||||||
<body>
|
<body>
|
||||||
<%- include("../partials/hyAdmin/header") %>
|
<%- include("../partials/admin/header") %>
|
||||||
<div class="header-spacer"></div>
|
<div class="header-spacer"></div>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<h1>チャンネルを編集</h1>
|
<h1>チャンネルを編集</h1>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<%- include("../partials/head") %>
|
<%- include("../partials/head") %>
|
||||||
<body>
|
<body>
|
||||||
<%- include("../partials/hyAdmin/header") %>
|
<%- include("../partials/admin/header") %>
|
||||||
<div class="header-spacer"></div>
|
<div class="header-spacer"></div>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<h1>新しいチャンネルを追加</h1>
|
<h1>新しいチャンネルを追加</h1>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<%- include("../partials/head") %>
|
<%- include("../partials/head") %>
|
||||||
<body>
|
<body>
|
||||||
<%- include("../partials/hyAdmin/header") %>
|
<%- include("../partials/admin/header") %>
|
||||||
<div class="header-spacer"></div>
|
<div class="header-spacer"></div>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<h1>チャンネル管理</h1>
|
<h1>チャンネル管理</h1>
|
||||||
|
@ -6,13 +6,15 @@
|
|||||||
</head>
|
</head>
|
||||||
<%- include("../partials/head") %>
|
<%- include("../partials/head") %>
|
||||||
<body>
|
<body>
|
||||||
<%- include("../partials/hyAdmin/header") %>
|
<%- include("../partials/admin/header") %>
|
||||||
<div class="header-spacer"></div>
|
<div class="header-spacer"></div>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<h1>管理画面</h1>
|
<h1>管理画面</h1>
|
||||||
<div class="admin-menu">
|
<div class="admin-menu">
|
||||||
<a href="/hyAdmin/channel" class="btn">チャンネル管理</a><br>
|
<a href="/hyAdmin/channel" class="btn">チャンネル管理</a><br>
|
||||||
<a href="/hyAdmin/schedule" class="btn">配信スケジュール管理</a>
|
<a href="/hyAdmin/schedule" class="btn">配信スケジュール管理</a>
|
||||||
|
<br>
|
||||||
|
<a href="/hyAdmin/logout" class="btn">ログアウト</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
29
views/admin/login.ejs
Normal file
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>
|
</head>
|
||||||
<%- include("../partials/head") %>
|
<%- include("../partials/head") %>
|
||||||
<body>
|
<body>
|
||||||
<%- include("../partials/hyAdmin/header") %>
|
<%- include("../partials/admin/header") %>
|
||||||
<div class="header-spacer"></div>
|
<div class="header-spacer"></div>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<h1>スケジュール編集</h1>
|
<h1>スケジュール編集</h1>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<%- include("../partials/head") %>
|
<%- include("../partials/head") %>
|
||||||
<body>
|
<body>
|
||||||
<%- include("../partials/hyAdmin/header") %>
|
<%- include("../partials/admin/header") %>
|
||||||
<div class="header-spacer"></div>
|
<div class="header-spacer"></div>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<h1>配信スケジュール管理</h1>
|
<h1>配信スケジュール管理</h1>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<a href="/hyAdmin">神椿配信スケジュール(管理画面)</a>
|
<a href="/hyAdmin">神椿ライブスケジュール(管理画面)</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<a href="/" class="header-link">一般ページへ</a>
|
<a href="/" class="header-link">一般ページへ</a>
|
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>神椿配信スケジュール</title>
|
<title>神椿ライブスケジュール</title>
|
||||||
<link rel="stylesheet" href="/css/header.css">
|
<link rel="stylesheet" href="/css/header.css">
|
||||||
<link rel="stylesheet" href="/css/errors.css">
|
<link rel="stylesheet" href="/css/errors.css">
|
||||||
</head>
|
</head>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span>神椿配信スケジュール</span>
|
<span>神椿ライブスケジュール</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<a href="/terms" class="header-link">利用規約</a>
|
<a href="/terms" class="header-link">利用規約</a>
|
||||||
|
<a href="https://forms.gle/uP7DsKR6HxzU9TKH7" class="header-link" target="_blank">お問い合わせ</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
Loading…
Reference in New Issue
Block a user