diff --git a/apps/keila/0.30.2/.env.sample b/apps/keila/0.30.2/.env.sample new file mode 100644 index 000000000..4a20393b2 --- /dev/null +++ b/apps/keila/0.30.2/.env.sample @@ -0,0 +1,23 @@ +PANEL_APP_PORT_HTTP=4000 +PANEL_DB_HOST= +PANEL_DB_PORT=5432 +PANEL_DB_NAME=keila +PANEL_DB_USER=keila +PANEL_DB_USER_PASSWORD= +KEILA_USER=root@localhost +KEILA_PASSWORD= +SECRET_KEY_BASE= +URL_HOST=localhost +URL_SCHEMA=http +URL_PORT=4000 +APP_DATA_DIR=./data +MAILER_SMTP_HOST= +MAILER_SMTP_PORT=587 +MAILER_SMTP_FROM_EMAIL= +MAILER_SMTP_USER= +MAILER_SMTP_PASSWORD= +MAILER_ENABLE_SSL=false +MAILER_ENABLE_STARTTLS=false +LOG_LEVEL=info +TZ=Asia/Shanghai +CONTAINER_NAME= diff --git a/apps/keila/0.30.2/data.yml b/apps/keila/0.30.2/data.yml new file mode 100644 index 000000000..c50c52713 --- /dev/null +++ b/apps/keila/0.30.2/data.yml @@ -0,0 +1,401 @@ +additionalProperties: + formFields: + - default: 4000 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: postgresql + envKey: PANEL_DB_TYPE + labelEn: Database Runtime + labelZh: 数据库 Runtime + label: + en: Database Runtime + zh: 数据库 Runtime + zh-Hant: 資料庫 Runtime + ja: データベース Runtime + ko: 데이터베이스 Runtime + ru: Среда базы данных + ms: Runtime Pangkalan Data + pt-br: Runtime de banco de dados + required: true + type: apps + values: + - label: PostgreSQL + value: postgresql + child: + default: '' + envKey: PANEL_DB_HOST + labelEn: PostgreSQL Service + labelZh: PostgreSQL 服务 + label: + en: PostgreSQL Service + zh: PostgreSQL 服务 + zh-Hant: PostgreSQL 服務 + ja: PostgreSQL サービス + ko: PostgreSQL 서비스 + ru: Сервис PostgreSQL + ms: Perkhidmatan PostgreSQL + pt-br: Serviço PostgreSQL + required: true + type: service + - default: 5432 + edit: true + envKey: PANEL_DB_PORT + labelEn: Database Port + labelZh: 数据库端口 + label: + en: Database Port + zh: 数据库端口 + zh-Hant: 資料庫連接埠 + ja: データベースポート + ko: 데이터베이스 포트 + ru: Порт базы данных + ms: Port Pangkalan Data + pt-br: Porta do banco de dados + required: true + rule: paramPort + type: number + - default: keila + edit: true + envKey: PANEL_DB_NAME + labelEn: Database Name + labelZh: 数据库名 + label: + en: Database Name + zh: 数据库名 + zh-Hant: 資料庫名稱 + ja: データベース名 + ko: 데이터베이스 이름 + ru: Имя базы данных + ms: Nama Pangkalan Data + pt-br: Nome do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: keila + edit: true + envKey: PANEL_DB_USER + labelEn: Database User + labelZh: 数据库用户 + label: + en: Database User + zh: 数据库用户 + zh-Hant: 資料庫使用者 + ja: データベースユーザー + ko: 데이터베이스 사용자 + ru: Пользователь базы данных + ms: Pengguna Pangkalan Data + pt-br: Usuário do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: '' + edit: true + envKey: PANEL_DB_USER_PASSWORD + labelEn: Database Password + labelZh: 数据库密码 + label: + en: Database Password + zh: 数据库密码 + zh-Hant: 資料庫密碼 + ja: データベースパスワード + ko: 데이터베이스 비밀번호 + ru: Пароль базы данных + ms: Kata Laluan Pangkalan Data + pt-br: Senha do banco de dados + required: true + rule: paramComplexity + type: password + - default: root@localhost + edit: true + envKey: KEILA_USER + labelEn: Initial Administrator Email + labelZh: 初始管理员邮箱 + label: + en: Initial Administrator Email + zh: 初始管理员邮箱 + zh-Hant: 初始管理員電子郵件 + ja: 初期管理者メールアドレス + ko: 초기 관리자 이메일 + ru: Email начального администратора + ms: E-mel Pentadbir Awal + pt-br: E-mail do administrador inicial + required: true + type: text + - default: '' + edit: true + envKey: KEILA_PASSWORD + labelEn: Initial Administrator Password (10+ characters) + labelZh: 初始管理员密码(至少 10 个字符) + label: + en: Initial Administrator Password (10+ characters) + zh: 初始管理员密码(至少 10 个字符) + zh-Hant: 初始管理員密碼(至少 10 個字元) + ja: 初期管理者パスワード(10 文字以上) + ko: 초기 관리자 비밀번호(10자 이상) + ru: Пароль начального администратора (не менее 10 символов) + ms: Kata Laluan Pentadbir Awal (10+ aksara) + pt-br: Senha do administrador inicial (10+ caracteres) + required: true + type: password + - default: '' + edit: true + envKey: SECRET_KEY_BASE + labelEn: Session Secret (64+ characters) + labelZh: 会话密钥(至少 64 个字符) + label: + en: Session Secret (64+ characters) + zh: 会话密钥(至少 64 个字符) + zh-Hant: 工作階段密鑰(至少 64 個字元) + ja: セッションシークレット(64 文字以上) + ko: 세션 비밀 키(64자 이상) + ru: Секрет сеанса (не менее 64 символов) + ms: Rahsia Sesi (64+ aksara) + pt-br: Segredo da sessão (64+ caracteres) + required: true + type: password + - default: '' + edit: true + envKey: URL_HOST + labelEn: Public Host (domain or IP) + labelZh: 公开访问主机(域名或 IP) + label: + en: Public Host (domain or IP) + zh: 公开访问主机(域名或 IP) + zh-Hant: 公開存取主機(網域或 IP) + ja: 公開ホスト(ドメインまたは IP) + ko: 공개 호스트(도메인 또는 IP) + ru: Публичный хост (домен или IP) + ms: Hos Awam (domain atau IP) + pt-br: Host público (domínio ou IP) + required: true + type: text + - default: http + edit: true + envKey: URL_SCHEMA + labelEn: Public URL Scheme + labelZh: 公开 URL 协议 + label: + en: Public URL Scheme + zh: 公开 URL 协议 + zh-Hant: 公開 URL 通訊協定 + ja: 公開 URL スキーム + ko: 공개 URL 스키마 + ru: Схема публичного URL + ms: Skema URL Awam + pt-br: Esquema da URL pública + required: true + type: select + values: + - label: http + value: http + - label: https + value: https + - default: 4000 + edit: true + envKey: URL_PORT + labelEn: Public URL Port + labelZh: 公开 URL 端口 + label: + en: Public URL Port + zh: 公开 URL 端口 + zh-Hant: 公開 URL 連接埠 + ja: 公開 URL ポート + ko: 공개 URL 포트 + ru: Порт публичного URL + ms: Port URL Awam + pt-br: Porta da URL pública + required: true + type: number + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori Data + pt-br: Diretório de dados + required: true + type: text + - default: '' + edit: true + envKey: MAILER_SMTP_HOST + labelEn: SMTP Host + labelZh: SMTP 主机 + label: + en: SMTP Host + zh: SMTP 主机 + zh-Hant: SMTP 主機 + ja: SMTP ホスト + ko: SMTP 호스트 + ru: SMTP-хост + ms: Hos SMTP + pt-br: Host SMTP + required: true + type: text + - default: 587 + edit: true + envKey: MAILER_SMTP_PORT + labelEn: SMTP Port + labelZh: SMTP 端口 + label: + en: SMTP Port + zh: SMTP 端口 + zh-Hant: SMTP 連接埠 + ja: SMTP ポート + ko: SMTP 포트 + ru: SMTP-порт + ms: Port SMTP + pt-br: Porta SMTP + required: true + type: number + - default: '' + edit: true + envKey: MAILER_SMTP_FROM_EMAIL + labelEn: SMTP From Email + labelZh: SMTP 发件邮箱 + label: + en: SMTP From Email + zh: SMTP 发件邮箱 + zh-Hant: SMTP 寄件者電子郵件 + ja: SMTP 送信元メールアドレス + ko: SMTP 발신 이메일 + ru: Email отправителя SMTP + ms: E-mel Pengirim SMTP + pt-br: E-mail do remetente SMTP + required: true + type: text + - default: '' + edit: true + envKey: MAILER_SMTP_USER + labelEn: SMTP Username + labelZh: SMTP 用户名 + label: + en: SMTP Username + zh: SMTP 用户名 + zh-Hant: SMTP 使用者名稱 + ja: SMTP ユーザー名 + ko: SMTP 사용자 이름 + ru: Имя пользователя SMTP + ms: Nama Pengguna SMTP + pt-br: Usuário SMTP + required: true + type: text + - default: '' + edit: true + envKey: MAILER_SMTP_PASSWORD + labelEn: SMTP Password + labelZh: SMTP 密码 + label: + en: SMTP Password + zh: SMTP 密码 + zh-Hant: SMTP 密碼 + ja: SMTP パスワード + ko: SMTP 비밀번호 + ru: Пароль SMTP + ms: Kata Laluan SMTP + pt-br: Senha SMTP + required: true + type: password + - default: 'false' + edit: true + envKey: MAILER_ENABLE_SSL + labelEn: Enable SMTP SSL/TLS + labelZh: 启用 SMTP SSL/TLS + label: + en: Enable SMTP SSL/TLS + zh: 启用 SMTP SSL/TLS + zh-Hant: 啟用 SMTP SSL/TLS + ja: SMTP SSL/TLS を有効化 + ko: SMTP SSL/TLS 활성화 + ru: Включить SMTP SSL/TLS + ms: Dayakan SMTP SSL/TLS + pt-br: Ativar SMTP SSL/TLS + required: true + type: select + values: + - label: 'false' + value: 'false' + - label: 'true' + value: 'true' + - default: 'false' + edit: true + envKey: MAILER_ENABLE_STARTTLS + labelEn: Enable SMTP STARTTLS + labelZh: 启用 SMTP STARTTLS + label: + en: Enable SMTP STARTTLS + zh: 启用 SMTP STARTTLS + zh-Hant: 啟用 SMTP STARTTLS + ja: SMTP STARTTLS を有効化 + ko: SMTP STARTTLS 활성화 + ru: Включить SMTP STARTTLS + ms: Dayakan SMTP STARTTLS + pt-br: Ativar SMTP STARTTLS + required: true + type: select + values: + - label: 'false' + value: 'false' + - label: 'true' + value: 'true' + - default: info + edit: true + envKey: LOG_LEVEL + labelEn: Log Level + labelZh: 日志级别 + label: + en: Log Level + zh: 日志级别 + zh-Hant: 日誌層級 + ja: ログレベル + ko: 로그 수준 + ru: Уровень журналирования + ms: Tahap Log + pt-br: Nível de log + required: true + type: select + values: + - label: debug + value: debug + - label: info + value: info + - label: error + value: error + - default: Asia/Shanghai + edit: true + envKey: TZ + labelEn: Timezone + labelZh: 时区 + label: + en: Timezone + zh: 时区 + zh-Hant: 時區 + ja: タイムゾーン + ko: 시간대 + ru: Часовой пояс + ms: Zon Waktu + pt-br: Fuso horário + required: true + type: text diff --git a/apps/keila/0.30.2/docker-compose.yml b/apps/keila/0.30.2/docker-compose.yml new file mode 100644 index 000000000..c77c6f9c6 --- /dev/null +++ b/apps/keila/0.30.2/docker-compose.yml @@ -0,0 +1,44 @@ +services: + keila: + image: "pentacent/keila:0.30.2" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + networks: + - 1panel-network + ports: + - "${PANEL_APP_PORT_HTTP}:4000" + environment: + - DB_URL=postgres://${PANEL_DB_USER}:${PANEL_DB_USER_PASSWORD}@${PANEL_DB_HOST}:${PANEL_DB_PORT}/${PANEL_DB_NAME} + - DB_ENABLE_SSL=false + - KEILA_USER=${KEILA_USER} + - KEILA_PASSWORD=${KEILA_PASSWORD} + - SECRET_KEY_BASE=${SECRET_KEY_BASE} + - URL_HOST=${URL_HOST} + - URL_SCHEMA=${URL_SCHEMA} + - URL_PORT=${URL_PORT} + - USER_CONTENT_DIR=/opt/app/uploads + - MAILER_SMTP_HOST=${MAILER_SMTP_HOST} + - MAILER_SMTP_PORT=${MAILER_SMTP_PORT} + - MAILER_SMTP_FROM_EMAIL=${MAILER_SMTP_FROM_EMAIL} + - MAILER_SMTP_USER=${MAILER_SMTP_USER} + - MAILER_SMTP_PASSWORD=${MAILER_SMTP_PASSWORD} + - MAILER_ENABLE_SSL=${MAILER_ENABLE_SSL} + - MAILER_ENABLE_STARTTLS=${MAILER_ENABLE_STARTTLS} + - LOG_LEVEL=${LOG_LEVEL} + - TZ=${TZ} + volumes: + - "${APP_DATA_DIR}/uploads:/opt/app/uploads" + healthcheck: + test: + - CMD-SHELL + - wget --quiet --tries=1 --spider http://127.0.0.1:4000/auth/login || exit 1 + interval: 30s + timeout: 10s + start_period: 90s + retries: 5 + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/keila/0.30.2/scripts/init.sh b/apps/keila/0.30.2/scripts/init.sh new file mode 100755 index 000000000..116e04e94 --- /dev/null +++ b/apps/keila/0.30.2/scripts/init.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +strip_matching_quotes() { + local value="$1" + + if [[ ${#value} -ge 2 ]]; then + if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then + value="${value:1:${#value}-2}" + elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then + value="${value:1:${#value}-2}" + fi + fi + printf '%s\n' "$value" +} + +read_env_value() { + local key="$1" + local value="" + + if [[ -f "$ENV_FILE" ]]; then + value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- || true)" + fi + strip_matching_quotes "$value" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + + temp_file="$(mktemp "${ENV_FILE}.tmp.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { updated = 0 } + $0 ~ ("^" key "=") { + if (!updated) { + print key "=" value + updated = 1 + } + next + } + { print } + END { + if (!updated) print key "=" value + } + ' "$ENV_FILE" > "$temp_file" + chmod --reference="$ENV_FILE" "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +ensure_secret_key_base() { + local secret_key_base + + secret_key_base="$(read_env_value SECRET_KEY_BASE)" + if [[ ${#secret_key_base} -lt 64 ]]; then + secret_key_base="$(od -An -N64 -tx1 /dev/urandom | tr -d ' \n')" + set_env_value SECRET_KEY_BASE "$secret_key_base" + fi +} + +path_is_dotenv_safe() { + local value="$1" + + case "$value" in + *$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) return 1 ;; + *) return 0 ;; + esac +} + +[[ -f "$ENV_FILE" ]] || fail "Environment file not found: ${ENV_FILE}" +ensure_secret_key_base + +if [[ ${APP_DATA_DIR+x} ]]; then + APP_DATA_DIR_RAW="$APP_DATA_DIR" +else + APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)" +fi +APP_DATA_DIR_RAW="$(strip_matching_quotes "${APP_DATA_DIR_RAW:-./data}")" + +[[ -n "$APP_DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty" +path_is_dotenv_safe "$APP_DATA_DIR_RAW" || fail "APP_DATA_DIR contains unsupported dotenv characters" + +case "$APP_DATA_DIR_RAW" in + /*) + APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" + ;; + *) + APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")" + case "$APP_DATA_DIR_ABS" in + "${ROOT_DIR}" | "${ROOT_DIR}"/*) ;; + *) fail "Relative APP_DATA_DIR must stay inside the application directory" ;; + esac + ;; +esac + +[[ "$APP_DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root" +[[ ! -L "$APP_DATA_DIR_ABS" ]] || fail "APP_DATA_DIR must not be a symbolic link" +if [[ -e "$APP_DATA_DIR_ABS" && ! -d "$APP_DATA_DIR_ABS" ]]; then + fail "APP_DATA_DIR must be a directory" +fi + +mkdir -p -- "${APP_DATA_DIR_ABS}/uploads" +chmod 750 -- "$APP_DATA_DIR_ABS" "${APP_DATA_DIR_ABS}/uploads" diff --git a/apps/keila/0.30.2/scripts/uninstall.sh b/apps/keila/0.30.2/scripts/uninstall.sh new file mode 100755 index 000000000..5a3f6bd6b --- /dev/null +++ b/apps/keila/0.30.2/scripts/uninstall.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v docker-compose >/dev/null 2>&1; then + docker-compose down --volumes --remove-orphans +else + docker compose down --volumes --remove-orphans +fi diff --git a/apps/keila/0.30.2/scripts/upgrade.sh b/apps/keila/0.30.2/scripts/upgrade.sh new file mode 100755 index 000000000..593b31667 --- /dev/null +++ b/apps/keila/0.30.2/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +exec bash "${SCRIPT_DIR}/init.sh" diff --git a/apps/keila/README.md b/apps/keila/README.md new file mode 100644 index 000000000..589ba7d17 --- /dev/null +++ b/apps/keila/README.md @@ -0,0 +1,74 @@ +# Keila + +## 产品介绍 + +Keila 是一款开源的自托管新闻简报与邮件营销平台,可用于管理联系人、订阅表单、邮件模板和营销活动,并支持 SMTP、AWS SES、SendGrid、Mailgun 与 Postmark 等发送渠道。 + +## 主要功能 + +- 管理联系人、分组、订阅表单、模板和新闻简报活动。 +- 提供可视化、Markdown 和纯文本邮件编辑能力。 +- 支持公开订阅页面、活动统计和 API。 +- 业务数据保存在 PostgreSQL,本地上传文件保存在独立持久化目录。 + +## 部署拓扑 + +- 本应用包只运行一个 Keila service,使用上游官方 `pentacent/keila` 镜像。 +- 安装时必须选择同一 1Panel 中已运行的 PostgreSQL Runtime;1Panel 会为 Keila 创建独立数据库和独立用户。 +- 上游示例 Compose 中的 PostgreSQL service 已替换为 1Panel Runtime 联动,不会在应用包中重复部署数据库。 +- SMTP 是生产启动的必填依赖,但属于用户提供的外部服务,不会作为 sidecar 安装。 + +## 访问说明 + +默认 Web 端口为 `4000`。安装后访问 `http://服务器地址:安装端口/auth/login`,使用安装表单中的初始管理员邮箱和密码登录。 + +- `公开访问主机` 只填写域名或 IP,不包含 `http://`、`https://` 或路径。 +- 直接按端口访问时,公开 URL 端口通常与 Web 端口相同。 +- 通过反向代理发布 HTTPS 时,公开 URL 协议应设为 `https`,公开 URL 端口通常设为 `443`;反向代理必须正确转发 WebSocket。 +- 初始管理员只在空数据库第一次启动时创建。后续修改安装参数不会重置已有管理员密码,请在 Keila 中管理账号。 +- 上游要求管理员密码至少 10 个字符;会话密钥应为至少 64 个字符的强随机值。 + +## SMTP + +Keila 在生产模式启动时必须配置系统 SMTP,用于注册、激活和密码重置等邮件。 + +- SMTP 用户名在上游可回退为发件邮箱;本应用包要求显式填写,以兼容用户名与发件邮箱不同的服务。 +- 端口 `587` 通常配合 STARTTLS,端口 `465` 通常配合 SSL/TLS。具体组合以邮件服务商文档为准。 +- SMTP 密码、数据库密码、管理员密码和会话密钥都会写入 1Panel 生成的运行时 `.env`,不得提交到 Git 或复制到公开日志。 + +## 数据、备份与升级 + +- Keila 会在启动时自动执行 PostgreSQL schema migration,并在空数据库中创建初始 root 用户。 +- 联系人、活动、模板、账号和任务队列数据位于所选 PostgreSQL Runtime 的独立数据库中。 +- 用户上传文件位于 `${APP_DATA_DIR}/uploads`,容器内路径为 `/opt/app/uploads`。 +- 升级前必须同时备份 PostgreSQL 数据库和 `${APP_DATA_DIR}/uploads`。升级期间等待数据库 migration 完成后再停止或重启容器。 +- 固定版和 `latest` 使用相同的持久化边界;回滚时应恢复与目标版本匹配的升级前数据库及上传文件备份。 +- 卸载应用不会删除所选 PostgreSQL Runtime 本身;是否删除 Keila 的独立数据库和用户取决于 1Panel 卸载选项。卸载前请完成备份。 + +## 安全与许可证 + +- Compose 不启用 privileged、host network、额外 capability、设备映射或 Docker Socket。 +- 建议通过 1Panel 反向代理启用 HTTPS,并限制管理入口的公网暴露范围。 +- Keila 源代码采用 GNU Affero General Public License v3.0 或更高版本。部署、修改或向网络用户提供服务时,请遵守上游许可证义务。 +- 上游 README 说明 Keila 标志不包含在 AGPL 授权中;标志权利归其权利人,本应用包仅将其用于识别对应产品。 + +## 参考资料 + +- 官网: +- 官方安装文档: +- 官方配置文档: +- 官方首次启动文档: +- 源码与官方 Compose: + +## Introduction + +Keila is an open-source self-hosted newsletter and email marketing platform. This package runs one official Keila container, links it to a selectable PostgreSQL Runtime managed by 1Panel, and persists local uploads separately. + +Production startup requires SMTP configuration. Keila automatically runs database migrations and creates the initial root user on an empty database. Back up both PostgreSQL and `${APP_DATA_DIR}/uploads` before upgrades. + +## Features + +- Contact, segment, subscription-form, template, and newsletter campaign management. +- Visual, Markdown, and plain-text email editing. +- PostgreSQL-backed application data and a persistent local upload directory. +- Explicit initial administrator, public URL, SMTP, and session-secret configuration. diff --git a/apps/keila/data.yml b/apps/keila/data.yml new file mode 100644 index 000000000..58b75a554 --- /dev/null +++ b/apps/keila/data.yml @@ -0,0 +1,30 @@ +name: Keila +tags: + - Email +title: 开源的自托管新闻简报与邮件营销平台 +description: 开源的自托管新闻简报与邮件营销平台 +additionalProperties: + key: keila + name: Keila + tags: + - Email + type: tool + website: https://www.keila.io/ + document: https://www.keila.io/docs/installation + github: https://github.com/pentacent/keila + shortDescZh: 开源的自托管新闻简报与邮件营销平台 + shortDescEn: An open-source self-hosted newsletter and email marketing platform + crossVersionUpdate: true + limit: 0 + recommend: 0 + architectures: + - amd64 + description: + en: An open-source self-hosted newsletter and email marketing platform + zh: 开源的自托管新闻简报与邮件营销平台 + zh-Hant: 開源的自託管電子報與電子郵件行銷平台 + ja: オープンソースのセルフホスト型ニュースレター・メールマーケティングプラットフォーム + ko: 오픈 소스 자체 호스팅 뉴스레터 및 이메일 마케팅 플랫폼 + ru: Самостоятельно размещаемая платформа с открытым исходным кодом для рассылок и email-маркетинга + ms: Platform sumber terbuka yang dihos sendiri untuk surat berita dan pemasaran e-mel + pt-br: Plataforma de código aberto auto-hospedada para newsletters e marketing por e-mail diff --git a/apps/keila/latest/.env.sample b/apps/keila/latest/.env.sample new file mode 100644 index 000000000..4a20393b2 --- /dev/null +++ b/apps/keila/latest/.env.sample @@ -0,0 +1,23 @@ +PANEL_APP_PORT_HTTP=4000 +PANEL_DB_HOST= +PANEL_DB_PORT=5432 +PANEL_DB_NAME=keila +PANEL_DB_USER=keila +PANEL_DB_USER_PASSWORD= +KEILA_USER=root@localhost +KEILA_PASSWORD= +SECRET_KEY_BASE= +URL_HOST=localhost +URL_SCHEMA=http +URL_PORT=4000 +APP_DATA_DIR=./data +MAILER_SMTP_HOST= +MAILER_SMTP_PORT=587 +MAILER_SMTP_FROM_EMAIL= +MAILER_SMTP_USER= +MAILER_SMTP_PASSWORD= +MAILER_ENABLE_SSL=false +MAILER_ENABLE_STARTTLS=false +LOG_LEVEL=info +TZ=Asia/Shanghai +CONTAINER_NAME= diff --git a/apps/keila/latest/data.yml b/apps/keila/latest/data.yml new file mode 100644 index 000000000..c50c52713 --- /dev/null +++ b/apps/keila/latest/data.yml @@ -0,0 +1,401 @@ +additionalProperties: + formFields: + - default: 4000 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: postgresql + envKey: PANEL_DB_TYPE + labelEn: Database Runtime + labelZh: 数据库 Runtime + label: + en: Database Runtime + zh: 数据库 Runtime + zh-Hant: 資料庫 Runtime + ja: データベース Runtime + ko: 데이터베이스 Runtime + ru: Среда базы данных + ms: Runtime Pangkalan Data + pt-br: Runtime de banco de dados + required: true + type: apps + values: + - label: PostgreSQL + value: postgresql + child: + default: '' + envKey: PANEL_DB_HOST + labelEn: PostgreSQL Service + labelZh: PostgreSQL 服务 + label: + en: PostgreSQL Service + zh: PostgreSQL 服务 + zh-Hant: PostgreSQL 服務 + ja: PostgreSQL サービス + ko: PostgreSQL 서비스 + ru: Сервис PostgreSQL + ms: Perkhidmatan PostgreSQL + pt-br: Serviço PostgreSQL + required: true + type: service + - default: 5432 + edit: true + envKey: PANEL_DB_PORT + labelEn: Database Port + labelZh: 数据库端口 + label: + en: Database Port + zh: 数据库端口 + zh-Hant: 資料庫連接埠 + ja: データベースポート + ko: 데이터베이스 포트 + ru: Порт базы данных + ms: Port Pangkalan Data + pt-br: Porta do banco de dados + required: true + rule: paramPort + type: number + - default: keila + edit: true + envKey: PANEL_DB_NAME + labelEn: Database Name + labelZh: 数据库名 + label: + en: Database Name + zh: 数据库名 + zh-Hant: 資料庫名稱 + ja: データベース名 + ko: 데이터베이스 이름 + ru: Имя базы данных + ms: Nama Pangkalan Data + pt-br: Nome do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: keila + edit: true + envKey: PANEL_DB_USER + labelEn: Database User + labelZh: 数据库用户 + label: + en: Database User + zh: 数据库用户 + zh-Hant: 資料庫使用者 + ja: データベースユーザー + ko: 데이터베이스 사용자 + ru: Пользователь базы данных + ms: Pengguna Pangkalan Data + pt-br: Usuário do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: '' + edit: true + envKey: PANEL_DB_USER_PASSWORD + labelEn: Database Password + labelZh: 数据库密码 + label: + en: Database Password + zh: 数据库密码 + zh-Hant: 資料庫密碼 + ja: データベースパスワード + ko: 데이터베이스 비밀번호 + ru: Пароль базы данных + ms: Kata Laluan Pangkalan Data + pt-br: Senha do banco de dados + required: true + rule: paramComplexity + type: password + - default: root@localhost + edit: true + envKey: KEILA_USER + labelEn: Initial Administrator Email + labelZh: 初始管理员邮箱 + label: + en: Initial Administrator Email + zh: 初始管理员邮箱 + zh-Hant: 初始管理員電子郵件 + ja: 初期管理者メールアドレス + ko: 초기 관리자 이메일 + ru: Email начального администратора + ms: E-mel Pentadbir Awal + pt-br: E-mail do administrador inicial + required: true + type: text + - default: '' + edit: true + envKey: KEILA_PASSWORD + labelEn: Initial Administrator Password (10+ characters) + labelZh: 初始管理员密码(至少 10 个字符) + label: + en: Initial Administrator Password (10+ characters) + zh: 初始管理员密码(至少 10 个字符) + zh-Hant: 初始管理員密碼(至少 10 個字元) + ja: 初期管理者パスワード(10 文字以上) + ko: 초기 관리자 비밀번호(10자 이상) + ru: Пароль начального администратора (не менее 10 символов) + ms: Kata Laluan Pentadbir Awal (10+ aksara) + pt-br: Senha do administrador inicial (10+ caracteres) + required: true + type: password + - default: '' + edit: true + envKey: SECRET_KEY_BASE + labelEn: Session Secret (64+ characters) + labelZh: 会话密钥(至少 64 个字符) + label: + en: Session Secret (64+ characters) + zh: 会话密钥(至少 64 个字符) + zh-Hant: 工作階段密鑰(至少 64 個字元) + ja: セッションシークレット(64 文字以上) + ko: 세션 비밀 키(64자 이상) + ru: Секрет сеанса (не менее 64 символов) + ms: Rahsia Sesi (64+ aksara) + pt-br: Segredo da sessão (64+ caracteres) + required: true + type: password + - default: '' + edit: true + envKey: URL_HOST + labelEn: Public Host (domain or IP) + labelZh: 公开访问主机(域名或 IP) + label: + en: Public Host (domain or IP) + zh: 公开访问主机(域名或 IP) + zh-Hant: 公開存取主機(網域或 IP) + ja: 公開ホスト(ドメインまたは IP) + ko: 공개 호스트(도메인 또는 IP) + ru: Публичный хост (домен или IP) + ms: Hos Awam (domain atau IP) + pt-br: Host público (domínio ou IP) + required: true + type: text + - default: http + edit: true + envKey: URL_SCHEMA + labelEn: Public URL Scheme + labelZh: 公开 URL 协议 + label: + en: Public URL Scheme + zh: 公开 URL 协议 + zh-Hant: 公開 URL 通訊協定 + ja: 公開 URL スキーム + ko: 공개 URL 스키마 + ru: Схема публичного URL + ms: Skema URL Awam + pt-br: Esquema da URL pública + required: true + type: select + values: + - label: http + value: http + - label: https + value: https + - default: 4000 + edit: true + envKey: URL_PORT + labelEn: Public URL Port + labelZh: 公开 URL 端口 + label: + en: Public URL Port + zh: 公开 URL 端口 + zh-Hant: 公開 URL 連接埠 + ja: 公開 URL ポート + ko: 공개 URL 포트 + ru: Порт публичного URL + ms: Port URL Awam + pt-br: Porta da URL pública + required: true + type: number + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori Data + pt-br: Diretório de dados + required: true + type: text + - default: '' + edit: true + envKey: MAILER_SMTP_HOST + labelEn: SMTP Host + labelZh: SMTP 主机 + label: + en: SMTP Host + zh: SMTP 主机 + zh-Hant: SMTP 主機 + ja: SMTP ホスト + ko: SMTP 호스트 + ru: SMTP-хост + ms: Hos SMTP + pt-br: Host SMTP + required: true + type: text + - default: 587 + edit: true + envKey: MAILER_SMTP_PORT + labelEn: SMTP Port + labelZh: SMTP 端口 + label: + en: SMTP Port + zh: SMTP 端口 + zh-Hant: SMTP 連接埠 + ja: SMTP ポート + ko: SMTP 포트 + ru: SMTP-порт + ms: Port SMTP + pt-br: Porta SMTP + required: true + type: number + - default: '' + edit: true + envKey: MAILER_SMTP_FROM_EMAIL + labelEn: SMTP From Email + labelZh: SMTP 发件邮箱 + label: + en: SMTP From Email + zh: SMTP 发件邮箱 + zh-Hant: SMTP 寄件者電子郵件 + ja: SMTP 送信元メールアドレス + ko: SMTP 발신 이메일 + ru: Email отправителя SMTP + ms: E-mel Pengirim SMTP + pt-br: E-mail do remetente SMTP + required: true + type: text + - default: '' + edit: true + envKey: MAILER_SMTP_USER + labelEn: SMTP Username + labelZh: SMTP 用户名 + label: + en: SMTP Username + zh: SMTP 用户名 + zh-Hant: SMTP 使用者名稱 + ja: SMTP ユーザー名 + ko: SMTP 사용자 이름 + ru: Имя пользователя SMTP + ms: Nama Pengguna SMTP + pt-br: Usuário SMTP + required: true + type: text + - default: '' + edit: true + envKey: MAILER_SMTP_PASSWORD + labelEn: SMTP Password + labelZh: SMTP 密码 + label: + en: SMTP Password + zh: SMTP 密码 + zh-Hant: SMTP 密碼 + ja: SMTP パスワード + ko: SMTP 비밀번호 + ru: Пароль SMTP + ms: Kata Laluan SMTP + pt-br: Senha SMTP + required: true + type: password + - default: 'false' + edit: true + envKey: MAILER_ENABLE_SSL + labelEn: Enable SMTP SSL/TLS + labelZh: 启用 SMTP SSL/TLS + label: + en: Enable SMTP SSL/TLS + zh: 启用 SMTP SSL/TLS + zh-Hant: 啟用 SMTP SSL/TLS + ja: SMTP SSL/TLS を有効化 + ko: SMTP SSL/TLS 활성화 + ru: Включить SMTP SSL/TLS + ms: Dayakan SMTP SSL/TLS + pt-br: Ativar SMTP SSL/TLS + required: true + type: select + values: + - label: 'false' + value: 'false' + - label: 'true' + value: 'true' + - default: 'false' + edit: true + envKey: MAILER_ENABLE_STARTTLS + labelEn: Enable SMTP STARTTLS + labelZh: 启用 SMTP STARTTLS + label: + en: Enable SMTP STARTTLS + zh: 启用 SMTP STARTTLS + zh-Hant: 啟用 SMTP STARTTLS + ja: SMTP STARTTLS を有効化 + ko: SMTP STARTTLS 활성화 + ru: Включить SMTP STARTTLS + ms: Dayakan SMTP STARTTLS + pt-br: Ativar SMTP STARTTLS + required: true + type: select + values: + - label: 'false' + value: 'false' + - label: 'true' + value: 'true' + - default: info + edit: true + envKey: LOG_LEVEL + labelEn: Log Level + labelZh: 日志级别 + label: + en: Log Level + zh: 日志级别 + zh-Hant: 日誌層級 + ja: ログレベル + ko: 로그 수준 + ru: Уровень журналирования + ms: Tahap Log + pt-br: Nível de log + required: true + type: select + values: + - label: debug + value: debug + - label: info + value: info + - label: error + value: error + - default: Asia/Shanghai + edit: true + envKey: TZ + labelEn: Timezone + labelZh: 时区 + label: + en: Timezone + zh: 时区 + zh-Hant: 時區 + ja: タイムゾーン + ko: 시간대 + ru: Часовой пояс + ms: Zon Waktu + pt-br: Fuso horário + required: true + type: text diff --git a/apps/keila/latest/docker-compose.yml b/apps/keila/latest/docker-compose.yml new file mode 100644 index 000000000..6da76e933 --- /dev/null +++ b/apps/keila/latest/docker-compose.yml @@ -0,0 +1,44 @@ +services: + keila: + image: "pentacent/keila:latest" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + networks: + - 1panel-network + ports: + - "${PANEL_APP_PORT_HTTP}:4000" + environment: + - DB_URL=postgres://${PANEL_DB_USER}:${PANEL_DB_USER_PASSWORD}@${PANEL_DB_HOST}:${PANEL_DB_PORT}/${PANEL_DB_NAME} + - DB_ENABLE_SSL=false + - KEILA_USER=${KEILA_USER} + - KEILA_PASSWORD=${KEILA_PASSWORD} + - SECRET_KEY_BASE=${SECRET_KEY_BASE} + - URL_HOST=${URL_HOST} + - URL_SCHEMA=${URL_SCHEMA} + - URL_PORT=${URL_PORT} + - USER_CONTENT_DIR=/opt/app/uploads + - MAILER_SMTP_HOST=${MAILER_SMTP_HOST} + - MAILER_SMTP_PORT=${MAILER_SMTP_PORT} + - MAILER_SMTP_FROM_EMAIL=${MAILER_SMTP_FROM_EMAIL} + - MAILER_SMTP_USER=${MAILER_SMTP_USER} + - MAILER_SMTP_PASSWORD=${MAILER_SMTP_PASSWORD} + - MAILER_ENABLE_SSL=${MAILER_ENABLE_SSL} + - MAILER_ENABLE_STARTTLS=${MAILER_ENABLE_STARTTLS} + - LOG_LEVEL=${LOG_LEVEL} + - TZ=${TZ} + volumes: + - "${APP_DATA_DIR}/uploads:/opt/app/uploads" + healthcheck: + test: + - CMD-SHELL + - wget --quiet --tries=1 --spider http://127.0.0.1:4000/auth/login || exit 1 + interval: 30s + timeout: 10s + start_period: 90s + retries: 5 + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/keila/latest/scripts/init.sh b/apps/keila/latest/scripts/init.sh new file mode 100755 index 000000000..116e04e94 --- /dev/null +++ b/apps/keila/latest/scripts/init.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +strip_matching_quotes() { + local value="$1" + + if [[ ${#value} -ge 2 ]]; then + if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then + value="${value:1:${#value}-2}" + elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then + value="${value:1:${#value}-2}" + fi + fi + printf '%s\n' "$value" +} + +read_env_value() { + local key="$1" + local value="" + + if [[ -f "$ENV_FILE" ]]; then + value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- || true)" + fi + strip_matching_quotes "$value" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + + temp_file="$(mktemp "${ENV_FILE}.tmp.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { updated = 0 } + $0 ~ ("^" key "=") { + if (!updated) { + print key "=" value + updated = 1 + } + next + } + { print } + END { + if (!updated) print key "=" value + } + ' "$ENV_FILE" > "$temp_file" + chmod --reference="$ENV_FILE" "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +ensure_secret_key_base() { + local secret_key_base + + secret_key_base="$(read_env_value SECRET_KEY_BASE)" + if [[ ${#secret_key_base} -lt 64 ]]; then + secret_key_base="$(od -An -N64 -tx1 /dev/urandom | tr -d ' \n')" + set_env_value SECRET_KEY_BASE "$secret_key_base" + fi +} + +path_is_dotenv_safe() { + local value="$1" + + case "$value" in + *$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) return 1 ;; + *) return 0 ;; + esac +} + +[[ -f "$ENV_FILE" ]] || fail "Environment file not found: ${ENV_FILE}" +ensure_secret_key_base + +if [[ ${APP_DATA_DIR+x} ]]; then + APP_DATA_DIR_RAW="$APP_DATA_DIR" +else + APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)" +fi +APP_DATA_DIR_RAW="$(strip_matching_quotes "${APP_DATA_DIR_RAW:-./data}")" + +[[ -n "$APP_DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty" +path_is_dotenv_safe "$APP_DATA_DIR_RAW" || fail "APP_DATA_DIR contains unsupported dotenv characters" + +case "$APP_DATA_DIR_RAW" in + /*) + APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" + ;; + *) + APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")" + case "$APP_DATA_DIR_ABS" in + "${ROOT_DIR}" | "${ROOT_DIR}"/*) ;; + *) fail "Relative APP_DATA_DIR must stay inside the application directory" ;; + esac + ;; +esac + +[[ "$APP_DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root" +[[ ! -L "$APP_DATA_DIR_ABS" ]] || fail "APP_DATA_DIR must not be a symbolic link" +if [[ -e "$APP_DATA_DIR_ABS" && ! -d "$APP_DATA_DIR_ABS" ]]; then + fail "APP_DATA_DIR must be a directory" +fi + +mkdir -p -- "${APP_DATA_DIR_ABS}/uploads" +chmod 750 -- "$APP_DATA_DIR_ABS" "${APP_DATA_DIR_ABS}/uploads" diff --git a/apps/keila/latest/scripts/uninstall.sh b/apps/keila/latest/scripts/uninstall.sh new file mode 100755 index 000000000..5a3f6bd6b --- /dev/null +++ b/apps/keila/latest/scripts/uninstall.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v docker-compose >/dev/null 2>&1; then + docker-compose down --volumes --remove-orphans +else + docker compose down --volumes --remove-orphans +fi diff --git a/apps/keila/latest/scripts/upgrade.sh b/apps/keila/latest/scripts/upgrade.sh new file mode 100755 index 000000000..593b31667 --- /dev/null +++ b/apps/keila/latest/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +exec bash "${SCRIPT_DIR}/init.sh" diff --git a/apps/keila/logo.png b/apps/keila/logo.png new file mode 100644 index 000000000..a50106d4d Binary files /dev/null and b/apps/keila/logo.png differ