@muhammedaksam/easiarr 0.8.5 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/package.json +1 -1
- package/src/api/cloudflare-api.ts +368 -0
- package/src/apps/registry.ts +36 -7
- package/src/compose/generator.ts +26 -3
- package/src/compose/index.ts +1 -0
- package/src/compose/templates.ts +5 -0
- package/src/compose/traefik-config.ts +174 -0
- package/src/config/bookmarks-generator.ts +26 -4
- package/src/config/schema.ts +11 -0
- package/src/ui/screens/AppConfigurator.ts +24 -1
- package/src/ui/screens/AppManager.ts +1 -1
- package/src/ui/screens/CloudflaredSetup.ts +758 -0
- package/src/ui/screens/FullAutoSetup.ts +72 -0
- package/src/ui/screens/MainMenu.ts +21 -6
- package/src/ui/screens/QuickSetup.ts +4 -4
- package/src/ui/screens/SettingsScreen.ts +571 -0
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +44 -0
- package/src/utils/migrations.ts +12 -0
|
@@ -12,6 +12,9 @@ import { QBittorrentClient, type QBittorrentCategory } from "../../api/qbittorre
|
|
|
12
12
|
import { PortainerApiClient } from "../../api/portainer-api"
|
|
13
13
|
import { JellyfinClient } from "../../api/jellyfin-api"
|
|
14
14
|
import { JellyseerrClient } from "../../api/jellyseerr-api"
|
|
15
|
+
import { CloudflareApi, setupCloudflaredTunnel } from "../../api/cloudflare-api"
|
|
16
|
+
import { saveConfig } from "../../config"
|
|
17
|
+
import { saveCompose } from "../../compose"
|
|
15
18
|
import { getApp } from "../../apps/registry"
|
|
16
19
|
// import type { AppId } from "../../config/schema"
|
|
17
20
|
import { getCategoriesForApps } from "../../utils/categories"
|
|
@@ -85,6 +88,7 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
85
88
|
{ name: "Portainer", status: "pending" },
|
|
86
89
|
{ name: "Jellyfin", status: "pending" },
|
|
87
90
|
{ name: "Jellyseerr", status: "pending" },
|
|
91
|
+
{ name: "Cloudflare Tunnel", status: "pending" },
|
|
88
92
|
]
|
|
89
93
|
}
|
|
90
94
|
|
|
@@ -139,6 +143,9 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
139
143
|
// Step 8: Jellyseerr
|
|
140
144
|
await this.setupJellyseerr()
|
|
141
145
|
|
|
146
|
+
// Step 9: Cloudflare Tunnel
|
|
147
|
+
await this.setupCloudflare()
|
|
148
|
+
|
|
142
149
|
this.isRunning = false
|
|
143
150
|
this.isDone = true
|
|
144
151
|
this.refreshContent()
|
|
@@ -556,6 +563,71 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
556
563
|
this.refreshContent()
|
|
557
564
|
}
|
|
558
565
|
|
|
566
|
+
private async setupCloudflare(): Promise<void> {
|
|
567
|
+
this.updateStep("Cloudflare Tunnel", "running")
|
|
568
|
+
this.refreshContent()
|
|
569
|
+
|
|
570
|
+
const cloudflaredConfig = this.config.apps.find((a) => a.id === "cloudflared" && a.enabled)
|
|
571
|
+
if (!cloudflaredConfig) {
|
|
572
|
+
this.updateStep("Cloudflare Tunnel", "skipped", "Not enabled")
|
|
573
|
+
this.refreshContent()
|
|
574
|
+
return
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const apiToken = this.env["CLOUDFLARE_API_TOKEN"]
|
|
578
|
+
if (!apiToken) {
|
|
579
|
+
this.updateStep("Cloudflare Tunnel", "skipped", "No CLOUDFLARE_API_TOKEN in .env")
|
|
580
|
+
this.refreshContent()
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const domain = this.env["CLOUDFLARE_DNS_ZONE"] || this.config.traefik?.domain
|
|
585
|
+
if (!domain) {
|
|
586
|
+
this.updateStep("Cloudflare Tunnel", "skipped", "No domain configured")
|
|
587
|
+
this.refreshContent()
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
// Create/update tunnel
|
|
593
|
+
const result = await setupCloudflaredTunnel(apiToken, domain, "easiarr")
|
|
594
|
+
|
|
595
|
+
// Save tunnel token to .env
|
|
596
|
+
await updateEnv({
|
|
597
|
+
CLOUDFLARE_TUNNEL_TOKEN: result.tunnelToken,
|
|
598
|
+
CLOUDFLARE_DNS_ZONE: domain,
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
// Update config
|
|
602
|
+
if (this.config.traefik) {
|
|
603
|
+
this.config.traefik.domain = domain
|
|
604
|
+
this.config.traefik.entrypoint = "web"
|
|
605
|
+
}
|
|
606
|
+
this.config.updatedAt = new Date().toISOString()
|
|
607
|
+
await saveConfig(this.config)
|
|
608
|
+
await saveCompose(this.config)
|
|
609
|
+
|
|
610
|
+
// Optional: Set up Cloudflare Access if email is available
|
|
611
|
+
// Check CLOUDFLARE_ACCESS_EMAIL first, then fall back to EMAIL_GLOBAL
|
|
612
|
+
const accessEmail = this.env["CLOUDFLARE_ACCESS_EMAIL"] || this.env["EMAIL_GLOBAL"]
|
|
613
|
+
if (accessEmail) {
|
|
614
|
+
try {
|
|
615
|
+
const api = new CloudflareApi(apiToken)
|
|
616
|
+
await api.setupAccessProtection(domain, [accessEmail], "easiarr")
|
|
617
|
+
this.updateStep("Cloudflare Tunnel", "success", `Tunnel + Access for ${accessEmail}`)
|
|
618
|
+
} catch {
|
|
619
|
+
// Access setup failed, but tunnel is still working
|
|
620
|
+
this.updateStep("Cloudflare Tunnel", "success", "Tunnel created (Access failed)")
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
this.updateStep("Cloudflare Tunnel", "success", "Tunnel created")
|
|
624
|
+
}
|
|
625
|
+
} catch (e) {
|
|
626
|
+
this.updateStep("Cloudflare Tunnel", "error", `${e}`)
|
|
627
|
+
}
|
|
628
|
+
this.refreshContent()
|
|
629
|
+
}
|
|
630
|
+
|
|
559
631
|
private updateStep(name: string, status: SetupStep["status"], message?: string): void {
|
|
560
632
|
const step = this.steps.find((s) => s.name === name)
|
|
561
633
|
if (step) {
|
|
@@ -21,6 +21,8 @@ import { MonitorDashboard } from "./MonitorDashboard"
|
|
|
21
21
|
import { HomepageSetup } from "./HomepageSetup"
|
|
22
22
|
import { JellyfinSetup } from "./JellyfinSetup"
|
|
23
23
|
import { JellyseerrSetup } from "./JellyseerrSetup"
|
|
24
|
+
import { SettingsScreen } from "./SettingsScreen"
|
|
25
|
+
import { CloudflaredSetup } from "./CloudflaredSetup"
|
|
24
26
|
|
|
25
27
|
type MenuItem = { name: string; description: string; action: () => void | Promise<void> }
|
|
26
28
|
|
|
@@ -57,6 +59,11 @@ export class MainMenu {
|
|
|
57
59
|
description: "Add, remove, or configure apps",
|
|
58
60
|
action: () => this.app.navigateTo("appManager"),
|
|
59
61
|
})
|
|
62
|
+
items.push({
|
|
63
|
+
name: "⚙️ Settings",
|
|
64
|
+
description: "Edit Traefik, VPN, and system configuration",
|
|
65
|
+
action: () => this.showScreen(SettingsScreen),
|
|
66
|
+
})
|
|
60
67
|
items.push({
|
|
61
68
|
name: "🐳 Container Control",
|
|
62
69
|
description: "Start, stop, restart containers",
|
|
@@ -140,13 +147,21 @@ export class MainMenu {
|
|
|
140
147
|
action: () => this.showScreen(JellyseerrSetup),
|
|
141
148
|
})
|
|
142
149
|
}
|
|
150
|
+
if (this.isAppEnabled("cloudflared") || this.isAppEnabled("traefik")) {
|
|
151
|
+
items.push({
|
|
152
|
+
name: "☁️ Cloudflare Tunnel",
|
|
153
|
+
description: "Setup or configure Cloudflare Tunnel via API",
|
|
154
|
+
action: () => this.showScreen(CloudflaredSetup),
|
|
155
|
+
})
|
|
156
|
+
}
|
|
143
157
|
// Bookmark generation options
|
|
144
|
-
const generateAndOpenBookmarks = async (
|
|
145
|
-
await saveBookmarks(this.config,
|
|
158
|
+
const generateAndOpenBookmarks = async (type: "local" | "remote") => {
|
|
159
|
+
await saveBookmarks(this.config, type)
|
|
146
160
|
// Open in browser if easiarr service is enabled
|
|
147
161
|
if (this.isAppEnabled("easiarr")) {
|
|
148
162
|
const port = this.config.apps.find((a) => a.id === "easiarr")?.port ?? 3010
|
|
149
|
-
|
|
163
|
+
const filename = type === "remote" ? "bookmarks-remote.html" : "bookmarks-local.html"
|
|
164
|
+
await openUrl(`http://localhost:${port}/${filename}`)
|
|
150
165
|
}
|
|
151
166
|
}
|
|
152
167
|
|
|
@@ -154,18 +169,18 @@ export class MainMenu {
|
|
|
154
169
|
items.push({
|
|
155
170
|
name: "📑 Bookmarks (Local URLs)",
|
|
156
171
|
description: "Generate bookmarks using localhost addresses",
|
|
157
|
-
action: async () => generateAndOpenBookmarks(
|
|
172
|
+
action: async () => generateAndOpenBookmarks("local"),
|
|
158
173
|
})
|
|
159
174
|
items.push({
|
|
160
175
|
name: "📑 Bookmarks (Traefik URLs)",
|
|
161
176
|
description: `Generate bookmarks using ${this.config.traefik.domain} addresses`,
|
|
162
|
-
action: async () => generateAndOpenBookmarks(
|
|
177
|
+
action: async () => generateAndOpenBookmarks("remote"),
|
|
163
178
|
})
|
|
164
179
|
} else {
|
|
165
180
|
items.push({
|
|
166
181
|
name: "📑 Generate Bookmarks",
|
|
167
182
|
description: "Create browser-importable bookmarks file",
|
|
168
|
-
action: async () => generateAndOpenBookmarks(
|
|
183
|
+
action: async () => generateAndOpenBookmarks("local"),
|
|
169
184
|
})
|
|
170
185
|
}
|
|
171
186
|
|
|
@@ -56,7 +56,7 @@ export class QuickSetup {
|
|
|
56
56
|
// Traefik config
|
|
57
57
|
private traefikEnabled: boolean = false
|
|
58
58
|
private traefikDomain: string = "CLOUDFLARE_DNS_ZONE"
|
|
59
|
-
private traefikEntrypoint: string = "
|
|
59
|
+
private traefikEntrypoint: string = "web"
|
|
60
60
|
private traefikMiddlewares: string[] = []
|
|
61
61
|
|
|
62
62
|
constructor(renderer: CliRenderer, container: BoxRenderable, app: App) {
|
|
@@ -732,7 +732,7 @@ export class QuickSetup {
|
|
|
732
732
|
content.add(
|
|
733
733
|
new TextRenderable(this.renderer, {
|
|
734
734
|
id: "traefik-entrypoint-label",
|
|
735
|
-
content: "Entrypoint (e.g.,
|
|
735
|
+
content: "Entrypoint (e.g., web, websecure):",
|
|
736
736
|
fg: "#aaaaaa",
|
|
737
737
|
})
|
|
738
738
|
)
|
|
@@ -740,7 +740,7 @@ export class QuickSetup {
|
|
|
740
740
|
const entrypointInput = new InputRenderable(this.renderer, {
|
|
741
741
|
id: "traefik-entrypoint-input",
|
|
742
742
|
width: "100%",
|
|
743
|
-
placeholder: "
|
|
743
|
+
placeholder: "web",
|
|
744
744
|
backgroundColor: "#2a2a3e",
|
|
745
745
|
textColor: "#ffffff",
|
|
746
746
|
focusedBackgroundColor: "#3a3a4e",
|
|
@@ -798,7 +798,7 @@ export class QuickSetup {
|
|
|
798
798
|
// Save traefik config
|
|
799
799
|
this.traefikEnabled = true
|
|
800
800
|
this.traefikDomain = domainInput.value || "${CLOUDFLARE_DNS_ZONE}"
|
|
801
|
-
this.traefikEntrypoint = entrypointInput.value || "
|
|
801
|
+
this.traefikEntrypoint = entrypointInput.value || "web"
|
|
802
802
|
this.traefikMiddlewares = middlewareInput.value
|
|
803
803
|
? middlewareInput.value
|
|
804
804
|
.split(",")
|