├── .dockerignore ├── ui ├── frontend │ ├── .env.production │ ├── .env.development │ ├── postcss.config.js │ ├── jsconfig.json │ ├── public │ │ └── lightning.svg │ ├── src │ │ ├── main.jsx │ │ ├── lib │ │ │ └── utils.js │ │ ├── App.jsx │ │ ├── components │ │ │ └── ui │ │ │ │ ├── label.jsx │ │ │ │ ├── input.jsx │ │ │ │ ├── switch.jsx │ │ │ │ ├── alert.jsx │ │ │ │ ├── tabs.jsx │ │ │ │ ├── card.jsx │ │ │ │ └── button.jsx │ │ ├── styles │ │ │ └── globals.css │ │ ├── authContext.jsx │ │ ├── LoginPage.jsx │ │ └── LightstackDashboard.jsx │ ├── index.html │ ├── .eslintrc.cjs │ ├── vite.config.js │ ├── package.json │ └── tailwind.config.js └── backend │ ├── requirements.txt │ └── main.py ├── .gitignore ├── Dockerfile ├── kickstarter.sh ├── docker-compose.yml.example ├── docker-compose.yml.stack.sqlite.example ├── docker-compose.yml.stack.example ├── default.conf.example ├── README.md ├── gui-install.sh ├── gui-install-interactive.sh ├── cloud-init.example ├── install.sh ├── initlib.sh ├── .env.sqlite.example ├── .env.example └── init.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | **/data 2 | -------------------------------------------------------------------------------- /ui/frontend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_URL=/api 2 | -------------------------------------------------------------------------------- /ui/frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_URL=http://localhost:8000 2 | -------------------------------------------------------------------------------- /ui/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /ui/frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | default.conf 3 | letsencrypt 4 | docker-compose.yml 5 | lnbitsdata 6 | pgtmp 7 | pgdata 8 | nginx 9 | stack* 10 | .backup 11 | .env 12 | stack_* 13 | -------------------------------------------------------------------------------- /ui/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.104.1 2 | uvicorn==0.24.0 3 | python-jose[cryptography]==3.3.0 4 | python-multipart==0.0.6 5 | pyjwt==2.8.0 6 | python-dotenv==1.0.0 7 | -------------------------------------------------------------------------------- /ui/frontend/public/lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './styles/globals.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:12.6-slim 2 | RUN apt-get update 3 | RUN apt-get install -y curl wget unzip libcurl4-gnutls-dev libsqlite3-dev 4 | 5 | 6 | WORKDIR /app 7 | RUN cd /app && wget https://github.com/ACINQ/phoenixd/releases/download/v0.6.0/phoenixd-0.6.0-linux-x64.zip &&\ 8 | unzip -j phoenixd-0.6.0-linux-x64.zip 9 | 10 | 11 | CMD [ "./phoenixd" ] 12 | 13 | -------------------------------------------------------------------------------- /ui/frontend/src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | export const getApiUrl = () => { 9 | if (import.meta.env.PROD) { 10 | return '/api' 11 | } 12 | // In sviluppo, usa la porta 8005 13 | return 'http://localhost:8005' 14 | } 15 | -------------------------------------------------------------------------------- /kickstarter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export DOMAIN=lightstack.yourdomain.com 3 | export EMAIL=tech@yourdomain.com 4 | export ADMIN_USER=admin 5 | export ADMIN_PASS=yourpassword 6 | export NODE_VERSION=20.9.0 7 | git clone https://github.com/massmux/lightstack.git /home/dev/lightstack 8 | cd /home/dev/lightstack 9 | chmod +x gui-install.sh 10 | chown -R dev:dev /home/dev/lightstack 11 | ./gui-install.sh 12 | -------------------------------------------------------------------------------- /ui/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Lightstack Management 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docker-compose.yml.example: -------------------------------------------------------------------------------- 1 | include: 2 | services: 3 | nginx: 4 | container_name: ${COMPOSE_PROJECT_NAME}-nginx 5 | hostname: nginx 6 | image: nginx:mainline 7 | restart: on-failure 8 | volumes: 9 | - ./nginx:/etc/nginx/conf.d:ro 10 | - ./letsencrypt:/etc/letsencrypt:ro 11 | ports: 12 | - 443:443 13 | networks: 14 | - backend 15 | - frontend 16 | 17 | networks: 18 | backend: 19 | internal: true 20 | frontend: 21 | internal: false 22 | -------------------------------------------------------------------------------- /ui/frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AuthProvider, useAuth } from './authContext'; 3 | import LoginPage from './LoginPage'; 4 | import LightstackDashboard from './LightstackDashboard'; 5 | 6 | const AppContent = () => { 7 | const { token } = useAuth(); 8 | return token ? : ; 9 | }; 10 | 11 | const App = () => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /ui/frontend/src/components/ui/label.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva } from "class-variance-authority" 4 | import { cn } from "@/lib/utils" 5 | 6 | const labelVariants = cva( 7 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 8 | ) 9 | 10 | const Label = React.forwardRef(({ className, ...props }, ref) => ( 11 | 12 | )) 13 | Label.displayName = LabelPrimitive.Root.displayName 14 | 15 | export { Label } 16 | -------------------------------------------------------------------------------- /ui/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /ui/frontend/src/components/ui/input.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cn } from "@/lib/utils" 3 | 4 | const Input = React.forwardRef(({ className, type, ...props }, ref) => { 5 | return ( 6 | 15 | ) 16 | }) 17 | Input.displayName = "Input" 18 | 19 | export { Input } 20 | -------------------------------------------------------------------------------- /docker-compose.yml.stack.sqlite.example: -------------------------------------------------------------------------------- 1 | services: 2 | phoenixd: 3 | build: 4 | context: ../ 5 | tags: 6 | - "massmux/phoenixd" 7 | container_name: ${COMPOSE_PROJECT_NAME}-phoenixd 8 | hostname: phoenixd 9 | image: massmux/phoenixd 10 | restart: on-failure 11 | volumes: 12 | - ./data:/root/.phoenix 13 | working_dir: /app 14 | environment: 15 | - TZ=Europe/Rome 16 | networks: 17 | - backend 18 | - frontend 19 | 20 | lnbits: 21 | container_name: ${COMPOSE_PROJECT_NAME}-lnbits 22 | image: massmux/lnbits:0.12.11 23 | hostname: lnbits 24 | restart: on-failure 25 | stop_grace_period: 1m 26 | volumes: 27 | - ./lnbitsdata:/app/data 28 | - ./.env:/app/.env 29 | environment: 30 | FORWARDED_ALLOW_IPS: "*" 31 | networks: 32 | - backend 33 | - frontend 34 | -------------------------------------------------------------------------------- /ui/frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import path from 'path' 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | '@': path.resolve(__dirname, './src'), 10 | }, 11 | }, 12 | server: { 13 | proxy: { 14 | '/api': { 15 | target: 'http://localhost:8005', // Aggiornato alla porta corretta del backend 16 | changeOrigin: true, 17 | rewrite: (path) => path.replace(/^\/api/, '') 18 | } 19 | } 20 | }, 21 | build: { 22 | outDir: 'dist', 23 | sourcemap: false, 24 | minify: true, 25 | rollupOptions: { 26 | output: { 27 | manualChunks: { 28 | vendor: ['react', 'react-dom'], 29 | ui: ['@radix-ui/react-switch', '@radix-ui/react-tabs', 'lucide-react'] 30 | } 31 | } 32 | } 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /ui/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lightstack-ui", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-alert-dialog": "^1.0.5", 13 | "@radix-ui/react-label": "^2.0.2", 14 | "@radix-ui/react-slot": "^1.0.2", 15 | "@radix-ui/react-switch": "^1.0.3", 16 | "@radix-ui/react-tabs": "^1.0.4", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.0.0", 19 | "lucide-react": "^0.292.0", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "tailwind-merge": "^2.0.0", 23 | "tailwindcss-animate": "^1.0.7" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20.8.10", 27 | "@types/react": "^18.2.15", 28 | "@types/react-dom": "^18.2.7", 29 | "@vitejs/plugin-react": "^4.2.0", 30 | "autoprefixer": "^10.4.16", 31 | "postcss": "^8.4.31", 32 | "tailwindcss": "^3.3.5", 33 | "typescript": "^5.0.2", 34 | "vite": "^5.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ui/frontend/src/components/ui/switch.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SwitchPrimitives from "@radix-ui/react-switch" 3 | import { cn } from "@/lib/utils" 4 | 5 | const Switch = React.forwardRef(({ className, ...props }, ref) => ( 6 | 14 | 19 | 20 | )) 21 | Switch.displayName = SwitchPrimitives.Root.displayName 22 | 23 | export { Switch } 24 | -------------------------------------------------------------------------------- /docker-compose.yml.stack.example: -------------------------------------------------------------------------------- 1 | services: 2 | phoenixd: 3 | build: 4 | context: ../ 5 | tags: 6 | - "massmux/phoenixd" 7 | container_name: ${COMPOSE_PROJECT_NAME}-phoenixd 8 | hostname: phoenixd 9 | image: massmux/phoenixd 10 | restart: on-failure 11 | volumes: 12 | - ./data:/root/.phoenix 13 | working_dir: /app 14 | environment: 15 | - TZ=Europe/Rome 16 | networks: 17 | - backend 18 | - frontend 19 | 20 | lnbits: 21 | container_name: ${COMPOSE_PROJECT_NAME}-lnbits 22 | image: massmux/lnbits:0.12.11 23 | hostname: lnbits 24 | restart: on-failure 25 | stop_grace_period: 1m 26 | volumes: 27 | - ./lnbitsdata:/app/data 28 | - ./.env:/app/.env 29 | environment: 30 | FORWARDED_ALLOW_IPS: "*" 31 | networks: 32 | - backend 33 | - frontend 34 | 35 | postgres: 36 | container_name: ${COMPOSE_PROJECT_NAME}-postgres 37 | image: postgres 38 | restart: on-failure 39 | environment: 40 | POSTGRES_PASSWORD: XXXX 41 | POSTGRES_DB: lnbits 42 | PGDATA: "/var/lib/postgresql/data/pgdata" 43 | volumes: 44 | - ./pgdata:/var/lib/postgresql/data 45 | - ./pgtmp:/var/tmp 46 | networks: 47 | - backend 48 | -------------------------------------------------------------------------------- /ui/frontend/src/components/ui/alert.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva } from "class-variance-authority" 3 | import { cn } from "@/lib/utils" 4 | 5 | const alertVariants = cva( 6 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 7 | { 8 | variants: { 9 | variant: { 10 | default: "bg-background text-foreground", 11 | destructive: 12 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 13 | }, 14 | }, 15 | defaultVariants: { 16 | variant: "default", 17 | }, 18 | } 19 | ) 20 | 21 | const Alert = React.forwardRef(({ className, variant, ...props }, ref) => ( 22 |
28 | )) 29 | Alert.displayName = "Alert" 30 | 31 | const AlertTitle = React.forwardRef(({ className, ...props }, ref) => ( 32 |
37 | )) 38 | AlertTitle.displayName = "AlertTitle" 39 | 40 | const AlertDescription = React.forwardRef(({ className, ...props }, ref) => ( 41 |
46 | )) 47 | AlertDescription.displayName = "AlertDescription" 48 | 49 | export { Alert, AlertTitle, AlertDescription } 50 | -------------------------------------------------------------------------------- /ui/frontend/src/components/ui/tabs.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as TabsPrimitive from "@radix-ui/react-tabs" 3 | import { cn } from "@/lib/utils" 4 | 5 | const Tabs = TabsPrimitive.Root 6 | 7 | const TabsList = React.forwardRef(({ className, ...props }, ref) => ( 8 | 16 | )) 17 | TabsList.displayName = TabsPrimitive.List.displayName 18 | 19 | const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => ( 20 | 28 | )) 29 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 30 | 31 | const TabsContent = React.forwardRef(({ className, ...props }, ref) => ( 32 | 40 | )) 41 | TabsContent.displayName = TabsPrimitive.Content.displayName 42 | 43 | export { Tabs, TabsList, TabsTrigger, TabsContent } 44 | -------------------------------------------------------------------------------- /ui/frontend/src/components/ui/card.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cn } from "@/lib/utils" 3 | 4 | const Card = React.forwardRef(({ className, ...props }, ref) => ( 5 |
13 | )) 14 | Card.displayName = "Card" 15 | 16 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => ( 17 |
22 | )) 23 | CardHeader.displayName = "CardHeader" 24 | 25 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => ( 26 |

34 | )) 35 | CardTitle.displayName = "CardTitle" 36 | 37 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => ( 38 |

43 | )) 44 | CardDescription.displayName = "CardDescription" 45 | 46 | const CardContent = React.forwardRef(({ className, ...props }, ref) => ( 47 |

48 | )) 49 | CardContent.displayName = "CardContent" 50 | 51 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => ( 52 |
57 | )) 58 | CardFooter.displayName = "CardFooter" 59 | 60 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 61 | -------------------------------------------------------------------------------- /ui/frontend/src/components/ui/button.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva } from "class-variance-authority" 4 | import { cn } from "@/lib/utils" 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 12 | destructive: 13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 14 | outline: 15 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 16 | secondary: 17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 18 | ghost: "hover:bg-accent hover:text-accent-foreground", 19 | link: "text-primary underline-offset-4 hover:underline", 20 | }, 21 | size: { 22 | default: "h-10 px-4 py-2", 23 | sm: "h-9 rounded-md px-3", 24 | lg: "h-11 rounded-md px-8", 25 | icon: "h-10 w-10", 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: "default", 30 | size: "default", 31 | }, 32 | } 33 | ) 34 | 35 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { 36 | const Comp = asChild ? Slot : "button" 37 | return ( 38 | 43 | ) 44 | }) 45 | Button.displayName = "Button" 46 | 47 | export { Button, buttonVariants } 48 | -------------------------------------------------------------------------------- /ui/frontend/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /default.conf.example: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | # phoenixd 4 | listen 443 ssl; 5 | server_name n1.yourdomain.com; 6 | 7 | access_log /var/log/nginx/reverse-access.log; 8 | error_log /var/log/nginx/reverse-error.log; 9 | 10 | 11 | location / { 12 | proxy_pass http://phoenixd:9740; 13 | 14 | proxy_redirect off; 15 | proxy_set_header Host $http_host; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 18 | proxy_set_header X-Forwarded-Proto https; 19 | } 20 | ssl_certificate /etc/letsencrypt/live/n1.yourdomain.com/fullchain.pem; 21 | ssl_certificate_key /etc/letsencrypt/live/n1.yourdomain.com/privkey.pem; 22 | 23 | } 24 | 25 | server { 26 | # lnbits 27 | listen 443 ssl; 28 | server_name lb1.yourdomain.com; 29 | 30 | access_log /var/log/nginx/reverse-access.log; 31 | error_log /var/log/nginx/reverse-error.log; 32 | 33 | location ~ ^/api/v1/payments/sse(.*) { 34 | proxy_pass http://lnbits:8000; 35 | proxy_http_version 1.1; 36 | proxy_set_header Connection ""; 37 | proxy_set_header Host $host; 38 | proxy_set_header X-Real-IP $remote_addr; 39 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 40 | proxy_set_header X-Forwarded-Proto https; 41 | proxy_buffering off; 42 | proxy_cache off; 43 | chunked_transfer_encoding off; 44 | gzip off; 45 | } 46 | 47 | 48 | location / { 49 | proxy_pass http://lnbits:5000; 50 | proxy_redirect off; 51 | proxy_set_header Host $http_host; 52 | proxy_set_header X-Real-IP $remote_addr; 53 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 54 | proxy_set_header X-Forwarded-Proto https; 55 | } 56 | ssl_certificate /etc/letsencrypt/live/lb1.yourdomain.com/fullchain.pem; 57 | ssl_certificate_key /etc/letsencrypt/live/lb1.yourdomain.com/privkey.pem; 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /ui/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{js,jsx}', 6 | './components/**/*.{js,jsx}', 7 | './app/**/*.{js,jsx}', 8 | './src/**/*.{js,jsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | } 77 | -------------------------------------------------------------------------------- /ui/frontend/src/authContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useContext, useEffect } from 'react'; 2 | import { getApiUrl } from '@/lib/utils'; 3 | 4 | const AuthContext = createContext(null); 5 | 6 | export const AuthProvider = ({ children }) => { 7 | const [token, setToken] = useState(localStorage.getItem('token')); 8 | const [isLoading, setIsLoading] = useState(true); 9 | const apiUrl = getApiUrl(); 10 | 11 | useEffect(() => { 12 | const validateToken = async () => { 13 | if (token) { 14 | try { 15 | const response = await fetch(`${apiUrl}/health`, { 16 | headers: { 17 | 'Authorization': `Bearer ${token}` 18 | } 19 | }); 20 | if (!response.ok) { 21 | localStorage.removeItem('token'); 22 | setToken(null); 23 | } 24 | } catch (error) { 25 | localStorage.removeItem('token'); 26 | setToken(null); 27 | } 28 | } 29 | setIsLoading(false); 30 | }; 31 | 32 | validateToken(); 33 | }, [token]); 34 | 35 | const login = async (username, password) => { 36 | try { 37 | const formData = new URLSearchParams(); 38 | formData.append('username', username); 39 | formData.append('password', password); 40 | 41 | const response = await fetch(`${apiUrl}/token`, { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/x-www-form-urlencoded', 45 | }, 46 | body: formData 47 | }); 48 | 49 | if (!response.ok) { 50 | throw new Error('Login failed'); 51 | } 52 | 53 | const data = await response.json(); 54 | localStorage.setItem('token', data.access_token); 55 | setToken(data.access_token); 56 | return true; 57 | } catch (error) { 58 | console.error('Login error:', error); 59 | return false; 60 | } 61 | }; 62 | 63 | const logout = () => { 64 | localStorage.removeItem('token'); 65 | setToken(null); 66 | }; 67 | 68 | if (isLoading) { 69 | return
70 |
71 |
; 72 | } 73 | 74 | return ( 75 | 76 | {children} 77 | 78 | ); 79 | }; 80 | 81 | export const useAuth = () => { 82 | const context = useContext(AuthContext); 83 | if (!context) { 84 | throw new Error('useAuth must be used within an AuthProvider'); 85 | } 86 | return context; 87 | }; 88 | -------------------------------------------------------------------------------- /ui/frontend/src/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 3 | import { Button } from '@/components/ui/button'; 4 | import { Input } from '@/components/ui/input'; 5 | import { Label } from '@/components/ui/label'; 6 | import { Alert, AlertDescription } from '@/components/ui/alert'; 7 | import { useAuth } from './authContext'; 8 | import { LogIn } from 'lucide-react'; 9 | 10 | const LoginPage = () => { 11 | const { login } = useAuth(); 12 | const [username, setUsername] = useState(''); 13 | const [password, setPassword] = useState(''); 14 | const [error, setError] = useState(''); 15 | const [isLoading, setIsLoading] = useState(false); 16 | 17 | const handleSubmit = async (e) => { 18 | e.preventDefault(); 19 | setIsLoading(true); 20 | setError(''); 21 | 22 | try { 23 | const success = await login(username, password); 24 | if (!success) { 25 | setError('Invalid username or password'); 26 | } 27 | } catch (err) { 28 | setError('Login failed. Please try again.'); 29 | } finally { 30 | setIsLoading(false); 31 | } 32 | }; 33 | 34 | return ( 35 |
36 | 37 | 38 | Lightstack Management 39 | 40 | 41 |
42 |
43 | 44 | setUsername(e.target.value)} 49 | required 50 | className="w-full" 51 | autoComplete="username" 52 | /> 53 |
54 |
55 | 56 | setPassword(e.target.value)} 61 | required 62 | className="w-full" 63 | autoComplete="current-password" 64 | /> 65 |
66 | {error && ( 67 | 68 | {error} 69 | 70 | )} 71 | 88 |
89 |
90 |
91 |
92 | ); 93 | }; 94 | 95 | export default LoginPage; 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Phoenixd Stack 2 | 3 | Just run your own self-custodial cloud Lightning node on your VPS, from your domain name, with this stack. You immediately get a LNBits instance on your own node and the phoenixd endpoint available on a SSL connection. 4 | 5 | A multistack feature has been added and makes possible to add multiple stacks to the same VPS, served simultaneously. A Web UI has been integrated as well 6 | 7 | ## Installation 8 | 9 | ### Get the repo 10 | 11 | ``` 12 | cd ~ 13 | git clone https://github.com/massmux/lightstack.git 14 | cd lightstack 15 | ``` 16 | 17 | ### Choose domain and cnames 18 | 19 | choose two subdomains on your domain, where you have the DNS management. They must point the A record to the host server IP and this must be already propagated when you are going to invoke initialization script. In my example they are 20 | 21 | - n1.yourdomain.com 22 | - lb1.yourdomain.com 23 | 24 | n1 will be the endpoint for phoenixd APIs 25 | lb1 will be the lnbits install 26 | 27 | ### Initialize (command line interface) 28 | 29 | You can install the stack either using the command line interface of by the Web UI. If you start by using command line, just always go on with that. 30 | 31 | ``` 32 | cd lightstack 33 | ./init.sh 34 | 35 | ``` 36 | 37 | The interactive init script makes possible to choose beween using PostgreSQL or SQLite as database support, and choose self-signed certificates (for testing purposes) or Letsencrypt ones. You will be asked during install. 38 | 39 | PostgreSQL is better when thinking to large amount of users installs. 40 | 41 | In case you need to start inizialization from the beginning, you need to clear existing configuration (if the case) by issuing: 42 | 43 | ``` 44 | ./init.sh clear 45 | ``` 46 | 47 | Please note that this will clear all data, configuration files and database in existing directory. If you had funds in that node, you will loose them all, so be careful and check it before 48 | 49 | Help shows usage and active stacks. Stacks can be also removed by the below commands: 50 | 51 | ``` 52 | dev@lightstack:~/lightstack$ sudo ./init.sh help 53 | 54 | You have the following active stacks: 55 | ID PHOENIXD LNBITS 56 | 1 n1.yourdomain.com lb1.yourdomain.com 57 | 2 n2.yourdomain.com lb2.yourdomain.com 58 | 59 | 60 | Usage: init.sh [command] 61 | command: 62 | add [DEFAULT]: to init a new system and/or add a new stack 63 | clear: to remove all stacks 64 | del|rem: to remove a stack 65 | help: to show this message 66 | ``` 67 | 68 | ### Install Web Interface 69 | 70 | A WEB UI has been added to manage stacks. This is for now installed into the host system. Installing the web UI does not interfere with command line interface. So you can decide to use it or not based on your skills and preferences. 71 | 72 | To install the web UI (interactive mode), run: 73 | 74 | ``` 75 | cd ~ 76 | git clone https://github.com/massmux/lightstack.git 77 | cd lightstack 78 | sudo ./gui-install-interactive.sh 79 | ``` 80 | 81 | This command will set up the web-based management interface for your LightStack. The interface provides an easy way to monitor and manage your node. 82 | 83 | Otherwise you can prefer non-interactive mode. In this case, set up your details into the kickstart.sh file with the environment variables, then run it. 84 | 85 | Please be always sure that all your domains and subdomains point to the IP of the VPS as DNS A record (not cname). 86 | 87 | 88 | ### Access 89 | 90 | Access LNBITS instance at: 91 | 92 | - https://lb1.yourdomain.com 93 | 94 | Access phoenixd API endpoint with: 95 | 96 | - https://n1.yourdomain.com 97 | - http password: provided in phoenix.conf file 98 | 99 | in case you want to tune your configuration you can always setup the .env file as you prefer. 100 | 101 | ### VPS 102 | 103 | VPS provided and tested by https://denali.eu 104 | -------------------------------------------------------------------------------- /gui-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | cd "$(dirname "$0")" 5 | 6 | # Colors for output 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | RED='\033[0;31m' 10 | NC='\033[0m' 11 | 12 | # Logging functions 13 | log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } 14 | log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } 15 | log_error() { echo -e "${RED}[ERROR]${NC} $1"; } 16 | 17 | # Default values (Override via environment variables) 18 | DOMAIN="${DOMAIN:-manager.example.com}" 19 | EMAIL="${EMAIL:-admin@example.com}" 20 | ADMIN_USER="${ADMIN_USER:-admin}" 21 | ADMIN_PASS="${ADMIN_PASS:-changeme}" 22 | NODE_VERSION="${NODE_VERSION:-20.9.0}" 23 | 24 | # Install system dependencies 25 | install_dependencies() { 26 | log_info "Updating system and installing dependencies..." 27 | 28 | apt-get update -y 29 | apt-get install -y \ 30 | python3 \ 31 | python3-pip \ 32 | python3-venv \ 33 | nginx \ 34 | certbot \ 35 | curl \ 36 | git \ 37 | python3-certbot-nginx \ 38 | build-essential \ 39 | openssl 40 | } 41 | 42 | # Install and setup nvm 43 | install_nvm() { 44 | log_info "Installing NVM (Node Version Manager)..." 45 | 46 | if ! command -v nvm &> /dev/null; then 47 | curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash 48 | export NVM_DIR="$HOME/.nvm" 49 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 50 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" 51 | else 52 | log_info "NVM is already installed." 53 | fi 54 | } 55 | 56 | # Install Node.js using nvm 57 | install_nodejs() { 58 | install_nvm 59 | 60 | export NVM_DIR="$HOME/.nvm" 61 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 62 | 63 | log_info "Installing Node.js $NODE_VERSION..." 64 | nvm install "$NODE_VERSION" 65 | nvm use "$NODE_VERSION" 66 | nvm alias default "$NODE_VERSION" 67 | 68 | log_info "Node.js version installed:" 69 | node --version 70 | npm --version 71 | } 72 | 73 | # Check system requirements 74 | check_requirements() { 75 | if [ "$EUID" -ne 0 ]; then 76 | log_error "This script must be run as root (use sudo)" 77 | exit 1 78 | fi 79 | } 80 | 81 | # Setup Python virtual environment and install backend 82 | setup_backend() { 83 | log_info "Setting up backend..." 84 | 85 | cd ui/backend 86 | python3 -m venv venv 87 | source venv/bin/activate 88 | 89 | pip install --upgrade pip 90 | pip install -r requirements.txt 91 | 92 | cat > /etc/systemd/system/lightstack-backend.service << EOF 93 | [Unit] 94 | Description=Lightstack Backend 95 | After=network.target 96 | 97 | [Service] 98 | Type=simple 99 | User=root 100 | Group=root 101 | WorkingDirectory=$(pwd) 102 | ExecStart=$(pwd)/venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8005 103 | Restart=always 104 | RestartSec=5 105 | StartLimitInterval=500 106 | StartLimitBurst=5 107 | 108 | [Install] 109 | WantedBy=multi-user.target 110 | EOF 111 | 112 | systemctl daemon-reload 113 | systemctl enable lightstack-backend 114 | systemctl start lightstack-backend 115 | 116 | cd ../.. 117 | } 118 | 119 | # Build and setup frontend 120 | setup_frontend() { 121 | log_info "Setting up frontend..." 122 | 123 | cd ui/frontend 124 | 125 | export NVM_DIR="$HOME/.nvm" 126 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 127 | 128 | nvm use "$NODE_VERSION" 129 | 130 | rm -rf node_modules package-lock.json 131 | npm cache clean --force 132 | npm install 133 | npm run build 134 | 135 | mkdir -p /var/www/lightstack 136 | cp -r dist/* /var/www/lightstack/ 137 | 138 | chown -R www-data:www-data /var/www/lightstack 139 | chmod -R 755 /var/www/lightstack 140 | 141 | cd ../.. 142 | } 143 | 144 | # Setup nginx configuration 145 | setup_nginx() { 146 | log_info "Setting up nginx configuration for $DOMAIN..." 147 | 148 | mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled 149 | 150 | cat > /etc/nginx/sites-available/lightstack << EOF 151 | upstream backend { 152 | server 127.0.0.1:8005; 153 | } 154 | 155 | server { 156 | listen 8443 ssl; 157 | server_name ${DOMAIN}; 158 | 159 | ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; 160 | ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; 161 | include /etc/letsencrypt/options-ssl-nginx.conf; 162 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 163 | 164 | root /var/www/lightstack; 165 | 166 | location / { 167 | try_files \$uri \$uri/ /index.html; 168 | add_header Cache-Control "no-cache, no-store, must-revalidate"; 169 | } 170 | 171 | location /api/ { 172 | proxy_pass http://backend/; 173 | proxy_http_version 1.1; 174 | proxy_set_header Upgrade \$http_upgrade; 175 | proxy_set_header Connection 'upgrade'; 176 | proxy_set_header Host \$host; 177 | proxy_cache_bypass \$http_upgrade; 178 | } 179 | } 180 | EOF 181 | 182 | ln -sf /etc/nginx/sites-available/lightstack /etc/nginx/sites-enabled/ 183 | rm -f /etc/nginx/sites-enabled/default 184 | 185 | nginx -t 186 | systemctl reload nginx || systemctl start nginx 187 | } 188 | 189 | # Setup SSL certificates 190 | setup_ssl() { 191 | log_info "Setting up SSL certificates..." 192 | 193 | apt-get install -y python3-certbot-nginx 194 | 195 | if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then 196 | log_info "SSL certificate already exists. Skipping renewal..." 197 | else 198 | certbot certonly --nginx --non-interactive --agree-tos --email "$EMAIL" --domains "$DOMAIN" 199 | fi 200 | } 201 | 202 | # Generate environment configuration 203 | generate_env() { 204 | log_info "Generating environment configuration..." 205 | 206 | JWT_SECRET=$(openssl rand -hex 32) 207 | 208 | cat > ui/backend/.env << EOF 209 | DOMAIN=$DOMAIN 210 | JWT_SECRET_KEY=$JWT_SECRET 211 | ADMIN_USER=$ADMIN_USER 212 | ADMIN_PASS=$ADMIN_PASS 213 | NODE_ENV=production 214 | EOF 215 | 216 | chmod 600 ui/backend/.env 217 | } 218 | 219 | # Main script 220 | main() { 221 | clear 222 | log_info "Starting Lightstack UI Installation (Non-Interactive Mode)" 223 | 224 | check_requirements 225 | install_dependencies 226 | install_nodejs 227 | 228 | systemctl stop lightstack-backend 2>/dev/null || true 229 | 230 | generate_env 231 | setup_backend 232 | setup_frontend 233 | setup_ssl 234 | setup_nginx 235 | 236 | log_info "Installation completed successfully!" 237 | echo 238 | echo "Access at https://$DOMAIN:8443" 239 | echo "Username: $ADMIN_USER" 240 | } 241 | 242 | trap 'echo -e "\n${RED}Error: Installation interrupted${NC}"; exit 1' ERR 243 | main 244 | -------------------------------------------------------------------------------- /gui-install-interactive.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | cd "$(dirname "$0")" 5 | 6 | # Colors for output 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | RED='\033[0;31m' 10 | NC='\033[0m' 11 | 12 | # Logging functions 13 | log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } 14 | log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } 15 | log_error() { echo -e "${RED}[ERROR]${NC} $1"; } 16 | 17 | # Install system dependencies 18 | install_dependencies() { 19 | log_info "System update and dependencies install..." 20 | 21 | apt-get update 22 | 23 | apt-get install -y \ 24 | python3 \ 25 | python3-pip \ 26 | python3-venv \ 27 | nginx \ 28 | certbot \ 29 | python3-certbot-nginx \ 30 | curl \ 31 | git \ 32 | build-essential \ 33 | openssl 34 | } 35 | 36 | # Install and setup nvm 37 | install_nvm() { 38 | log_info "Installing NVM (Node Version Manager)..." 39 | 40 | if ! command -v nvm &> /dev/null; then 41 | curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash 42 | export NVM_DIR="$HOME/.nvm" 43 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 44 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" 45 | else 46 | log_info "NVM is already installed. Skipping..." 47 | fi 48 | } 49 | 50 | # Install Node.js using nvm 51 | install_nodejs() { 52 | install_nvm 53 | 54 | # Ensure nvm is loaded 55 | export NVM_DIR="$HOME/.nvm" 56 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 57 | 58 | # Prompt for Node.js version 59 | read -p "Enter Node.js version to install (default: 20.9.0): " NODE_VERSION 60 | NODE_VERSION=${NODE_VERSION:-20.9.0} 61 | 62 | log_info "Installing Node.js $NODE_VERSION using nvm..." 63 | nvm install "$NODE_VERSION" 64 | nvm use "$NODE_VERSION" 65 | nvm alias default "$NODE_VERSION" 66 | 67 | log_info "Node.js version installed:" 68 | node --version 69 | npm --version 70 | } 71 | 72 | # Check system requirements 73 | check_requirements() { 74 | if [ "$EUID" -ne 0 ]; then 75 | log_error "This script must be run as root (use sudo)" 76 | exit 1 77 | fi 78 | } 79 | 80 | # Setup Python virtual environment and install backend 81 | setup_backend() { 82 | log_info "Setting up backend..." 83 | 84 | cd ui/backend 85 | python3 -m venv venv 86 | source venv/bin/activate 87 | 88 | pip install --upgrade pip 89 | pip install -r requirements.txt 90 | 91 | cat > /etc/systemd/system/lightstack-backend.service << EOF 92 | [Unit] 93 | Description=Lightstack Backend 94 | After=network.target 95 | 96 | [Service] 97 | Type=simple 98 | User=root 99 | Group=root 100 | WorkingDirectory=$(pwd) 101 | ExecStart=$(pwd)/venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8005 102 | Restart=always 103 | RestartSec=5 104 | StartLimitInterval=500 105 | StartLimitBurst=5 106 | 107 | [Install] 108 | WantedBy=multi-user.target 109 | EOF 110 | 111 | systemctl daemon-reload 112 | systemctl enable lightstack-backend 113 | systemctl start lightstack-backend 114 | 115 | cd ../.. 116 | } 117 | 118 | # Build and setup frontend 119 | setup_frontend() { 120 | log_info "Setting up frontend..." 121 | 122 | cd ui/frontend 123 | 124 | # Load nvm 125 | export NVM_DIR="$HOME/.nvm" 126 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 127 | 128 | # Use the selected Node.js version 129 | nvm use "$(node --version | sed 's/v//')" 130 | 131 | rm -rf node_modules package-lock.json 132 | npm cache clean --force 133 | npm install 134 | npm run build 135 | 136 | mkdir -p /var/www/lightstack 137 | cp -r dist/* /var/www/lightstack/ 138 | 139 | chown -R www-data:www-data /var/www/lightstack 140 | chmod -R 755 /var/www/lightstack 141 | 142 | cd ../.. 143 | } 144 | 145 | # Setup nginx configuration 146 | setup_nginx() { 147 | local DOMAIN=$1 148 | log_info "Setting up nginx configuration for $DOMAIN..." 149 | 150 | mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled 151 | 152 | cat > /etc/nginx/sites-available/lightstack << EOF 153 | upstream backend { 154 | server 127.0.0.1:8005; 155 | } 156 | 157 | server { 158 | listen 8443 ssl; 159 | server_name ${DOMAIN}; 160 | 161 | ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; 162 | ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; 163 | include /etc/letsencrypt/options-ssl-nginx.conf; 164 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 165 | 166 | root /var/www/lightstack; 167 | 168 | location / { 169 | try_files \$uri \$uri/ /index.html; 170 | add_header Cache-Control "no-cache, no-store, must-revalidate"; 171 | } 172 | 173 | location /api/ { 174 | proxy_pass http://backend/; 175 | proxy_http_version 1.1; 176 | proxy_set_header Upgrade \$http_upgrade; 177 | proxy_set_header Connection 'upgrade'; 178 | proxy_set_header Host \$host; 179 | proxy_cache_bypass \$http_upgrade; 180 | } 181 | } 182 | EOF 183 | 184 | ln -sf /etc/nginx/sites-available/lightstack /etc/nginx/sites-enabled/ 185 | rm -f /etc/nginx/sites-enabled/default 186 | 187 | nginx -t 188 | systemctl reload nginx || systemctl start nginx 189 | } 190 | 191 | # Setup SSL certificates 192 | setup_ssl() { 193 | local DOMAIN=$1 194 | local EMAIL=$2 195 | log_info "Setting up SSL certificates..." 196 | 197 | apt-get install -y python3-certbot-nginx 198 | 199 | if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then 200 | log_info "SSL certificate already exists. Skipping renewal..." 201 | else 202 | certbot certonly --nginx --non-interactive --agree-tos --email "$EMAIL" --domains "$DOMAIN" 203 | fi 204 | } 205 | 206 | # Generate environment configuration 207 | generate_env() { 208 | log_info "Generating environment configuration..." 209 | 210 | JWT_SECRET=$(openssl rand -hex 32) 211 | 212 | cat > ui/backend/.env << EOF 213 | DOMAIN=$DOMAIN 214 | JWT_SECRET_KEY=$JWT_SECRET 215 | ADMIN_USER=$ADMIN_USER 216 | ADMIN_PASS=$ADMIN_PASS 217 | NODE_ENV=production 218 | EOF 219 | 220 | chmod 600 ui/backend/.env 221 | } 222 | 223 | # Main script 224 | main() { 225 | clear 226 | log_info "Installing Lightstack UI (Traditional Setup)" 227 | 228 | check_requirements 229 | install_dependencies 230 | install_nodejs 231 | 232 | read -p "Domain for Web UI (ex: manager.yourdomain.com): " DOMAIN 233 | read -p "Email for SSL certificates: " EMAIL 234 | read -p "Admin Username: " ADMIN_USER 235 | read -s -p "Admin Password: " ADMIN_PASS 236 | echo 237 | 238 | systemctl stop lightstack-backend 2>/dev/null || true 239 | 240 | generate_env 241 | setup_backend 242 | setup_frontend 243 | setup_ssl "$DOMAIN" "$EMAIL" 244 | setup_nginx "$DOMAIN" 245 | 246 | log_info "Installation completed successfully!" 247 | echo 248 | echo "Access at https://$DOMAIN:8443" 249 | echo "Username: $ADMIN_USER" 250 | } 251 | 252 | trap 'echo -e "\n${RED}Error: Installation interrupted${NC}"; exit 1' ERR 253 | main 254 | 255 | -------------------------------------------------------------------------------- /cloud-init.example: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | write_files: 3 | - path: /root/run_script.sh 4 | permissions: '0755' 5 | owner: root:root 6 | content: | 7 | #!/bin/bash 8 | export DOMAIN=test.gwoq.com 9 | export EMAIL=admin@gwoq.com 10 | export ADMIN_USER=admin 11 | export ADMIN_PASS=abc123 12 | export NODE_VERSION=20.9.0 13 | git clone https://github.com/massmux/lightstack.git /home/dev/lightstack 14 | cd /home/dev/lightstack 15 | chmod +x gui-install.sh 16 | chown -R dev:dev /home/dev/lightstack 17 | ./gui-install.sh 18 | 19 | - path: /usr/share/keyrings/docker.asc 20 | owner: root:root 21 | permissions: '0644' 22 | content: | 23 | -----BEGIN PGP PUBLIC KEY BLOCK----- 24 | 25 | mQINBFit2ioBEADhWpZ8/wvZ6hUTiXOwQHXMAlaFHcPH9hAtr4F1y2+OYdbtMuth 26 | lqqwp028AqyY+PRfVMtSYMbjuQuu5byyKR01BbqYhuS3jtqQmljZ/bJvXqnmiVXh 27 | 38UuLa+z077PxyxQhu5BbqntTPQMfiyqEiU+BKbq2WmANUKQf+1AmZY/IruOXbnq 28 | L4C1+gJ8vfmXQt99npCaxEjaNRVYfOS8QcixNzHUYnb6emjlANyEVlZzeqo7XKl7 29 | UrwV5inawTSzWNvtjEjj4nJL8NsLwscpLPQUhTQ+7BbQXAwAmeHCUTQIvvWXqw0N 30 | cmhh4HgeQscQHYgOJjjDVfoY5MucvglbIgCqfzAHW9jxmRL4qbMZj+b1XoePEtht 31 | ku4bIQN1X5P07fNWzlgaRL5Z4POXDDZTlIQ/El58j9kp4bnWRCJW0lya+f8ocodo 32 | vZZ+Doi+fy4D5ZGrL4XEcIQP/Lv5uFyf+kQtl/94VFYVJOleAv8W92KdgDkhTcTD 33 | G7c0tIkVEKNUq48b3aQ64NOZQW7fVjfoKwEZdOqPE72Pa45jrZzvUFxSpdiNk2tZ 34 | XYukHjlxxEgBdC/J3cMMNRE1F4NCA3ApfV1Y7/hTeOnmDuDYwr9/obA8t016Yljj 35 | q5rdkywPf4JF8mXUW5eCN1vAFHxeg9ZWemhBtQmGxXnw9M+z6hWwc6ahmwARAQAB 36 | tCtEb2NrZXIgUmVsZWFzZSAoQ0UgZGViKSA8ZG9ja2VyQGRvY2tlci5jb20+iQI3 37 | BBMBCgAhBQJYrefAAhsvBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEI2BgDwO 38 | v82IsskP/iQZo68flDQmNvn8X5XTd6RRaUH33kXYXquT6NkHJciS7E2gTJmqvMqd 39 | tI4mNYHCSEYxI5qrcYV5YqX9P6+Ko+vozo4nseUQLPH/ATQ4qL0Zok+1jkag3Lgk 40 | jonyUf9bwtWxFp05HC3GMHPhhcUSexCxQLQvnFWXD2sWLKivHp2fT8QbRGeZ+d3m 41 | 6fqcd5Fu7pxsqm0EUDK5NL+nPIgYhN+auTrhgzhK1CShfGccM/wfRlei9Utz6p9P 42 | XRKIlWnXtT4qNGZNTN0tR+NLG/6Bqd8OYBaFAUcue/w1VW6JQ2VGYZHnZu9S8LMc 43 | FYBa5Ig9PxwGQOgq6RDKDbV+PqTQT5EFMeR1mrjckk4DQJjbxeMZbiNMG5kGECA8 44 | g383P3elhn03WGbEEa4MNc3Z4+7c236QI3xWJfNPdUbXRaAwhy/6rTSFbzwKB0Jm 45 | ebwzQfwjQY6f55MiI/RqDCyuPj3r3jyVRkK86pQKBAJwFHyqj9KaKXMZjfVnowLh 46 | 9svIGfNbGHpucATqREvUHuQbNnqkCx8VVhtYkhDb9fEP2xBu5VvHbR+3nfVhMut5 47 | G34Ct5RS7Jt6LIfFdtcn8CaSas/l1HbiGeRgc70X/9aYx/V/CEJv0lIe8gP6uDoW 48 | FPIZ7d6vH+Vro6xuWEGiuMaiznap2KhZmpkgfupyFmplh0s6knymuQINBFit2ioB 49 | EADneL9S9m4vhU3blaRjVUUyJ7b/qTjcSylvCH5XUE6R2k+ckEZjfAMZPLpO+/tF 50 | M2JIJMD4SifKuS3xck9KtZGCufGmcwiLQRzeHF7vJUKrLD5RTkNi23ydvWZgPjtx 51 | Q+DTT1Zcn7BrQFY6FgnRoUVIxwtdw1bMY/89rsFgS5wwuMESd3Q2RYgb7EOFOpnu 52 | w6da7WakWf4IhnF5nsNYGDVaIHzpiqCl+uTbf1epCjrOlIzkZ3Z3Yk5CM/TiFzPk 53 | z2lLz89cpD8U+NtCsfagWWfjd2U3jDapgH+7nQnCEWpROtzaKHG6lA3pXdix5zG8 54 | eRc6/0IbUSWvfjKxLLPfNeCS2pCL3IeEI5nothEEYdQH6szpLog79xB9dVnJyKJb 55 | VfxXnseoYqVrRz2VVbUI5Blwm6B40E3eGVfUQWiux54DspyVMMk41Mx7QJ3iynIa 56 | 1N4ZAqVMAEruyXTRTxc9XW0tYhDMA/1GYvz0EmFpm8LzTHA6sFVtPm/ZlNCX6P1X 57 | zJwrv7DSQKD6GGlBQUX+OeEJ8tTkkf8QTJSPUdh8P8YxDFS5EOGAvhhpMBYD42kQ 58 | pqXjEC+XcycTvGI7impgv9PDY1RCC1zkBjKPa120rNhv/hkVk/YhuGoajoHyy4h7 59 | ZQopdcMtpN2dgmhEegny9JCSwxfQmQ0zK0g7m6SHiKMwjwARAQABiQQ+BBgBCAAJ 60 | BQJYrdoqAhsCAikJEI2BgDwOv82IwV0gBBkBCAAGBQJYrdoqAAoJEH6gqcPyc/zY 61 | 1WAP/2wJ+R0gE6qsce3rjaIz58PJmc8goKrir5hnElWhPgbq7cYIsW5qiFyLhkdp 62 | YcMmhD9mRiPpQn6Ya2w3e3B8zfIVKipbMBnke/ytZ9M7qHmDCcjoiSmwEXN3wKYI 63 | mD9VHONsl/CG1rU9Isw1jtB5g1YxuBA7M/m36XN6x2u+NtNMDB9P56yc4gfsZVES 64 | KA9v+yY2/l45L8d/WUkUi0YXomn6hyBGI7JrBLq0CX37GEYP6O9rrKipfz73XfO7 65 | JIGzOKZlljb/D9RX/g7nRbCn+3EtH7xnk+TK/50euEKw8SMUg147sJTcpQmv6UzZ 66 | cM4JgL0HbHVCojV4C/plELwMddALOFeYQzTif6sMRPf+3DSj8frbInjChC3yOLy0 67 | 6br92KFom17EIj2CAcoeq7UPhi2oouYBwPxh5ytdehJkoo+sN7RIWua6P2WSmon5 68 | U888cSylXC0+ADFdgLX9K2zrDVYUG1vo8CX0vzxFBaHwN6Px26fhIT1/hYUHQR1z 69 | VfNDcyQmXqkOnZvvoMfz/Q0s9BhFJ/zU6AgQbIZE/hm1spsfgvtsD1frZfygXJ9f 70 | irP+MSAI80xHSf91qSRZOj4Pl3ZJNbq4yYxv0b1pkMqeGdjdCYhLU+LZ4wbQmpCk 71 | SVe2prlLureigXtmZfkqevRz7FrIZiu9ky8wnCAPwC7/zmS18rgP/17bOtL4/iIz 72 | QhxAAoAMWVrGyJivSkjhSGx1uCojsWfsTAm11P7jsruIL61ZzMUVE2aM3Pmj5G+W 73 | 9AcZ58Em+1WsVnAXdUR//bMmhyr8wL/G1YO1V3JEJTRdxsSxdYa4deGBBY/Adpsw 74 | 24jxhOJR+lsJpqIUeb999+R8euDhRHG9eFO7DRu6weatUJ6suupoDTRWtr/4yGqe 75 | dKxV3qQhNLSnaAzqW/1nA3iUB4k7kCaKZxhdhDbClf9P37qaRW467BLCVO/coL3y 76 | Vm50dwdrNtKpMBh3ZpbB1uJvgi9mXtyBOMJ3v8RZeDzFiG8HdCtg9RvIt/AIFoHR 77 | H3S+U79NT6i0KPzLImDfs8T7RlpyuMc4Ufs8ggyg9v3Ae6cN3eQyxcK3w0cbBwsh 78 | /nQNfsA6uu+9H7NhbehBMhYnpNZyrHzCmzyXkauwRAqoCbGCNykTRwsur9gS41TQ 79 | M8ssD1jFheOJf3hODnkKU+HKjvMROl1DK7zdmLdNzA1cvtZH/nCC9KPj1z8QC47S 80 | xx+dTZSx4ONAhwbS/LN3PoKtn8LPjY9NP9uDWI+TWYquS2U+KHDrBDlsgozDbs/O 81 | jCxcpDzNmXpWQHEtHU7649OXHP7UeNST1mCUCH5qdank0V1iejF6/CfTFU4MfcrG 82 | YT90qFF93M3v01BbxP+EIY2/9tiIPbrd 83 | =0YYh 84 | -----END PGP PUBLIC KEY BLOCK----- 85 | 86 | 87 | apt: 88 | sources: 89 | docker.list: 90 | source: deb [arch=amd64 signed-by=/usr/share/keyrings/docker.asc] https://download.docker.com/linux/debian $RELEASE stable 91 | 92 | package_update: true 93 | package_upgrade: true 94 | 95 | packages: 96 | - docker-ce 97 | - docker-ce-cli 98 | - containerd.io 99 | - docker-buildx-plugin 100 | - docker-compose-plugin 101 | - htop 102 | - nload 103 | - sysstat 104 | - pydf 105 | - ncdu 106 | - byobu 107 | - vim 108 | - ufw 109 | - console-data 110 | - git 111 | - wget 112 | 113 | 114 | # create the docker group 115 | groups: 116 | - docker 117 | 118 | users: 119 | - name: dev 120 | groups: docker 121 | home: /home/dev 122 | shell: /bin/bash 123 | sudo: ALL=(ALL) NOPASSWD:ALL 124 | ssh-authorized-keys: ssh-rsa AAAAB3NXXXXXXXXXXXXXXXX 125 | 126 | # Add default auto created user to docker group 127 | system_info: 128 | default_user: 129 | groups: [docker] 130 | 131 | runcmd: 132 | # enable firewall 133 | - ufw default deny incoming 134 | - ufw default allow outgoing 135 | - ufw allow 22 comment "Allow SSH for server management" 136 | - ufw allow 80 comment "Allow 80 for certbot" 137 | - ufw allow 443 comment "Allow 443" 138 | - ufw allow 8443 comment "Allow for web UI" 139 | - ufw enable 140 | 141 | # fix vim pasting issue 142 | - echo "set mouse=" >> /home/dev/.vimrc 143 | - chown dev:dev /home/dev/.vimrc 144 | - echo "set mouse=" > /root/.vimrc 145 | 146 | # Fix vim syntax highlighting 147 | - /usr/bin/sed -i 's;"syntax on;syntax on;' /etc/vim/vimrc 148 | 149 | # Example of pulling and running a docker image. 150 | - /usr/bin/sleep 10 151 | - /usr/bin/docker pull hello-world 152 | - /usr/bin/docker run hello-world 153 | 154 | # configure openssh for security 155 | - sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config 156 | - sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config 157 | - sed -i -e '/^\(#\|\)KbdInteractiveAuthentication/s/^.*$/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config 158 | - sed -i -e '/^\(#\|\)ChallengeResponseAuthentication/s/^.*$/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config 159 | - sed -i -e '/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 2/' /etc/ssh/sshd_config 160 | - sed -i -e '/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/' /etc/ssh/sshd_config 161 | - sed -i -e '/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/' /etc/ssh/sshd_config 162 | - sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config 163 | - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config 164 | - sed -i '$a AllowUsers dev' /etc/ssh/sshd_config 165 | 166 | # install lightstack gui 167 | - /root/run_script.sh 168 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | cd "$(dirname "$0")" 5 | 6 | # Colors for output 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | RED='\033[0;31m' 10 | NC='\033[0m' 11 | 12 | # Logging functions 13 | log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } 14 | log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } 15 | log_error() { echo -e "${RED}[ERROR]${NC} $1"; } 16 | 17 | # Funzione per installare Node.js 18 | install_nodejs() { 19 | log_info "Installazione Node.js 20.x..." 20 | 21 | # Rimuovi versioni precedenti di Node.js se presenti 22 | apt-get remove -y nodejs npm || true 23 | 24 | # Aggiungi il repository NodeSource per Node.js 20.x 25 | curl -fsSL https://deb.nodesource.com/setup_20.x | bash - 26 | 27 | # Installa Node.js 28 | apt-get install -y nodejs 29 | 30 | # Verifica le versioni 31 | log_info "Versione Node.js installata:" 32 | node --version 33 | npm --version 34 | } 35 | 36 | # Install system dependencies 37 | install_dependencies() { 38 | log_info "Aggiornamento del sistema e installazione dipendenze..." 39 | 40 | # Aggiorna i repository 41 | apt-get update 42 | 43 | # Installa le dipendenze di sistema 44 | apt-get install -y \ 45 | python3 \ 46 | python3-pip \ 47 | python3-venv \ 48 | nginx \ 49 | certbot \ 50 | python3-certbot-nginx \ 51 | curl \ 52 | git \ 53 | build-essential \ 54 | openssl 55 | 56 | # Installa Node.js 57 | install_nodejs 58 | } 59 | 60 | # Check system requirements 61 | check_requirements() { 62 | if [ "$EUID" -ne 0 ]; then 63 | log_error "This script must be run as root (use sudo)" 64 | exit 1 65 | fi 66 | } 67 | 68 | # Setup Python virtual environment and install backend 69 | setup_backend() { 70 | log_info "Setting up backend..." 71 | 72 | # Create and activate virtual environment 73 | cd ui/backend 74 | python3 -m venv venv 75 | source venv/bin/activate 76 | 77 | # Install requirements 78 | pip install --upgrade pip 79 | pip install -r requirements.txt 80 | 81 | # Create systemd service for backend 82 | cat > /etc/systemd/system/lightstack-backend.service << EOF 83 | [Unit] 84 | Description=Lightstack Backend 85 | After=network.target 86 | 87 | [Service] 88 | Type=simple 89 | User=root 90 | Group=root 91 | WorkingDirectory=$(pwd) 92 | ExecStart=$(pwd)/venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8005 93 | Restart=always 94 | RestartSec=10 95 | 96 | [Install] 97 | WantedBy=multi-user.target 98 | EOF 99 | 100 | # Reload systemd and start service 101 | systemctl daemon-reload 102 | systemctl enable lightstack-backend 103 | systemctl start lightstack-backend 104 | 105 | cd ../.. 106 | } 107 | 108 | # Build and setup frontend 109 | setup_frontend() { 110 | log_info "Setting up frontend..." 111 | 112 | cd ui/frontend 113 | 114 | # Pulisci l'installazione npm se esiste 115 | rm -rf node_modules package-lock.json 116 | 117 | # Installa le dipendenze e costruisci 118 | npm cache clean --force 119 | npm install 120 | npm run build 121 | 122 | # Move built files to nginx directory 123 | mkdir -p /var/www/lightstack 124 | cp -r dist/* /var/www/lightstack/ 125 | 126 | # Set correct permissions 127 | chown -R www-data:www-data /var/www/lightstack 128 | chmod -R 755 /var/www/lightstack 129 | 130 | cd ../.. 131 | } 132 | 133 | # Setup nginx configuration 134 | setup_nginx() { 135 | local DOMAIN=$1 136 | log_info "Setting up nginx configuration for $DOMAIN..." 137 | 138 | # Assicurati che la directory sites-available esista 139 | mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled 140 | 141 | # Create nginx configuration 142 | cat > /etc/nginx/sites-available/lightstack << EOF 143 | upstream backend { 144 | server 127.0.0.1:8005; 145 | } 146 | 147 | server { 148 | listen 8443 ssl; 149 | server_name ${DOMAIN}; 150 | 151 | ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; 152 | ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; 153 | include /etc/letsencrypt/options-ssl-nginx.conf; 154 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 155 | 156 | root /var/www/lightstack; 157 | 158 | location / { 159 | try_files \$uri \$uri/ /index.html; 160 | add_header Cache-Control "no-cache, no-store, must-revalidate"; 161 | proxy_connect_timeout 300; 162 | proxy_send_timeout 300; 163 | proxy_read_timeout 300; 164 | } 165 | 166 | location /api/ { 167 | proxy_pass http://backend/; 168 | proxy_http_version 1.1; 169 | proxy_set_header Upgrade \$http_upgrade; 170 | proxy_set_header Connection 'upgrade'; 171 | proxy_set_header Host \$host; 172 | proxy_cache_bypass \$http_upgrade; 173 | proxy_set_header X-Real-IP \$remote_addr; 174 | proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; 175 | proxy_set_header X-Forwarded-Proto \$scheme; 176 | proxy_connect_timeout 300; 177 | proxy_send_timeout 300; 178 | proxy_read_timeout 300; 179 | } 180 | } 181 | EOF 182 | 183 | # Enable site 184 | ln -sf /etc/nginx/sites-available/lightstack /etc/nginx/sites-enabled/ 185 | 186 | # Remove default nginx site if exists 187 | rm -f /etc/nginx/sites-enabled/default 188 | 189 | # Test nginx configuration 190 | nginx -t 191 | 192 | # Reload nginx 193 | systemctl reload nginx || systemctl start nginx 194 | } 195 | 196 | # Setup SSL certificates 197 | setup_ssl() { 198 | local DOMAIN=$1 199 | local EMAIL=$2 200 | log_info "Setting up SSL certificates..." 201 | 202 | # Install certbot nginx plugin if not already installed 203 | apt-get install -y python3-certbot-nginx 204 | 205 | # Generate certificates 206 | certbot certonly --nginx \ 207 | --non-interactive --agree-tos \ 208 | --email "$EMAIL" \ 209 | --domains "$DOMAIN" 210 | } 211 | 212 | # Generate environment configuration 213 | generate_env() { 214 | log_info "Generating environment configuration..." 215 | 216 | JWT_SECRET=$(openssl rand -hex 32) 217 | 218 | cat > ui/backend/.env << EOF 219 | DOMAIN=$DOMAIN 220 | JWT_SECRET_KEY=$JWT_SECRET 221 | ADMIN_USER=$ADMIN_USER 222 | ADMIN_PASS=$ADMIN_PASS 223 | NODE_ENV=production 224 | EOF 225 | 226 | chmod 600 ui/backend/.env 227 | } 228 | 229 | # Main script 230 | main() { 231 | clear 232 | log_info "Installing Lightstack UI (Traditional Setup)" 233 | 234 | # Check requirements 235 | check_requirements 236 | 237 | # Install dependencies 238 | install_dependencies 239 | 240 | # Get configuration input 241 | read -p "Domain for web interface (e.g., manager.yourdomain.com): " DOMAIN 242 | read -p "Email for SSL certificates: " EMAIL 243 | read -p "Admin username: " ADMIN_USER 244 | read -s -p "Admin password: " ADMIN_PASS 245 | echo 246 | 247 | # Stop existing services if running 248 | systemctl stop lightstack-backend 2>/dev/null || true 249 | 250 | # Generate configurations 251 | generate_env 252 | 253 | # Setup components 254 | setup_backend 255 | setup_frontend 256 | setup_ssl "$DOMAIN" "$EMAIL" 257 | setup_nginx "$DOMAIN" 258 | 259 | log_info "Installation completed successfully!" 260 | echo 261 | echo "Access at https://$DOMAIN:8443" 262 | echo "Username: $ADMIN_USER" 263 | echo 264 | log_info "Useful commands:" 265 | echo "- View backend logs: journalctl -u lightstack-backend -f" 266 | echo "- Restart backend: systemctl restart lightstack-backend" 267 | echo "- Check nginx status: systemctl status nginx" 268 | } 269 | 270 | # Error handling 271 | trap 'echo -e "\n${RED}Error: Installation interrupted${NC}"; exit 1' ERR 272 | 273 | # Start installation 274 | main 275 | -------------------------------------------------------------------------------- /initlib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Update or add a variable in the .env file 4 | # 5 | 6 | # Function to generate a random password 7 | generate_password() { 8 | openssl rand -base64 12 | tr -d "=+/" | cut -c1-16 9 | } 10 | 11 | update_env() { 12 | local key=$1 13 | local value=$2 14 | local file="$STACK/.env" 15 | if grep -q "^$key=" "$file"; then 16 | sed -i "s|^$key=.*|$key=$value|" "$file" 17 | else 18 | echo "$key=$value" >> "$file" 19 | fi 20 | } 21 | 22 | # Wait for a container to be ready 23 | wait_for_container() { 24 | echo "Waiting for $1 to be ready..." 25 | until [ "`docker inspect --format=\"{{.State.Running}}\" $1`"=="true" ]; do 26 | sleep 1; 27 | done; 28 | sleep 2; 29 | echo "$1 is ready." 30 | } 31 | 32 | # Generate self-signed certificates 33 | generate_certificates() { 34 | local phoenixd_domain=$1 35 | local lnbits_domain=$2 36 | local cert_dir="letsencrypt/live" 37 | 38 | echo "Generating self-signed certificates for testing..." 39 | 40 | # Create necessary directories 41 | mkdir -p "$cert_dir/$phoenixd_domain" 42 | mkdir -p "$cert_dir/$lnbits_domain" 43 | 44 | # Generate certificates for Phoenixd domain 45 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ 46 | -keyout "$cert_dir/$phoenixd_domain/privkey.pem" \ 47 | -out "$cert_dir/$phoenixd_domain/fullchain.pem" \ 48 | -subj "/CN=$phoenixd_domain" 2>/dev/null 49 | 50 | if [ $? -eq 0 ]; then 51 | echo "Certificates for $phoenixd_domain generated successfully." 52 | else 53 | echo "An error occurred while generating certificates for $phoenixd_domain." 54 | exit 1 55 | fi 56 | 57 | # Generate certificates for LNbits domain 58 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ 59 | -keyout "$cert_dir/$lnbits_domain/privkey.pem" \ 60 | -out "$cert_dir/$lnbits_domain/fullchain.pem" \ 61 | -subj "/CN=$lnbits_domain" 2>/dev/null 62 | 63 | if [ $? -eq 0 ]; then 64 | echo "Certificates for $lnbits_domain generated successfully." 65 | else 66 | echo "An error occurred while generating certificates for $lnbits_domain." 67 | exit 1 68 | fi 69 | 70 | echo "Self-signed certificates generated successfully for testing." 71 | } 72 | 73 | # Generate Letsencrypt certificates 74 | generate_certificates_certbot() { 75 | local phoenixd_domain=$1 76 | local lnbits_domain=$2 77 | local cert_email=$3 # Aggiungiamo l'email come terzo parametro 78 | 79 | echo "Generating valid certificates using Certbot..." 80 | echo "DEBUG: Using email: $cert_email" >&2 81 | 82 | # Generate certificate for Phoenixd domain 83 | echo "Generating certificate for $phoenixd_domain" 84 | sudo certbot certonly --standalone -d "$phoenixd_domain" --email "$cert_email" --agree-tos --non-interactive 85 | 86 | if [ $? -eq 0 ]; then 87 | echo "Certificate for $phoenixd_domain generated successfully." 88 | else 89 | echo "An error occurred while generating certificate for $phoenixd_domain." 90 | exit 1 91 | fi 92 | 93 | # Generate certificate for LNbits domain 94 | echo "Generating certificate for $lnbits_domain" 95 | sudo certbot certonly --standalone -d "$lnbits_domain" --email "$cert_email" --agree-tos --non-interactive 96 | 97 | if [ $? -eq 0 ]; then 98 | echo "Certificate for $lnbits_domain generated successfully." 99 | else 100 | echo "An error occurred while generating certificate for $lnbits_domain." 101 | exit 1 102 | fi 103 | 104 | echo "Valid certificates generated successfully using Certbot." 105 | echo "Copying letsencrypt dir..." 106 | sudo cp -R /etc/letsencrypt . 107 | } 108 | 109 | # calculate the next stack id 110 | generate_stack_id() { 111 | local sid=$( find ./ -type d -name "stack_*" | sed 's/^.*stack_//' | sort -n | tail -1 ) 112 | if [[ -n $sid ]] 113 | then 114 | echo $(( $sid + 1 )) 115 | else 116 | echo 1 117 | fi 118 | } 119 | 120 | # print active stacks 121 | print_stacks() { 122 | for sid in $( find ./ -type d -name "stack_*" | sed 's/^.*stack_//' ) 123 | do 124 | local domains=$(grep "server_name" "nginx/stack_$sid.conf" | sed -e 's/^.*server_name *//' -e 's/;//' | tr '\n' ' ') 125 | echo "$sid $domains" 126 | done 127 | } 128 | 129 | # print script help 130 | print_help(){ 131 | echo 132 | echo "Usage: init.sh [command]" 133 | echo " command:" 134 | echo " add [DEFAULT]: to init a new system and/or add a new stack" 135 | echo " clear: to remove all stacks" 136 | echo " del|rem: to remove a stack" 137 | echo " help: to show this message" 138 | } 139 | 140 | # Restore on error during migration 141 | migration_trap() { 142 | local exit_code=$? 143 | 144 | trap - SIGINT SIGQUIT SIGTERM 145 | 146 | if [[ $exit_code -eq 0 ]]; then 147 | rm -rf .backup 148 | else 149 | echo 150 | echo "***An error occurred during the migration process***" 151 | echo "Your previous stack will be restored." 152 | echo 153 | 154 | if [[ -f docker-compose.yml ]]; then 155 | # Stop all containers 156 | echo "Stopping all containers..." 157 | docker compose down 158 | echo "All containers have been stopped." 159 | fi 160 | 161 | # Restore configurations and data 162 | echo "Restoring configurations and data" 163 | rm -rf nginx $STACK docker-compose.yml 164 | cd .backup 165 | mv -t ../ data lnbitsdata .env default.conf docker-compose.yml 166 | if [[ -d pgdata ]]; then 167 | mv pgdata ../ 168 | fi 169 | if [[ -d pgtmp ]]; then 170 | mv pgtmp ../ 171 | fi 172 | cd ../ 173 | rm -rf .backup 174 | echo "Restore completed." 175 | 176 | # Start all containers 177 | echo "Starting all containers..." 178 | docker compose up -d 179 | echo "All containers have been started." 180 | echo 181 | 182 | echo "***Migration failed with code $exit_code***" 183 | echo "Your previous system was restored and is now ready for use." 184 | echo "You can save logs and notify the issue at https://github.com/massmux/lightstack/issues." 185 | fi 186 | 187 | exit $exit_code 188 | } 189 | 190 | # Restore on error during init/add 191 | init_trap() { 192 | local exit_code=$? 193 | 194 | trap - SIGINT SIGQUIT SIGTERM 195 | 196 | if [[ ! $exit_code -eq 0 ]]; then 197 | echo 198 | echo "***An error occurred during the init process***" 199 | echo "Cleaning up..." 200 | 201 | # Stop containers 202 | echo "Stopping $STACK containers..." 203 | if docker ps | grep -E "lightstack-(lnbits|phoenixd|postgres)-$SID"; then 204 | docker rm -f $( docker ps | grep -E "lightstack-(lnbits|phoenixd|postgres)-$SID" | awk '{print $1}' ) 205 | fi 206 | docker compose down nginx 207 | echo "$STACK containers have been stopped." 208 | 209 | echo "Removing $STACK data..." 210 | rm -rf $STACK nginx/$STACK.conf 211 | sed -i "/$STACK/d" docker-compose.yml 212 | if [[ $( print_stacks | wc -l ) -eq 0 ]]; then 213 | rm -rf letsencrypt nginx docker-compose.yml 214 | fi 215 | echo "$STACK data successfully removed." 216 | 217 | if [[ $( print_stacks | wc -l ) -gt 0 ]]; then 218 | # Restarting nginx 219 | echo "Restarting nginx container..." 220 | docker compose up -d 221 | echo "nginx container restarted" 222 | fi 223 | 224 | echo "Cleanup completed." 225 | echo 226 | echo "***Init failed with code $exit_code***" 227 | echo "You can save logs and notify the issue at https://github.com/massmux/lightstack/issues." 228 | fi 229 | 230 | exit $exit_code 231 | } 232 | 233 | ## Functions section end. 234 | 235 | -------------------------------------------------------------------------------- /.env.sqlite.example: -------------------------------------------------------------------------------- 1 | #For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/ 2 | 3 | ###################################### 4 | ########### Admin Settings ########### 5 | ###################################### 6 | 7 | # Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available. 8 | # Warning: Enabling this will make LNbits ignore most configurations in file. Only the 9 | # configurations defined in `ReadOnlySettings` will still be read from the environment variables. 10 | # The rest of the settings will be stored in your database and you will be able to change them 11 | # only through the Admin UI. 12 | # Disable this to make LNbits use this config file again. 13 | LNBITS_ADMIN_UI=false 14 | 15 | # Change theme 16 | LNBITS_SITE_TITLE="LNbits" 17 | LNBITS_SITE_TAGLINE="free and open-source lightning wallet" 18 | LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack." 19 | # Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber 20 | LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber" 21 | # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" 22 | 23 | HOST=127.0.0.1 24 | PORT=5000 25 | 26 | ###################################### 27 | ########## Funding Source ############ 28 | ###################################### 29 | 30 | # which fundingsources are allowed in the admin ui 31 | # LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet" 32 | 33 | LNBITS_BACKEND_WALLET_CLASS=VoidWallet 34 | # VoidWallet is just a fallback that works without any actual Lightning capabilities, 35 | # just so you can see the UI before dealing with this file. 36 | 37 | # How many times to retry connectiong to the Funding Source before defaulting to the VoidWallet 38 | # FUNDING_SOURCE_MAX_RETRIES=4 39 | 40 | # Invoice expiry for LND, CLN, Eclair, LNbits funding sources 41 | LIGHTNING_INVOICE_EXPIRY=3600 42 | 43 | # Set one of these blocks depending on the wallet kind you chose above: 44 | 45 | # ClicheWallet 46 | CLICHE_ENDPOINT=ws://127.0.0.1:12000 47 | 48 | # SparkWallet 49 | SPARK_URL=http://localhost:9737/rpc 50 | SPARK_TOKEN=myaccesstoken 51 | 52 | # CoreLightningWallet 53 | CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc" 54 | 55 | # CoreLightningRestWallet 56 | CORELIGHTNING_REST_URL=http://127.0.0.1:8185/ 57 | CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon" # or BASE64/HEXSTRING 58 | CORELIGHTNING_REST_CERT="/path/to/clnrest/tls.cert" 59 | 60 | # LnbitsWallet 61 | LNBITS_ENDPOINT=https://demo.lnbits.com 62 | LNBITS_KEY=LNBITS_ADMIN_KEY 63 | 64 | # LndWallet 65 | LND_GRPC_ENDPOINT=127.0.0.1 66 | LND_GRPC_PORT=10009 67 | LND_GRPC_CERT="/home/bob/.lnd/tls.cert" 68 | LND_GRPC_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING 69 | # To use an AES-encrypted macaroon, set 70 | # LND_GRPC_MACAROON="eNcRyPtEdMaCaRoOn" 71 | 72 | # LndRestWallet 73 | LND_REST_ENDPOINT=https://127.0.0.1:8080/ 74 | LND_REST_CERT="/home/bob/.lnd/tls.cert" 75 | LND_REST_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING 76 | # To use an AES-encrypted macaroon, set 77 | # LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn" 78 | 79 | # LNPayWallet 80 | LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/ 81 | # Secret API Key under developers tab 82 | LNPAY_API_KEY=LNPAY_API_KEY 83 | # Wallet Admin in Wallet Access Keys 84 | LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY 85 | 86 | # AlbyWallet 87 | ALBY_API_ENDPOINT=https://api.getalby.com/ 88 | ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN 89 | 90 | # BoltzWallet 91 | BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002 92 | BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroon" # or HEXSTRING 93 | BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert" # or HEXSTRING 94 | BOLTZ_CLIENT_WALLET="lnbits" 95 | 96 | # ZBDWallet 97 | ZBD_API_ENDPOINT=https://api.zebedee.io/v0/ 98 | ZBD_API_KEY=ZBD_ACCESS_TOKEN 99 | 100 | # BlinkWallet 101 | BLINK_API_ENDPOINT=https://api.blink.sv/graphql 102 | BLINK_WS_ENDPOINT=wss://ws.blink.sv/graphql 103 | BLINK_TOKEN=BLINK_TOKEN 104 | 105 | # PhoenixdWallet 106 | PHOENIXD_API_ENDPOINT=http://localhost:9740/ 107 | PHOENIXD_API_PASSWORD=PHOENIXD_KEY 108 | 109 | # OpenNodeWallet 110 | OPENNODE_API_ENDPOINT=https://api.opennode.com/ 111 | OPENNODE_KEY=OPENNODE_ADMIN_KEY 112 | 113 | # FakeWallet 114 | FAKE_WALLET_SECRET="ToTheMoon1" 115 | LNBITS_DENOMINATION=sats 116 | 117 | # EclairWallet 118 | ECLAIR_URL=http://127.0.0.1:8283 119 | ECLAIR_PASS=eclairpw 120 | 121 | # NWCWalllet 122 | NWC_PAIRING_URL="nostr+walletconnect://000...000?relay=example.com&secret=123" 123 | 124 | # LnTipsWallet 125 | # Enter /api in LightningTipBot to get your key 126 | LNTIPS_API_KEY=LNTIPS_ADMIN_KEY 127 | LNTIPS_API_ENDPOINT=https://ln.tips 128 | 129 | # BreezSdkWallet 130 | BREEZ_API_KEY=KEY 131 | BREEZ_GREENLIGHT_SEED=SEED 132 | # A Greenlight invite code or Greenlight partner certificate/key can be used 133 | BREEZ_GREENLIGHT_INVITE_CODE=CODE 134 | BREEZ_GREENLIGHT_DEVICE_KEY="/path/to/breezsdk/device.pem" # or BASE64/HEXSTRING 135 | BREEZ_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING 136 | 137 | ###################################### 138 | ####### Auth Configurations ########## 139 | ###################################### 140 | # Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value. 141 | AUTH_SECRET_KEY="" 142 | AUTH_TOKEN_EXPIRE_MINUTES=525600 143 | # Possible authorization methods: user-id-only, username-password, google-auth, github-auth, keycloak-auth 144 | AUTH_ALLOWED_METHODS="user-id-only, username-password" 145 | # Set this flag if HTTP is used for OAuth 146 | # OAUTHLIB_INSECURE_TRANSPORT="1" 147 | 148 | # Google OAuth Config 149 | # Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token 150 | GOOGLE_CLIENT_ID="" 151 | GOOGLE_CLIENT_SECRET="" 152 | 153 | # GitHub OAuth Config 154 | # Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token 155 | GITHUB_CLIENT_ID="" 156 | GITHUB_CLIENT_SECRET="" 157 | 158 | # Keycloak OAuth Config 159 | # Make sure that the valid redirect URIs contain https://{domain}/api/v1/auth/keycloak/token 160 | KEYCLOAK_CLIENT_ID="" 161 | KEYCLOAK_CLIENT_SECRET="" 162 | KEYCLOAK_DISCOVERY_URL="" 163 | 164 | 165 | ###################################### 166 | 167 | # uvicorn variable, uncomment to allow https behind a proxy 168 | # IMPORTANT: this also needs the webserver to be configured to forward the headers 169 | # http://docs.lnbits.org/guide/installation.html#running-behind-an-apache2-reverse-proxy-over-https 170 | # FORWARDED_ALLOW_IPS="*" 171 | 172 | # Server security, rate limiting ips, blocked ips, allowed ips 173 | LNBITS_RATE_LIMIT_NO="200" 174 | LNBITS_RATE_LIMIT_UNIT="minute" 175 | LNBITS_ALLOWED_IPS="" 176 | LNBITS_BLOCKED_IPS="" 177 | 178 | # Allow users and admins by user IDs (comma separated list) 179 | # if set new users will not be able to create accounts 180 | LNBITS_ALLOWED_USERS="" 181 | LNBITS_ADMIN_USERS="" 182 | # ID of the super user. The user ID must exist. 183 | # SUPER_USER="" 184 | 185 | # Extensions only admin can access 186 | LNBITS_ADMIN_EXTENSIONS="ngrok, admin" 187 | # Extensions enabled by default when a user is created 188 | LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp" 189 | 190 | # Start LNbits core only. The extensions are not loaded. 191 | # LNBITS_EXTENSIONS_DEACTIVATE_ALL=true 192 | 193 | # Disable account creation for new users 194 | # LNBITS_ALLOW_NEW_ACCOUNTS=false 195 | 196 | # Enable Node Management without activating the LNBITS Admin GUI 197 | # by setting the following variables to true. 198 | LNBITS_NODE_UI=false 199 | LNBITS_PUBLIC_NODE_UI=false 200 | # Enabling the transactions tab can cause crashes on large Core Lightning nodes. 201 | LNBITS_NODE_UI_TRANSACTIONS=false 202 | 203 | LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" 204 | 205 | # Ad space description 206 | # LNBITS_AD_SPACE_TITLE="Supported by" 207 | # csv ad space, format ";;, ;;", extensions can choose to honor 208 | # LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png" 209 | # LNBITS_SHOW_HOME_PAGE_ELEMENTS=true # if set to true, the ad space will be displayed on the home page 210 | # LNBITS_CUSTOM_BADGE="USE WITH CAUTION - LNbits wallet is still in BETA" 211 | # LNBITS_CUSTOM_BADGE_COLOR="warning" 212 | 213 | # Hides wallet api, extensions can choose to honor 214 | LNBITS_HIDE_API=false 215 | 216 | # LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json" 217 | # GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN 218 | # LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx 219 | 220 | # Path where extensions will be installed (defaults to `./lnbits/`). 221 | # Inside this directory the `extensions` and `upgrades` sub-directories will be created. 222 | # LNBITS_EXTENSIONS_PATH="/path/to/some/dir" 223 | 224 | # Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart. 225 | # The extension must be removed from this list in order to not be re-installed. 226 | LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos" 227 | 228 | # Database: to use SQLite, specify LNBITS_DATA_FOLDER 229 | # to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... 230 | # to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://... 231 | # for both PostgreSQL and CockroachDB, you'll need to install 232 | # psycopg2 as an additional dependency 233 | LNBITS_DATA_FOLDER="./data" 234 | # LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename" 235 | 236 | # the service fee (in percent) 237 | LNBITS_SERVICE_FEE=0.0 238 | # the wallet where fees go to 239 | # LNBITS_SERVICE_FEE_WALLET= 240 | # the maximum fee per transaction (in satoshis) 241 | # LNBITS_SERVICE_FEE_MAX=1000 242 | # disable fees for internal transactions 243 | # LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true 244 | 245 | # value in millisats 246 | LNBITS_RESERVE_FEE_MIN=2000 247 | # value in percent 248 | LNBITS_RESERVE_FEE_PERCENT=1.0 249 | 250 | # limit the maximum balance for each wallet 251 | # throw an error if the wallet attempts to create a new invoice 252 | 253 | # LNBITS_WALLET_LIMIT_MAX_BALANCE=1000000 254 | # LNBITS_WALLET_LIMIT_DAILY_MAX_WITHDRAW=1000000 255 | # LNBITS_WALLET_LIMIT_SECS_BETWEEN_TRANS=60 256 | 257 | # Limit fiat currencies allowed to see in UI 258 | # LNBITS_ALLOWED_CURRENCIES="EUR, USD" 259 | 260 | ###################################### 261 | ###### Logging and Development ####### 262 | ###################################### 263 | 264 | DEBUG=false 265 | DEBUG_DATABASE=false 266 | BUNDLE_ASSETS=true 267 | 268 | # logging into LNBITS_DATA_FOLDER/logs/ 269 | ENABLE_LOG_TO_FILE=true 270 | 271 | # https://loguru.readthedocs.io/en/stable/api/logger.html#file 272 | LOG_ROTATION="100 MB" 273 | LOG_RETENTION="3 months" 274 | 275 | # for database cleanup commands 276 | # CLEANUP_WALLETS_DAYS=90 277 | 278 | # PhoenixdWallet 279 | LNBITS_BACKEND_WALLET_CLASS=PhoenixdWallet 280 | PHOENIXD_API_ENDPOINT=http://phoenixd:9740/ 281 | PHOENIXD_API_PASSWORD=YYY 282 | 283 | LNBITS_SITE_TITLE="lb1.yourdomain.com" 284 | LNBITS_SITE_TAGLINE="free and open-source lightning wallet" 285 | LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack." 286 | 287 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/ 2 | 3 | ###################################### 4 | ########### Admin Settings ########### 5 | ###################################### 6 | 7 | # Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available. 8 | # Warning: Enabling this will make LNbits ignore most configurations in file. Only the 9 | # configurations defined in `ReadOnlySettings` will still be read from the environment variables. 10 | # The rest of the settings will be stored in your database and you will be able to change them 11 | # only through the Admin UI. 12 | # Disable this to make LNbits use this config file again. 13 | LNBITS_ADMIN_UI=false 14 | 15 | # Change theme 16 | LNBITS_SITE_TITLE="LNbits" 17 | LNBITS_SITE_TAGLINE="free and open-source lightning wallet" 18 | LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack." 19 | # Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber 20 | LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber" 21 | # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" 22 | 23 | HOST=127.0.0.1 24 | PORT=5000 25 | 26 | ###################################### 27 | ########## Funding Source ############ 28 | ###################################### 29 | 30 | # which fundingsources are allowed in the admin ui 31 | # LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet" 32 | 33 | LNBITS_BACKEND_WALLET_CLASS=VoidWallet 34 | # VoidWallet is just a fallback that works without any actual Lightning capabilities, 35 | # just so you can see the UI before dealing with this file. 36 | 37 | # How many times to retry connectiong to the Funding Source before defaulting to the VoidWallet 38 | # FUNDING_SOURCE_MAX_RETRIES=4 39 | 40 | # Invoice expiry for LND, CLN, Eclair, LNbits funding sources 41 | LIGHTNING_INVOICE_EXPIRY=3600 42 | 43 | # Set one of these blocks depending on the wallet kind you chose above: 44 | 45 | # ClicheWallet 46 | CLICHE_ENDPOINT=ws://127.0.0.1:12000 47 | 48 | # SparkWallet 49 | SPARK_URL=http://localhost:9737/rpc 50 | SPARK_TOKEN=myaccesstoken 51 | 52 | # CoreLightningWallet 53 | CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc" 54 | 55 | # CoreLightningRestWallet 56 | CORELIGHTNING_REST_URL=http://127.0.0.1:8185/ 57 | CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon" # or BASE64/HEXSTRING 58 | CORELIGHTNING_REST_CERT="/path/to/clnrest/tls.cert" 59 | 60 | # LnbitsWallet 61 | LNBITS_ENDPOINT=https://demo.lnbits.com 62 | LNBITS_KEY=LNBITS_ADMIN_KEY 63 | 64 | # LndWallet 65 | LND_GRPC_ENDPOINT=127.0.0.1 66 | LND_GRPC_PORT=10009 67 | LND_GRPC_CERT="/home/bob/.lnd/tls.cert" 68 | LND_GRPC_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING 69 | # To use an AES-encrypted macaroon, set 70 | # LND_GRPC_MACAROON="eNcRyPtEdMaCaRoOn" 71 | 72 | # LndRestWallet 73 | LND_REST_ENDPOINT=https://127.0.0.1:8080/ 74 | LND_REST_CERT="/home/bob/.lnd/tls.cert" 75 | LND_REST_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING 76 | # To use an AES-encrypted macaroon, set 77 | # LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn" 78 | 79 | # LNPayWallet 80 | LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/ 81 | # Secret API Key under developers tab 82 | LNPAY_API_KEY=LNPAY_API_KEY 83 | # Wallet Admin in Wallet Access Keys 84 | LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY 85 | 86 | # AlbyWallet 87 | ALBY_API_ENDPOINT=https://api.getalby.com/ 88 | ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN 89 | 90 | # BoltzWallet 91 | BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002 92 | BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroon" # or HEXSTRING 93 | BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert" # or HEXSTRING 94 | BOLTZ_CLIENT_WALLET="lnbits" 95 | 96 | # ZBDWallet 97 | ZBD_API_ENDPOINT=https://api.zebedee.io/v0/ 98 | ZBD_API_KEY=ZBD_ACCESS_TOKEN 99 | 100 | # BlinkWallet 101 | BLINK_API_ENDPOINT=https://api.blink.sv/graphql 102 | BLINK_WS_ENDPOINT=wss://ws.blink.sv/graphql 103 | BLINK_TOKEN=BLINK_TOKEN 104 | 105 | # PhoenixdWallet 106 | PHOENIXD_API_ENDPOINT=http://localhost:9740/ 107 | PHOENIXD_API_PASSWORD=PHOENIXD_KEY 108 | 109 | # OpenNodeWallet 110 | OPENNODE_API_ENDPOINT=https://api.opennode.com/ 111 | OPENNODE_KEY=OPENNODE_ADMIN_KEY 112 | 113 | # FakeWallet 114 | FAKE_WALLET_SECRET="ToTheMoon1" 115 | LNBITS_DENOMINATION=sats 116 | 117 | # EclairWallet 118 | ECLAIR_URL=http://127.0.0.1:8283 119 | ECLAIR_PASS=eclairpw 120 | 121 | # NWCWalllet 122 | NWC_PAIRING_URL="nostr+walletconnect://000...000?relay=example.com&secret=123" 123 | 124 | # LnTipsWallet 125 | # Enter /api in LightningTipBot to get your key 126 | LNTIPS_API_KEY=LNTIPS_ADMIN_KEY 127 | LNTIPS_API_ENDPOINT=https://ln.tips 128 | 129 | # BreezSdkWallet 130 | BREEZ_API_KEY=KEY 131 | BREEZ_GREENLIGHT_SEED=SEED 132 | # A Greenlight invite code or Greenlight partner certificate/key can be used 133 | BREEZ_GREENLIGHT_INVITE_CODE=CODE 134 | BREEZ_GREENLIGHT_DEVICE_KEY="/path/to/breezsdk/device.pem" # or BASE64/HEXSTRING 135 | BREEZ_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING 136 | 137 | ###################################### 138 | ####### Auth Configurations ########## 139 | ###################################### 140 | # Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value. 141 | AUTH_SECRET_KEY="" 142 | AUTH_TOKEN_EXPIRE_MINUTES=525600 143 | # Possible authorization methods: user-id-only, username-password, google-auth, github-auth, keycloak-auth 144 | AUTH_ALLOWED_METHODS="user-id-only, username-password" 145 | # Set this flag if HTTP is used for OAuth 146 | # OAUTHLIB_INSECURE_TRANSPORT="1" 147 | 148 | # Google OAuth Config 149 | # Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token 150 | GOOGLE_CLIENT_ID="" 151 | GOOGLE_CLIENT_SECRET="" 152 | 153 | # GitHub OAuth Config 154 | # Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token 155 | GITHUB_CLIENT_ID="" 156 | GITHUB_CLIENT_SECRET="" 157 | 158 | # Keycloak OAuth Config 159 | # Make sure that the valid redirect URIs contain https://{domain}/api/v1/auth/keycloak/token 160 | KEYCLOAK_CLIENT_ID="" 161 | KEYCLOAK_CLIENT_SECRET="" 162 | KEYCLOAK_DISCOVERY_URL="" 163 | 164 | 165 | ###################################### 166 | 167 | # uvicorn variable, uncomment to allow https behind a proxy 168 | # IMPORTANT: this also needs the webserver to be configured to forward the headers 169 | # http://docs.lnbits.org/guide/installation.html#running-behind-an-apache2-reverse-proxy-over-https 170 | # FORWARDED_ALLOW_IPS="*" 171 | 172 | # Server security, rate limiting ips, blocked ips, allowed ips 173 | LNBITS_RATE_LIMIT_NO="200" 174 | LNBITS_RATE_LIMIT_UNIT="minute" 175 | LNBITS_ALLOWED_IPS="" 176 | LNBITS_BLOCKED_IPS="" 177 | 178 | # Allow users and admins by user IDs (comma separated list) 179 | # if set new users will not be able to create accounts 180 | LNBITS_ALLOWED_USERS="" 181 | LNBITS_ADMIN_USERS="" 182 | # ID of the super user. The user ID must exist. 183 | # SUPER_USER="" 184 | 185 | # Extensions only admin can access 186 | LNBITS_ADMIN_EXTENSIONS="ngrok, admin" 187 | # Extensions enabled by default when a user is created 188 | LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp" 189 | 190 | # Start LNbits core only. The extensions are not loaded. 191 | # LNBITS_EXTENSIONS_DEACTIVATE_ALL=true 192 | 193 | # Disable account creation for new users 194 | # LNBITS_ALLOW_NEW_ACCOUNTS=false 195 | 196 | # Enable Node Management without activating the LNBITS Admin GUI 197 | # by setting the following variables to true. 198 | LNBITS_NODE_UI=false 199 | LNBITS_PUBLIC_NODE_UI=false 200 | # Enabling the transactions tab can cause crashes on large Core Lightning nodes. 201 | LNBITS_NODE_UI_TRANSACTIONS=false 202 | 203 | LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" 204 | 205 | # Ad space description 206 | # LNBITS_AD_SPACE_TITLE="Supported by" 207 | # csv ad space, format ";;, ;;", extensions can choose to honor 208 | # LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png" 209 | # LNBITS_SHOW_HOME_PAGE_ELEMENTS=true # if set to true, the ad space will be displayed on the home page 210 | # LNBITS_CUSTOM_BADGE="USE WITH CAUTION - LNbits wallet is still in BETA" 211 | # LNBITS_CUSTOM_BADGE_COLOR="warning" 212 | 213 | # Hides wallet api, extensions can choose to honor 214 | LNBITS_HIDE_API=false 215 | 216 | # LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json" 217 | # GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN 218 | # LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx 219 | 220 | # Path where extensions will be installed (defaults to `./lnbits/`). 221 | # Inside this directory the `extensions` and `upgrades` sub-directories will be created. 222 | # LNBITS_EXTENSIONS_PATH="/path/to/some/dir" 223 | 224 | # Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart. 225 | # The extension must be removed from this list in order to not be re-installed. 226 | LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos" 227 | 228 | # Database: to use SQLite, specify LNBITS_DATA_FOLDER 229 | # to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... 230 | # to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://... 231 | # for both PostgreSQL and CockroachDB, you'll need to install 232 | # psycopg2 as an additional dependency 233 | LNBITS_DATA_FOLDER="./data" 234 | # LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename" 235 | 236 | # the service fee (in percent) 237 | LNBITS_SERVICE_FEE=0.0 238 | # the wallet where fees go to 239 | # LNBITS_SERVICE_FEE_WALLET= 240 | # the maximum fee per transaction (in satoshis) 241 | # LNBITS_SERVICE_FEE_MAX=1000 242 | # disable fees for internal transactions 243 | # LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true 244 | 245 | # value in millisats 246 | LNBITS_RESERVE_FEE_MIN=2000 247 | # value in percent 248 | LNBITS_RESERVE_FEE_PERCENT=1.0 249 | 250 | # limit the maximum balance for each wallet 251 | # throw an error if the wallet attempts to create a new invoice 252 | 253 | # LNBITS_WALLET_LIMIT_MAX_BALANCE=1000000 254 | # LNBITS_WALLET_LIMIT_DAILY_MAX_WITHDRAW=1000000 255 | # LNBITS_WALLET_LIMIT_SECS_BETWEEN_TRANS=60 256 | 257 | # Limit fiat currencies allowed to see in UI 258 | # LNBITS_ALLOWED_CURRENCIES="EUR, USD" 259 | 260 | ###################################### 261 | ###### Logging and Development ####### 262 | ###################################### 263 | 264 | DEBUG=false 265 | DEBUG_DATABASE=false 266 | BUNDLE_ASSETS=true 267 | 268 | # logging into LNBITS_DATA_FOLDER/logs/ 269 | ENABLE_LOG_TO_FILE=true 270 | 271 | # https://loguru.readthedocs.io/en/stable/api/logger.html#file 272 | LOG_ROTATION="100 MB" 273 | LOG_RETENTION="3 months" 274 | 275 | # for database cleanup commands 276 | # CLEANUP_WALLETS_DAYS=90 277 | 278 | # PhoenixdWallet 279 | LNBITS_BACKEND_WALLET_CLASS=PhoenixdWallet 280 | PHOENIXD_API_ENDPOINT=http://phoenixd:9740/ 281 | PHOENIXD_API_PASSWORD=YYY 282 | 283 | LNBITS_DATABASE_URL="postgres://postgres:XXX@postgres:5432/lnbits" 284 | 285 | LNBITS_SITE_TITLE="lb1.yourdomain.com" 286 | LNBITS_SITE_TAGLINE="free and open-source lightning wallet" 287 | LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack." 288 | 289 | -------------------------------------------------------------------------------- /ui/backend/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException, Depends, status 2 | from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm 3 | from fastapi.middleware.cors import CORSMiddleware 4 | from fastapi.responses import JSONResponse 5 | from pydantic import BaseModel 6 | from typing import List, Optional, Dict 7 | import jwt 8 | from datetime import datetime, timedelta 9 | import subprocess 10 | import os 11 | import json 12 | import re 13 | import logging 14 | import threading 15 | import uuid 16 | from pathlib import Path 17 | from dotenv import load_dotenv 18 | 19 | # Carica le variabili d'ambiente dal file .env 20 | load_dotenv() 21 | 22 | # Configurazione logging 23 | logging.basicConfig(level=logging.DEBUG) 24 | logger = logging.getLogger(__name__) 25 | 26 | app = FastAPI(title="Lightstack UI API") 27 | 28 | app.add_middleware( 29 | CORSMiddleware, 30 | allow_origins=["*"], 31 | allow_credentials=True, 32 | allow_methods=["*"], 33 | allow_headers=["*"], 34 | ) 35 | 36 | # Configurazione 37 | SECRET_KEY = os.getenv("JWT_SECRET_KEY") 38 | if not SECRET_KEY: 39 | logger.error("JWT_SECRET_KEY non trovata nel file .env") 40 | raise ValueError("JWT_SECRET_KEY mancante") 41 | 42 | ALGORITHM = "HS256" 43 | ACCESS_TOKEN_EXPIRE_MINUTES = 30 44 | # Modifica del percorso per l'installazione tradizionale 45 | SCRIPT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 46 | 47 | # Configurazione utenti 48 | ADMIN_USER = os.getenv("ADMIN_USER") 49 | ADMIN_PASS = os.getenv("ADMIN_PASS") 50 | if not ADMIN_USER or not ADMIN_PASS: 51 | logger.error("ADMIN_USER o ADMIN_PASS non trovati nel file .env") 52 | raise ValueError("Credenziali admin mancanti") 53 | 54 | USERS_DB = {ADMIN_USER: ADMIN_PASS} 55 | 56 | logger.debug(f"Utenti disponibili: {list(USERS_DB.keys())}") 57 | 58 | # Classe per lo stato dei job 59 | class JobStatus: 60 | PENDING = "pending" 61 | RUNNING = "running" 62 | COMPLETED = "completed" 63 | FAILED = "failed" 64 | 65 | # Dizionario per tenere traccia dei job 66 | job_tracker: Dict[str, Dict] = {} 67 | 68 | # Modelli 69 | class Token(BaseModel): 70 | access_token: str 71 | token_type: str 72 | 73 | class TokenData(BaseModel): 74 | username: Optional[str] = None 75 | 76 | class User(BaseModel): 77 | username: str 78 | 79 | class UserInDB(User): 80 | password: str 81 | 82 | class Stack(BaseModel): 83 | phoenixd_domain: str 84 | lnbits_domain: str 85 | use_real_certs: bool = False 86 | use_postgres: bool = False 87 | email: Optional[str] = None 88 | 89 | class StackResponse(BaseModel): 90 | id: str 91 | phoenixd_domain: str 92 | lnbits_domain: str 93 | 94 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") 95 | 96 | # Funzione per eseguire la creazione dello stack in un thread separato 97 | def run_stack_creation_thread(job_id: str, stack: Stack, input_data: str): 98 | try: 99 | job_tracker[job_id]["status"] = JobStatus.RUNNING 100 | 101 | process = subprocess.run( 102 | [os.path.join(SCRIPT_DIR, "init.sh"), "add"], 103 | input=input_data, 104 | capture_output=True, 105 | text=True, 106 | cwd=SCRIPT_DIR 107 | ) 108 | 109 | logger.debug(f"Process stdout:\n{process.stdout}") 110 | logger.debug(f"Process stderr:\n{process.stderr}") 111 | 112 | if process.returncode != 0: 113 | logger.error(f"Stack creation failed: {process.stdout}\n{process.stderr}") 114 | job_tracker[job_id]["status"] = JobStatus.FAILED 115 | job_tracker[job_id]["error"] = process.stderr or "Failed to create stack" 116 | return 117 | 118 | # Estrai l'ID dello stack dal risultato 119 | stack_id = None 120 | for line in process.stdout.splitlines(): 121 | if "stack_" in line: 122 | match = re.search(r"stack_(\d+)", line) 123 | if match: 124 | stack_id = match.group(1) 125 | break 126 | 127 | if not stack_id: 128 | job_tracker[job_id]["status"] = JobStatus.FAILED 129 | job_tracker[job_id]["error"] = "Failed to extract stack ID" 130 | return 131 | 132 | # Aggiorna lo stato del job 133 | job_tracker[job_id]["status"] = JobStatus.COMPLETED 134 | job_tracker[job_id]["stack_id"] = stack_id 135 | 136 | logger.info(f"Stack creation completed for job {job_id}, stack_id: {stack_id}") 137 | except Exception as e: 138 | logger.error(f"Error in run_stack_creation_thread: {str(e)}") 139 | job_tracker[job_id]["status"] = JobStatus.FAILED 140 | job_tracker[job_id]["error"] = str(e) 141 | 142 | # Funzioni di gestione nginx 143 | def manage_nginx(action: str): 144 | """Gestisce l'avvio/stop di nginx usando systemd""" 145 | try: 146 | if action == "stop": 147 | logger.info("Stopping nginx service...") 148 | subprocess.run(["systemctl", "stop", "nginx"], check=True) 149 | return True 150 | elif action == "start": 151 | logger.info("Starting nginx service...") 152 | subprocess.run(["systemctl", "start", "nginx"], check=True) 153 | return True 154 | except subprocess.CalledProcessError as e: 155 | logger.error(f"Error managing nginx service: {str(e)}") 156 | return False 157 | 158 | # Funzioni di autenticazione 159 | def verify_password(plain_password: str, username: str) -> bool: 160 | stored_password = USERS_DB.get(username) 161 | if not stored_password: 162 | return False 163 | return plain_password == stored_password 164 | 165 | def get_user(username: str) -> Optional[UserInDB]: 166 | if username in USERS_DB: 167 | return UserInDB(username=username, password=USERS_DB[username]) 168 | return None 169 | 170 | def authenticate_user(username: str, password: str) -> Optional[UserInDB]: 171 | logger.debug(f"Tentativo autenticazione per utente: {username}") 172 | user = get_user(username) 173 | if not user: 174 | logger.debug("Utente non trovato") 175 | return None 176 | if not verify_password(password, username): 177 | logger.debug("Password non corretta") 178 | return None 179 | logger.debug("Autenticazione riuscita") 180 | return user 181 | 182 | def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): 183 | to_encode = data.copy() 184 | if expires_delta: 185 | expire = datetime.utcnow() + expires_delta 186 | else: 187 | expire = datetime.utcnow() + timedelta(minutes=15) 188 | to_encode.update({"exp": expire}) 189 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) 190 | return encoded_jwt 191 | 192 | async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: 193 | credentials_exception = HTTPException( 194 | status_code=status.HTTP_401_UNAUTHORIZED, 195 | detail="Could not validate credentials", 196 | headers={"WWW-Authenticate": "Bearer"}, 197 | ) 198 | try: 199 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 200 | username: str = payload.get("sub") 201 | if username is None: 202 | raise credentials_exception 203 | token_data = TokenData(username=username) 204 | except jwt.PyJWTError: 205 | raise credentials_exception 206 | user = get_user(token_data.username) 207 | if user is None: 208 | raise credentials_exception 209 | return user 210 | 211 | # Endpoints 212 | @app.post("/token", response_model=Token) 213 | async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): 214 | logger.debug(f"Tentativo di login con username: {form_data.username}") 215 | user = authenticate_user(form_data.username, form_data.password) 216 | if not user: 217 | logger.debug("Autenticazione fallita") 218 | raise HTTPException( 219 | status_code=status.HTTP_401_UNAUTHORIZED, 220 | detail="Incorrect username or password", 221 | headers={"WWW-Authenticate": "Bearer"}, 222 | ) 223 | logger.debug("Autenticazione riuscita, genero token") 224 | access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 225 | access_token = create_access_token( 226 | data={"sub": user.username}, expires_delta=access_token_expires 227 | ) 228 | return {"access_token": access_token, "token_type": "bearer"} 229 | 230 | @app.get("/stacks", response_model=List[StackResponse]) 231 | async def get_stacks(current_user: User = Depends(get_current_user)): 232 | try: 233 | logger.debug(f"Using init script at: {os.path.join(SCRIPT_DIR, 'init.sh')}") 234 | result = subprocess.run( 235 | [os.path.join(SCRIPT_DIR, 'init.sh'), "list"], 236 | capture_output=True, 237 | text=True, 238 | check=True, 239 | cwd=SCRIPT_DIR 240 | ) 241 | 242 | stacks = [] 243 | for line in result.stdout.strip().split('\n'): 244 | try: 245 | if line.startswith("{"): # Solo le linee che iniziano con { 246 | stack_data = json.loads(line) 247 | stacks.append(stack_data) 248 | except json.JSONDecodeError: 249 | # Ignora silenziosamente le righe che non sono JSON valido 250 | continue 251 | 252 | return stacks 253 | except subprocess.CalledProcessError as e: 254 | logger.error(f"Error listing stacks: {e.stderr}") 255 | raise HTTPException(status_code=500, detail=e.stderr) 256 | 257 | @app.post("/stacks") 258 | async def add_stack(stack: Stack, current_user: User = Depends(get_current_user)): 259 | try: 260 | logger.debug(f"Adding new stack with config: {stack.dict()}") 261 | if stack.use_real_certs: 262 | logger.info("Real certificates requested, reconfiguring nginx") 263 | try: 264 | subprocess.run(["nginx", "-s", "reload"], check=True) 265 | except subprocess.CalledProcessError as e: 266 | logger.error(f"Failed to reconfigure nginx: {e}") 267 | raise HTTPException(status_code=500, detail="Failed to reconfigure nginx") 268 | 269 | # Crea l'input per lo script 270 | input_data = f"""{stack.phoenixd_domain} 271 | {stack.lnbits_domain} 272 | {"y" if stack.use_real_certs else "n"} 273 | {"y" if stack.use_postgres else "n"} 274 | {stack.email if stack.use_real_certs else ""} 275 | y 276 | y 277 | """ 278 | logger.debug(f"Input data exactly as sent to script:\n[START]\n{input_data}[END]") 279 | 280 | # Genera un ID per il job 281 | job_id = str(uuid.uuid4()) 282 | 283 | # Inizializza il job nel tracker 284 | job_tracker[job_id] = { 285 | "status": JobStatus.PENDING, 286 | "stack": stack.dict(), 287 | "stack_id": None, 288 | "error": None, 289 | "created_at": datetime.utcnow().isoformat() 290 | } 291 | 292 | # Avvia il processo in un thread separato 293 | thread = threading.Thread( 294 | target=run_stack_creation_thread, 295 | args=(job_id, stack, input_data) 296 | ) 297 | thread.daemon = True 298 | thread.start() 299 | 300 | # Rispondi subito con l'ID del job 301 | return { 302 | "job_id": job_id, 303 | "status": JobStatus.PENDING, 304 | "message": "Stack creation started. Check job status to monitor progress." 305 | } 306 | except Exception as e: 307 | logger.error(f"Error creating stack: {str(e)}") 308 | if stack.use_real_certs: 309 | try: 310 | subprocess.run(["nginx", "-s", "reload"], check=True) 311 | except: 312 | pass 313 | raise HTTPException(status_code=500, detail=str(e)) 314 | 315 | @app.get("/jobs/{job_id}") 316 | async def get_job_status(job_id: str, current_user: User = Depends(get_current_user)): 317 | if job_id not in job_tracker: 318 | raise HTTPException(status_code=404, detail="Job not found") 319 | 320 | job = job_tracker[job_id] 321 | 322 | response = { 323 | "job_id": job_id, 324 | "status": job["status"] 325 | } 326 | 327 | if job["status"] == JobStatus.COMPLETED: 328 | response["stack_id"] = job["stack_id"] 329 | elif job["status"] == JobStatus.FAILED: 330 | response["error"] = job["error"] 331 | 332 | return response 333 | 334 | @app.delete("/stacks/{stack_id}") 335 | async def remove_stack(stack_id: str, current_user: User = Depends(get_current_user)): 336 | try: 337 | logger.debug(f"Removing stack {stack_id}") 338 | script_path = os.path.join(SCRIPT_DIR, "init.sh") 339 | 340 | # Controlla quanti stack sono attivi 341 | list_process = subprocess.run( 342 | [script_path, "list"], 343 | capture_output=True, 344 | text=True, 345 | cwd=SCRIPT_DIR 346 | ) 347 | 348 | # Conta gli stack attivi (le righe che iniziano con un numero) 349 | active_stacks = len([line for line in list_process.stdout.splitlines() 350 | if line.strip() and line[0].isdigit()]) 351 | logger.debug(f"Found {active_stacks} active stacks") 352 | 353 | # Input diverso basato sul numero di stack 354 | input_data = "y\n" if active_stacks == 1 else f"{stack_id}\ny\n" 355 | logger.debug(f"Using input data: {input_data}") 356 | 357 | process = subprocess.run( 358 | [script_path, "del"], 359 | input=input_data, 360 | capture_output=True, 361 | text=True, 362 | cwd=SCRIPT_DIR, 363 | env={ 364 | "PATH": os.environ["PATH"], 365 | "SCRIPT_DIR": SCRIPT_DIR 366 | } 367 | ) 368 | 369 | logger.debug(f"Command stdout: {process.stdout}") 370 | logger.debug(f"Command stderr: {process.stderr}") 371 | 372 | if process.returncode != 0: 373 | raise HTTPException( 374 | status_code=500, 375 | detail=process.stderr or "Failed to remove stack" 376 | ) 377 | 378 | stack_path = os.path.join(SCRIPT_DIR, f"stack_{stack_id}") 379 | if os.path.exists(stack_path): 380 | raise HTTPException( 381 | status_code=500, 382 | detail="Stack removal incomplete" 383 | ) 384 | 385 | return {"message": f"Stack {stack_id} removed successfully"} 386 | 387 | except Exception as e: 388 | logger.error(f"Error removing stack: {str(e)}") 389 | logger.exception("Stack trace:") 390 | raise HTTPException(status_code=500, detail=str(e)) 391 | 392 | @app.get("/health") 393 | async def health_check(): 394 | return {"status": "healthy"} 395 | 396 | if __name__ == "__main__": 397 | import uvicorn 398 | uvicorn.run(app, host="0.0.0.0", port=8005) 399 | 400 | -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Interactive initialization script for phoenixd/lnbits stack 4 | 5 | set -eEo pipefail 6 | source initlib.sh 7 | 8 | cd $( dirname $0 ) 9 | 10 | # Check if the script is being run as root 11 | if [ ! "$(id -u)" -eq 0 ]; then 12 | echo "This script needs root priviledges. Run it using sudo." 13 | exit 1 14 | fi 15 | 16 | # Check if Certbot is installed 17 | if ! command -v certbot &> /dev/null; then 18 | echo "Certbot is not installed. Please install Certbot and try again." >&2 19 | exit 1 20 | fi 21 | 22 | # Check if ufw is installed 23 | if ! command -v ufw &> /dev/null; then 24 | echo "ufw Firewall is not installed on this system. Please install and run again." >&2 25 | exit 1 26 | fi 27 | 28 | # Check if ufw is active 29 | if ! ufw status | grep -q "Status: active"; then 30 | echo "ufw Firewall is not active. Please enable ufw first." >&2 31 | exit 1 32 | fi 33 | 34 | # Check if port 80 is allowed in ufw 35 | if ! ufw status | grep -q "80"; then 36 | echo "Port $PORT is not allowed through ufw." >&2 37 | echo "Port 80 status open is necessary to run certbot. Please open and run again" >&2 38 | exit 1 39 | fi 40 | echo 41 | 42 | SID=$(generate_stack_id) 43 | STACK="stack_$SID" 44 | 45 | # Migration path 46 | if [[ -d data && -d letsencrypt && -d lnbitsdata && -f .env && -f default.conf && -f docker-compose.yml ]]; then 47 | echo ">>>A previous installation was detected<<<" 48 | echo 49 | echo "In order to use the new multistack enhancements, you need first to migrate your running environment." 50 | echo "All configurations and data will be preserved." 51 | echo 52 | read -p "Do you want to continue? (y/N): " migrateyesno 53 | echo 54 | 55 | if [[ ! $migrateyesno =~ ^[Yy]$ ]]; then 56 | exit 0 57 | fi 58 | 59 | # Stop all containers 60 | echo "Stopping all containers..." 61 | docker compose down 62 | echo "All containers have been stopped." 63 | 64 | # Backup configurations and data 65 | echo "Backing up configuration and data..." 66 | mkdir -p .backup 67 | mv -t .backup data lnbitsdata .env default.conf docker-compose.yml 68 | if [[ -d pgdata ]]; then 69 | mv pgdata .backup/ 70 | fi 71 | if [[ -d pgtmp ]]; then 72 | mv pgtmp .backup/ 73 | fi 74 | echo "Backup completed" 75 | 76 | # Restore on error 77 | trap "exit 128" SIGINT 78 | trap "exit 129" SIGQUIT 79 | trap "exit 143" SIGTERM 80 | trap "migration_trap" EXIT 81 | 82 | mkdir -p nginx $STACK 83 | 84 | # Setup stack 85 | echo "Migrating configurations and data..." 86 | cp -R .backup/{data,lnbitsdata,.env} $STACK 87 | if [[ -d .backup/pgdata ]]; then 88 | cp -R .backup/pgdata $STACK 89 | fi 90 | if [[ -d .backup/pgtmp ]]; then 91 | cp -R .backup/pgtmp $STACK 92 | fi 93 | 94 | cp docker-compose.yml.example docker-compose.yml 95 | sed -i "/^services:/i \ \ - $STACK/docker-compose.yml" docker-compose.yml 96 | 97 | if grep -q postgres .backup/docker-compose.yml; then 98 | POSTGRES_PASSWORD=$( grep POSTGRES_PASSWORD .backup/docker-compose.yml | sed 's/^.*: *//' ) 99 | 100 | cp docker-compose.yml.stack.example $STACK/docker-compose.yml 101 | sed -i "s/^\( *POSTGRES_PASSWORD: \).*$/\1$POSTGRES_PASSWORD/" $STACK/docker-compose.yml 102 | sed -i "s/^\( *postgres\):/\1-$SID:/" $STACK/docker-compose.yml 103 | 104 | update_env "LNBITS_DATABASE_URL" "postgres://postgres:$POSTGRES_PASSWORD@postgres-$SID:5432/lnbits" 105 | else 106 | cp docker-compose.yml.stack.sqlite.example $STACK/docker-compose.yml 107 | fi 108 | sed -i "s/^\( *phoenixd\):/\1-$SID:/" $STACK/docker-compose.yml 109 | sed -i "s/^\( *lnbits\):/\1-$SID:/" $STACK/docker-compose.yml 110 | sed -i "s/^\( *postgres\):/\1-$SID:/" $STACK/docker-compose.yml 111 | sed -i "s/^\( *container_name: .*\)$/\1-$SID/" $STACK/docker-compose.yml 112 | 113 | update_env "PHOENIXD_API_ENDPOINT" "http://phoenixd-$SID:9740/" 114 | update_env "LNBITS_SITE_TAGLINE" "\"free and open-source lightning wallet\"" 115 | update_env "LNBITS_SITE_DESCRIPTION" "\"The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack.\"" 116 | 117 | # Nginx 118 | cp .backup/default.conf nginx/$STACK.conf 119 | sed -i "s|\(http://phoenixd\)|\1-$SID|" nginx/$STACK.conf 120 | sed -i "s|\(http://lnbits\)|\1-$SID|" nginx/$STACK.conf 121 | 122 | # Verify the contents of the updated files 123 | echo 124 | echo "Relevant contents of the nginx/$STACK.conf file after update:" 125 | grep -E "(server_name|ssl_certificate|proxy_pass)" nginx/$STACK.conf | sed 's/^ *//' 126 | echo 127 | echo "Relevant contents of the $STACK/.env file after update:" 128 | grep -E "^(LNBITS_BACKEND_WALLET_CLASS|PHOENIXD_API_ENDPOINT|PHOENIXD_API_PASSWORD|LNBITS_DATABASE_URL|LNBITS_SITE_TITLE|LNBITS_SITE_TAGLINE|LNBITS_SITE_DESCRIPTION)=" $STACK/.env 129 | echo 130 | echo "Migration completed." 131 | 132 | # Restart all containers 133 | echo "Restarting all containers..." 134 | docker compose up -d 135 | echo "All containers have been started." 136 | 137 | # Verify active stacks 138 | echo 139 | echo "You have the following active stacks:" 140 | print_stacks | sort | column -t -N "ID,PHOENIXD,LNBITS" 141 | 142 | PHOENIXD_DOMAIN=$( print_stacks | awk {'print $2'} ) 143 | LNBITS_DOMAIN=$( print_stacks | awk {'print $3'} ) 144 | 145 | echo 146 | echo "Initialization complete. All containers have been successfully started with the new configurations." 147 | echo "Your system is now ready for use." 148 | echo 149 | echo "- You can access LNbits at https://$LNBITS_DOMAIN" 150 | echo "- The Phoenixd API is accessible at https://$PHOENIXD_DOMAIN" 151 | echo "- To manage the containers, use the docker compose commands in the project directory." 152 | echo 153 | echo "In order to view container logs, just use 'docker compose logs [container_name]' or " 154 | echo "docker compose logs -t -f --tail 300" 155 | 156 | exit 0 157 | fi 158 | 159 | if [[ $( print_stacks | wc -l ) -gt 0 ]]; then 160 | echo "You have the following active stacks:" 161 | print_stacks | sort | column -t -N "ID,PHOENIXD,LNBITS" 162 | echo 163 | fi 164 | 165 | 166 | case $1 in 167 | ""|"add") 168 | # Request configuration data from the user 169 | echo ">>>Please provide needed configuration infos<<<" 170 | echo 171 | read -p "Enter the domain for Phoenixd API (e.g., api.yourdomain.com): " PHOENIXD_DOMAIN 172 | read -p "Enter the domain for LNbits (e.g., lnbits.yourdomain.com): " LNBITS_DOMAIN 173 | read -p "Do you want real Letsencrypt certificates to be issued? (y/N): " letscertificates 174 | read -p "Do you want LNBits to use PostgreSQL? (y/N): " postgresyesno 175 | 176 | echo "DEBUG: PostgreSQL initial value: '$postgresyesno'" >&2 177 | if [[ $postgresyesno =~ ^[Yy]$ ]]; then 178 | postgresyesno="y" 179 | echo "DEBUG: Using PostgreSQL configuration" >&2 180 | echo "DEBUG: Creating directories..." >&2 181 | mkdir -p $STACK nginx 182 | echo "DEBUG: Copying configuration files..." >&2 183 | cp docker-compose.yml.stack.example $STACK/docker-compose.yml 184 | cp .env.example $STACK/.env 185 | else 186 | postgresyesno="n" 187 | echo "DEBUG: Using SQLite configuration" >&2 188 | echo "DEBUG: Creating directories..." >&2 189 | mkdir -p $STACK nginx 190 | echo "DEBUG: Copying configuration files..." >&2 191 | cp docker-compose.yml.stack.sqlite.example $STACK/docker-compose.yml 192 | cp .env.sqlite.example $STACK/.env 193 | fi 194 | 195 | echo "DEBUG: PostgreSQL final value: '$postgresyesno'" >&2 196 | echo 197 | echo "A new $STACK is going to be added with the following specs:" 198 | echo "$SID $PHOENIXD_DOMAIN $LNBITS_DOMAIN $letscertificates $postgresyesno" | column -t -N "ID,PHOENIXD,LNBITS,LETSENCRYPT,POSTGRES" 199 | 200 | # Check if script is running interactively 201 | if [ -t 0 ]; then 202 | # Interactive mode 203 | read -p "Do you want to continue? (y/N): " addyesno 204 | echo 205 | if [[ ! $addyesno =~ ^[Yy]$ ]]; then 206 | exit 0 207 | fi 208 | else 209 | # Non-interactive mode (API) 210 | echo "DEBUG: Running in non-interactive mode, continuing..." >&2 211 | fi 212 | 213 | trap "exit 128" SIGINT 214 | trap "exit 129" SIGQUIT 215 | trap "exit 143" SIGTERM 216 | trap init_trap EXIT 217 | 218 | # Copy example files 219 | mkdir -p nginx $STACK 220 | if [[ ! -f docker-compose.yml ]]; then 221 | cp docker-compose.yml.example docker-compose.yml 222 | fi 223 | 224 | cp default.conf.example nginx/$STACK.conf 225 | if [[ $postgresyesno =~ ^[Yy]$ ]]; then 226 | cp docker-compose.yml.stack.example $STACK/docker-compose.yml 227 | cp .env.example $STACK/.env 228 | else 229 | cp docker-compose.yml.stack.sqlite.example $STACK/docker-compose.yml 230 | cp .env.sqlite.example $STACK/.env 231 | fi 232 | 233 | echo "docker-compose.yml, nginx/$STACK.conf and $STACK/.env files set up." 234 | echo 235 | 236 | # Generate certificates 237 | if [[ ! $letscertificates =~ ^[Yy]$ ]]; then 238 | echo "Issuing selfsigned certificates on local host..." 239 | echo "DEBUG: Starting self-signed certificate generation..." >&2 240 | generate_certificates $PHOENIXD_DOMAIN $LNBITS_DOMAIN 241 | echo "DEBUG: Self-signed certificate generation completed" >&2 242 | else 243 | echo "Issuing Letsencrypt certificates on local host..." 244 | echo "DEBUG: Starting Letsencrypt certificate generation..." >&2 245 | generate_certificates_certbot "$PHOENIXD_DOMAIN" "$LNBITS_DOMAIN" "$cert_email" 246 | echo "DEBUG: Letsencrypt certificate generation completed" >&2 247 | fi 248 | 249 | echo "DEBUG: Setting up stack configuration..." >&2 250 | 251 | # Generate password for Postgres 252 | POSTGRES_PASSWORD=$(generate_password) 253 | 254 | # Update the .env file 255 | echo "Updating the $STACK/.env file..." 256 | 257 | # Remove or comment out unnecessary variables 258 | sed -i '/^LNBITS_BACKEND_WALLET_CLASS=/d' $STACK/.env 259 | sed -i '/^PHOENIXD_API_ENDPOINT=/d' $STACK/.env 260 | sed -i '/^PHOENIXD_API_PASSWORD=/d' $STACK/.env 261 | sed -i '/^LNBITS_DATABASE_URL=/d' $STACK/.env 262 | sed -i '/^LNBITS_SITE_TITLE=/d' $STACK/.env 263 | sed -i '/^LNBITS_SITE_TAGLINE=/d' $STACK/.env 264 | sed -i '/^LNBITS_SITE_DESCRIPTION=/d' $STACK/.env 265 | 266 | # Add or update necessary variables 267 | update_env "LNBITS_BACKEND_WALLET_CLASS" "PhoenixdWallet" 268 | update_env "PHOENIXD_API_ENDPOINT" "http://phoenixd-$SID:9740/" 269 | 270 | # If no postgresql, there is no LNBITS_DATABASE_URL to configure in .env file 271 | if [[ $postgresyesno =~ ^[Yy]$ ]]; then 272 | update_env "LNBITS_DATABASE_URL" "postgres://postgres:$POSTGRES_PASSWORD@postgres-$SID:5432/lnbits" 273 | fi 274 | 275 | update_env "LNBITS_SITE_TITLE" "$LNBITS_DOMAIN" 276 | update_env "LNBITS_SITE_TAGLINE" "\"free and open-source lightning wallet\"" 277 | update_env "LNBITS_SITE_DESCRIPTION" "\"The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack.\"" 278 | 279 | # Add a comment for PHOENIXD_API_PASSWORD 280 | echo "# PHOENIXD_API_PASSWORD will be set after the first run" >> $STACK/.env 281 | 282 | echo "$STACK/.env file successfully updated." 283 | 284 | # Update the docker-compose.yml stack file 285 | echo "Updating the $STACK/docker-compose.yml file..." 286 | sed -i "s/POSTGRES_PASSWORD: XXXX/POSTGRES_PASSWORD: $POSTGRES_PASSWORD/" $STACK/docker-compose.yml 287 | sed -i "s/^\( *phoenixd\):/\1-$SID:/" $STACK/docker-compose.yml 288 | sed -i "s/^\( *lnbits\):/\1-$SID:/" $STACK/docker-compose.yml 289 | sed -i "s/^\( *postgres\):/\1-$SID:/" $STACK/docker-compose.yml 290 | sed -i "s/^\( *container_name: .*\)$/\1-$SID/" $STACK/docker-compose.yml 291 | 292 | echo "$STACK/docker-compose.yml file successfully updated." 293 | 294 | # Update the default.conf file 295 | echo "Updating the nginx/$STACK.conf file..." 296 | sed -i "s/server_name n1\.yourdomain\.com;/server_name $PHOENIXD_DOMAIN;/" nginx/$STACK.conf 297 | sed -i "s/server_name lb1\.yourdomain\.com;/server_name $LNBITS_DOMAIN;/" nginx/$STACK.conf 298 | sed -i "s|ssl_certificate /etc/letsencrypt/live/n1\.yourdomain\.com/|ssl_certificate /etc/letsencrypt/live/$PHOENIXD_DOMAIN/|" nginx/$STACK.conf 299 | sed -i "s|ssl_certificate_key /etc/letsencrypt/live/n1\.yourdomain\.com/|ssl_certificate_key /etc/letsencrypt/live/$PHOENIXD_DOMAIN/|" nginx/$STACK.conf 300 | sed -i "s|ssl_certificate /etc/letsencrypt/live/lb1\.yourdomain\.com/|ssl_certificate /etc/letsencrypt/live/$LNBITS_DOMAIN/|" nginx/$STACK.conf 301 | sed -i "s|ssl_certificate_key /etc/letsencrypt/live/lb1\.yourdomain\.com/|ssl_certificate_key /etc/letsencrypt/live/$LNBITS_DOMAIN/|" nginx/$STACK.conf 302 | sed -i "s|\(http://phoenixd\)|\1-$SID|" nginx/$STACK.conf 303 | sed -i "s|\(http://lnbits\)|\1-$SID|" nginx/$STACK.conf 304 | echo "nginx/$STACK.conf file successfully updated." 305 | 306 | # Link to docker-compose file 307 | echo "Linking $STACK to docker-compose.yml file" 308 | sed -i "/^services:/i \ \ - $STACK/docker-compose.yml" docker-compose.yml 309 | 310 | echo "Configuration completed. " 311 | echo "Certificates have been generated for $PHOENIXD_DOMAIN and $LNBITS_DOMAIN" 312 | 313 | # Start the Postgres container 314 | if [[ $postgresyesno =~ ^[Yy]$ ]]; then 315 | echo "Starting the Postgres container..." 316 | docker compose up -d postgres-$SID 317 | 318 | # Wait for Postgres to be ready 319 | echo "Waiting for Postgres to be ready..." 320 | until docker compose exec postgres-$SID pg_isready 321 | do 322 | echo "Postgres is not ready yet. Waiting..." 323 | sleep 2 324 | done 325 | echo "Postgres is ready." 326 | fi 327 | 328 | # Start the Phoenixd container 329 | echo "Starting the Phoenixd container..." 330 | docker compose up -d phoenixd-$SID 331 | wait_for_container lightstack-phoenixd-$SID 332 | 333 | echo "Waiting phoenixd to write stuffs..." 334 | sleep 20 335 | 336 | # Start the LNbits container 337 | echo "Starting the LNbits container..." 338 | docker compose up -d lnbits-$SID 339 | wait_for_container lightstack-lnbits-$SID 340 | 341 | # Start the Nginx container 342 | echo "Starting the Nginx container..." 343 | if docker ps | grep lightstack-nginx; then 344 | docker compose restart nginx 345 | else 346 | docker compose up -d nginx 347 | fi 348 | 349 | wait_for_container lightstack-nginx 350 | 351 | echo "All containers have been started." 352 | 353 | # Wait a bit to allow containers to fully initialize 354 | echo "Waiting 30 seconds to allow for complete initialization..." 355 | sleep 30 356 | 357 | # Stop stack containers 358 | echo "Stopping $STACK containers..." 359 | docker rm -f $( docker ps | grep -E "lightstack-(lnbits|phoenixd|postgres)-$SID" | awk '{print $1}' ) 360 | docker compose down nginx 361 | 362 | echo "$STACK containers have been stopped." 363 | 364 | # Configure phoenix.conf and update .env 365 | echo "Configuring $STACK/phoenix.conf and updating $STACK/.env..." 366 | 367 | # Use the relative path to the current directory 368 | PHOENIX_CONF="$STACK/data/phoenix.conf" 369 | 370 | if [ ! -f "$PHOENIX_CONF" ]; then 371 | echo "ERROR: $PHOENIX_CONF file not found" >&2 372 | echo "Setup aborted!" 373 | exit 1 374 | fi 375 | 376 | # Allow phoenixd to listen from 0.0.0.0 377 | if ! grep -q "^http-bind-ip=0.0.0.0" "$PHOENIX_CONF"; then 378 | sed -i '1ihttp-bind-ip=0.0.0.0' "$PHOENIX_CONF" 379 | echo "http-bind-ip=0.0.0.0 added to $PHOENIX_CONF" 380 | else 381 | echo "http-bind-ip=0.0.0.0 already present in $PHOENIX_CONF" 382 | fi 383 | 384 | # Extract Phoenixd password 385 | PHOENIXD_PASSWORD=$(grep -oP '(?<=http-password=).*' "$PHOENIX_CONF") 386 | if [ -n "$PHOENIXD_PASSWORD" ]; then 387 | echo "Phoenixd password found: $PHOENIXD_PASSWORD" 388 | update_env "PHOENIXD_API_PASSWORD" "$PHOENIXD_PASSWORD" 389 | echo "PHOENIXD_API_PASSWORD updated in $STACK/.env file" 390 | else 391 | echo "ERROR: Phoenixd password not found in $PHOENIX_CONF" >&2 392 | echo "Setup aborted!" 393 | exit 1 394 | fi 395 | 396 | # Verify the contents of the files 397 | echo 398 | echo "Relevant contents of the nginx/$STACK.conf file after update:" 399 | grep -E "(server_name|ssl_certificate|proxy_pass)" nginx/$STACK.conf | sed 's/^ *//' 400 | echo 401 | echo "Relevant contents of the $STACK/.env file after update:" 402 | grep -E "^(LNBITS_BACKEND_WALLET_CLASS|PHOENIXD_API_ENDPOINT|PHOENIXD_API_PASSWORD|LNBITS_DATABASE_URL|LNBITS_SITE_TITLE|LNBITS_SITE_TAGLINE|LNBITS_SITE_DESCRIPTION)=" $STACK/.env 403 | echo 404 | 405 | echo "Configuration of phoenix.conf and .env update completed." 406 | 407 | echo "Setup completed." 408 | echo "Postgres password: $POSTGRES_PASSWORD" 409 | if [[ $postgresyesno =~ ^[Yy]$ ]]; then 410 | echo "Phoenixd password: $PHOENIXD_PASSWORD" 411 | fi 412 | echo 413 | 414 | # Restart all containers 415 | echo "Restarting all containers with the new configurations..." 416 | docker compose up -d 417 | 418 | # Verify active stacks 419 | echo 420 | echo "You have the following active stacks:" 421 | print_stacks | sort | column -t -N "ID,PHOENIXD,LNBITS" 422 | 423 | echo 424 | echo "Initialization complete. All containers have been successfully started with the new configurations." 425 | echo "Your system is now ready for use." 426 | echo 427 | echo "- You can access LNbits at https://$LNBITS_DOMAIN" 428 | echo "- The Phoenixd API is accessible at https://$PHOENIXD_DOMAIN" 429 | echo "- To manage the containers, use the docker compose commands in the project directory." 430 | echo 431 | echo "In order to view container logs, just use 'docker compose logs [container_name]' or " 432 | echo "docker compose logs -t -f --tail 300" 433 | ;; 434 | 435 | "clear") 436 | if [[ $( print_stacks | wc -l ) -gt 0 ]]; then 437 | echo ">>>This will remove all of your stacks. Data will not be recoverable<<<" 438 | echo 439 | read -p "Are you sure you want to continue? (y/N): " clearyesno 440 | echo 441 | 442 | if [[ $clearyesno =~ ^[Yy]$ ]]; then 443 | docker compose down 444 | rm -Rf letsencrypt nginx stack_* docker-compose.yml 445 | echo "Setup cleared" 446 | fi 447 | fi 448 | ;; 449 | 450 | "del"|"rem") 451 | if [[ $( print_stacks | wc -l ) -eq 0 ]]; then 452 | echo "No active stacks found." 453 | echo "Please, run 'sudo ./init.sh' in order to initialize the system." 454 | print_help 455 | exit 0 456 | fi 457 | 458 | if [[ $( print_stacks | wc -l ) -eq 1 ]]; then 459 | SID=$( print_stacks | awk '{print $1}' ) 460 | else 461 | read -p "Which stack ID do you want to delete? " SID 462 | 463 | if ! print_stacks | awk '{print $1}' | grep $SID; then 464 | echo "ERROR: stack $SID not found" >&2 465 | exit 1 466 | fi 467 | fi 468 | STACK="stack_$SID" 469 | 470 | echo 471 | echo ">>>This will remove stack $SID. Data will not be recoverable<<<" 472 | echo 473 | read -p "Are you sure you want to continue? (y/N): " remyesno 474 | 475 | if [[ ! $remyesno =~ ^[Yy]$ ]]; then 476 | exit 0 477 | fi 478 | 479 | # Stop stack containers 480 | echo "Stopping $STACK containers..." 481 | if [[ $( print_stacks | wc -l ) -eq 1 ]]; then 482 | docker compose down 483 | else 484 | if docker ps | grep -E "lightstack-(lnbits|phoenixd|postgres)-$SID"; then 485 | docker rm -f $( docker ps | grep -E "lightstack-(lnbits|phoenixd|postgres)-$SID" | awk '{print $1}' ) 486 | fi 487 | fi 488 | echo "$STACK containers have been stopped." 489 | 490 | # Remove data 491 | echo "Removing $STACK data..." 492 | if [[ $( print_stacks | wc -l ) -eq 1 ]]; then 493 | rm -rf letsencrypt nginx $STACK docker-compose.yml 494 | else 495 | rm -rf nginx/$STACK.conf rm -rf $STACK 496 | sed -i "/$STACK/d" docker-compose.yml 497 | fi 498 | echo "$STACK data removed." 499 | 500 | # Restart nginx 501 | if [[ $( print_stacks | wc -l ) -gt 1 ]]; then 502 | echo "Restarting nginx..." 503 | docker compose restart nginx 504 | echo "Nginx restarted." 505 | fi 506 | echo "$STACK successfully removed." 507 | 508 | if [[ $( print_stacks | wc -l ) -gt 0 ]]; then 509 | echo "You have the following active stacks:" 510 | print_stacks | sort | column -t -N "ID,PHOENIXD,LNBITS" 511 | fi 512 | ;; 513 | 514 | "help") 515 | print_help 516 | ;; 517 | "list") 518 | if [[ $( print_stacks 2>/dev/null | grep "^[0-9]" | wc -l ) -eq 0 ]]; then 519 | echo "[]" # Return empty JSON array if no stacks 520 | else 521 | print_stacks 2>/dev/null | grep "^[0-9]" | while read -r line; do 522 | sid=$(echo "$line" | awk '{print $1}') 523 | phoenixd=$(echo "$line" | awk '{print $2}') 524 | lnbits=$(echo "$line" | awk '{print $3}') 525 | echo "{\"id\":\"$sid\",\"phoenixd_domain\":\"$phoenixd\",\"lnbits_domain\":\"$lnbits\"}" 526 | done 527 | fi 528 | ;; 529 | *) 530 | echo "Unsupported command '$1'" >&2 531 | print_help 532 | exit 1 533 | ;; 534 | esac 535 | -------------------------------------------------------------------------------- /ui/frontend/src/LightstackDashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 3 | import { Button } from '@/components/ui/button'; 4 | import { Input } from '@/components/ui/input'; 5 | import { Alert, AlertDescription } from '@/components/ui/alert'; 6 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 7 | import { Switch } from '@/components/ui/switch'; 8 | import { Label } from '@/components/ui/label'; 9 | import { 10 | CheckCircle, 11 | XCircle, 12 | Trash2, 13 | Plus, 14 | RefreshCw, 15 | LogOut, 16 | AlertCircle, 17 | ExternalLink 18 | } from 'lucide-react'; 19 | import { useAuth } from './authContext'; 20 | 21 | const LightstackDashboard = () => { 22 | const { token, logout, apiUrl } = useAuth(); 23 | const [activeStacks, setActiveStacks] = useState([]); 24 | const [isLoading, setIsLoading] = useState(false); 25 | const [error, setError] = useState(null); 26 | const [success, setSuccess] = useState(null); 27 | const [activeJobs, setActiveJobs] = useState([]); 28 | const [activeTab, setActiveTab] = useState('add'); 29 | const [isRefreshing, setIsRefreshing] = useState(false); 30 | 31 | const [formData, setFormData] = useState({ 32 | phoenixd_domain: '', 33 | lnbits_domain: '', 34 | use_real_certs: false, 35 | use_postgres: false, 36 | email: '' 37 | }); 38 | 39 | // Fetch active stacks 40 | const fetchStacks = async () => { 41 | try { 42 | setIsRefreshing(true); 43 | console.log('Fetching active stacks...'); 44 | const response = await fetch(`${apiUrl}/stacks`, { 45 | headers: { 46 | 'Authorization': `Bearer ${token}` 47 | } 48 | }); 49 | if (!response.ok) throw new Error('Failed to fetch stacks'); 50 | 51 | const responseText = await response.text(); 52 | console.log('Stacks response:', responseText); 53 | 54 | const data = JSON.parse(responseText); 55 | console.log('Parsed stacks:', data); 56 | 57 | setActiveStacks(data); 58 | console.log('Active stacks updated:', data); 59 | } catch (err) { 60 | console.error('Error fetching stacks:', err); 61 | setError('Failed to fetch stacks: ' + err.message); 62 | } finally { 63 | setIsRefreshing(false); 64 | } 65 | }; 66 | 67 | // Verifica se ci sono job attivi all'avvio 68 | useEffect(() => { 69 | fetchStacks(); 70 | 71 | // Controlla se ci sono job salvati nel localStorage 72 | const savedJobsString = localStorage.getItem('lightstack_activeJobs'); 73 | if (savedJobsString) { 74 | try { 75 | const savedJobs = JSON.parse(savedJobsString); 76 | console.log('Job recuperati dal localStorage:', savedJobs); 77 | 78 | if (Array.isArray(savedJobs) && savedJobs.length > 0) { 79 | setActiveJobs(savedJobs); 80 | 81 | // Riprendi il monitoraggio di ciascun job non completato 82 | savedJobs.forEach(job => { 83 | if (job.status !== 'completed' && job.status !== 'failed') { 84 | console.log(`Riprendendo monitoraggio job: ${job.id}`); 85 | monitorJobStatus(job.id); 86 | } 87 | }); 88 | } 89 | } catch (e) { 90 | console.error("Errore nel recupero dei job salvati:", e); 91 | localStorage.removeItem('lightstack_activeJobs'); 92 | } 93 | } 94 | }, [token]); 95 | 96 | // Salva i job attivi nel localStorage ad ogni cambiamento 97 | useEffect(() => { 98 | if (activeJobs.length > 0) { 99 | console.log('Salvataggio job nel localStorage:', activeJobs); 100 | localStorage.setItem('lightstack_activeJobs', JSON.stringify(activeJobs)); 101 | } else { 102 | localStorage.removeItem('lightstack_activeJobs'); 103 | } 104 | }, [activeJobs]); 105 | 106 | // Handle form input changes 107 | const handleInputChange = (e) => { 108 | const { name, value } = e.target; 109 | setFormData(prevState => ({ 110 | ...prevState, 111 | [name]: value 112 | })); 113 | }; 114 | 115 | // Handle toggle changes 116 | const handleToggleChange = (name) => { 117 | setFormData(prevState => ({ 118 | ...prevState, 119 | [name]: !prevState[name] 120 | })); 121 | }; 122 | 123 | // Funzione per monitorare lo stato del job 124 | const monitorJobStatus = async (jobId) => { 125 | try { 126 | let jobCompleted = false; 127 | let attempts = 0; 128 | const maxAttempts = 60; // 10 minuti con intervallo di 10s 129 | 130 | // Aggiungi il job alla lista dei job attivi se non esiste già 131 | setActiveJobs(prev => { 132 | if (!prev.some(j => j.id === jobId)) { 133 | return [...prev, { id: jobId, status: 'pending' }]; 134 | } 135 | return prev; 136 | }); 137 | 138 | while (!jobCompleted && attempts < maxAttempts) { 139 | attempts++; 140 | console.log(`[Job ${jobId}] Tentativo ${attempts}/${maxAttempts}`); 141 | 142 | await new Promise(resolve => setTimeout(resolve, 10000)); // Aspetta 10 secondi 143 | 144 | try { 145 | console.log(`[Job ${jobId}] Richiesta status al server...`); 146 | const response = await fetch(`${apiUrl}/jobs/${jobId}`, { 147 | headers: { 148 | 'Authorization': `Bearer ${token}` 149 | } 150 | }); 151 | 152 | if (!response.ok) { 153 | console.warn(`[Job ${jobId}] Risposta non valida: ${response.status}`); 154 | continue; 155 | } 156 | 157 | const responseText = await response.text(); 158 | console.log(`[Job ${jobId}] Risposta: ${responseText}`); 159 | 160 | const jobData = JSON.parse(responseText); 161 | console.log(`[Job ${jobId}] Status: ${jobData.status}`); 162 | 163 | // Aggiorna lo stato del job nella lista 164 | setActiveJobs(prev => 165 | prev.map(job => 166 | job.id === jobId ? { ...job, status: jobData.status } : job 167 | ) 168 | ); 169 | 170 | if (jobData.status === 'completed') { 171 | console.log(`[Job ${jobId}] Completato con stack_id: ${jobData.stack_id}`); 172 | jobCompleted = true; 173 | 174 | // Forza aggiornamento stacks 175 | await fetchStacks(); 176 | 177 | // Mostra messaggio di successo più evidente 178 | setSuccess(`Stack ${jobData.stack_id} creato con successo! Disponibile nella scheda "Manage Stacks".`); 179 | 180 | // Aggiorna il job con lo stack_id 181 | setActiveJobs(prev => 182 | prev.map(job => 183 | job.id === jobId ? { ...job, status: 'completed', stack_id: jobData.stack_id } : job 184 | ) 185 | ); 186 | 187 | // Cambia tab dopo un breve ritardo 188 | setTimeout(() => { 189 | setActiveTab('manage'); 190 | }, 1000); 191 | 192 | break; 193 | } else if (jobData.status === 'failed') { 194 | jobCompleted = true; 195 | setError(`Stack creation failed: ${jobData.error || 'Unknown error'}`); 196 | 197 | // Aggiorna il job con l'errore 198 | setActiveJobs(prev => 199 | prev.map(job => 200 | job.id === jobId ? { ...job, status: 'failed', error: jobData.error } : job 201 | ) 202 | ); 203 | 204 | break; 205 | } 206 | } catch (error) { 207 | console.error(`[Job ${jobId}] Errore:`, error); 208 | } 209 | } 210 | 211 | if (!jobCompleted) { 212 | // Caso in cui il job è ancora in corso dopo tutti i tentativi 213 | console.log(`[Job ${jobId}] Limite tentativi raggiunto, si consiglia verifica manuale`); 214 | setSuccess('Stack creation is still in progress. Please check the Manage Stacks tab or refresh the page later.'); 215 | } 216 | 217 | // In ogni caso, aggiorna la lista degli stack alla fine 218 | await fetchStacks(); 219 | } catch (err) { 220 | console.error(`[Job ${jobId}] Errore nel monitoraggio:`, err); 221 | setError('Failed to monitor job status. Check Manage Stacks tab later.'); 222 | await fetchStacks(); 223 | } 224 | }; 225 | 226 | // Handle stack addition 227 | const handleAddStack = async (e) => { 228 | e.preventDefault(); 229 | setIsLoading(true); 230 | setError(null); 231 | setSuccess(null); 232 | 233 | try { 234 | // Invia la richiesta per creare lo stack 235 | console.log('Creating stack with data:', formData); 236 | const response = await fetch(`${apiUrl}/stacks`, { 237 | method: 'POST', 238 | headers: { 239 | 'Authorization': `Bearer ${token}`, 240 | 'Content-Type': 'application/json', 241 | }, 242 | body: JSON.stringify(formData) 243 | }); 244 | 245 | console.log('Response status:', response.status); 246 | const responseText = await response.text(); 247 | console.log('Response text:', responseText); 248 | 249 | if (!response.ok) { 250 | throw new Error(responseText || 'Failed to add stack'); 251 | } 252 | 253 | const responseData = JSON.parse(responseText); 254 | console.log('Parsed response:', responseData); 255 | 256 | const jobId = responseData.job_id; 257 | 258 | // Mostra un messaggio informativo 259 | setSuccess('Stack creation started. This process may take several minutes. You will be notified when complete.'); 260 | 261 | // Avvia il polling per controllare lo stato del job 262 | monitorJobStatus(jobId); 263 | 264 | // Resetta il form 265 | setFormData({ 266 | phoenixd_domain: '', 267 | lnbits_domain: '', 268 | use_real_certs: false, 269 | use_postgres: false, 270 | email: '' 271 | }); 272 | } catch (err) { 273 | console.error('Error in handleAddStack:', err); 274 | setError(err.message); 275 | } finally { 276 | setIsLoading(false); 277 | } 278 | }; 279 | 280 | // Handle stack removal 281 | const handleRemoveStack = async (stackId) => { 282 | if (!confirm('Are you sure you want to remove this stack? This action cannot be undone.')) { 283 | return; 284 | } 285 | 286 | setIsLoading(true); 287 | setError(null); 288 | setSuccess(null); 289 | 290 | try { 291 | console.log(`Removing stack ${stackId}...`); 292 | const response = await fetch(`${apiUrl}/stacks/${stackId}`, { 293 | method: 'DELETE', 294 | headers: { 295 | 'Authorization': `Bearer ${token}` 296 | } 297 | }); 298 | 299 | if (!response.ok) { 300 | const errorData = await response.json(); 301 | throw new Error(errorData.detail || 'Failed to remove stack'); 302 | } 303 | 304 | console.log(`Stack ${stackId} removed successfully`); 305 | setActiveStacks(prev => prev.filter(stack => stack.id !== stackId)); 306 | setSuccess('Stack removed successfully!'); 307 | 308 | // Rimuovi anche eventuali job completati relativi a questo stack 309 | setActiveJobs(prev => prev.filter(job => job.stack_id !== stackId)); 310 | } catch (err) { 311 | console.error(`Error removing stack ${stackId}:`, err); 312 | setError(err.message); 313 | } finally { 314 | setIsLoading(false); 315 | } 316 | }; 317 | 318 | // Forza aggiornamento 319 | const handleForceRefresh = async () => { 320 | setSuccess("Aggiornamento in corso..."); 321 | await fetchStacks(); 322 | setSuccess("Lista stack aggiornata!"); 323 | }; 324 | 325 | // Ottieni lo stato di un job formattato per la visualizzazione 326 | const getJobStatusLabel = (status) => { 327 | switch (status) { 328 | case 'pending': 329 | return In attesa; 330 | case 'running': 331 | return In esecuzione; 332 | case 'completed': 333 | return Completato; 334 | case 'failed': 335 | return Fallito; 336 | default: 337 | return {status}; 338 | } 339 | }; 340 | 341 | // Rimuovi job completati 342 | const clearCompletedJobs = () => { 343 | setActiveJobs(prev => prev.filter(job => job.status !== 'completed' && job.status !== 'failed')); 344 | }; 345 | 346 | return ( 347 |
348 |
349 |

Lightstack Management

350 |
351 | 359 | 363 |
364 |
365 | 366 | 367 | 368 | 369 | 370 | Add Stack 371 | 372 | Manage Stacks 373 | {activeJobs.length > 0 && ( 374 | 375 | {activeJobs.filter(j => j.status === 'pending' || j.status === 'running').length} 376 | 377 | )} 378 | 379 | 380 | 381 | 382 |
383 |
384 | 385 | 393 |
394 | 395 |
396 | 397 | 405 |
406 | 407 |
408 | handleToggleChange('use_real_certs')} 412 | /> 413 | 414 |
415 | 416 | {formData.use_real_certs && ( 417 |
418 | 419 | 428 |

429 | This email will be used for important notifications about your SSL certificates. 430 |

431 |
432 | )} 433 | 434 |
435 | handleToggleChange('use_postgres')} 439 | /> 440 | 441 |
442 | 443 | 444 | 445 | 446 | Make sure your domains are correctly configured and pointing to this server before adding a stack. 447 | 448 | 449 | 450 | 467 |
468 | 469 | {/* Mostra job attivi */} 470 | {activeJobs.length > 0 && ( 471 |
472 |
473 |

Active Jobs

474 | {activeJobs.some(job => job.status === 'completed' || job.status === 'failed') && ( 475 | 482 | )} 483 |
484 |
485 | {activeJobs.map(job => ( 486 |
491 |
492 |
493 | Job: {job.id.substring(0, 8)}... 494 | {job.stack_id && ( 495 | Stack ID: {job.stack_id} 496 | )} 497 |
498 |
{getJobStatusLabel(job.status)}
499 |
500 | {job.status === 'completed' && job.stack_id && ( 501 |
502 | Stack created successfully! 503 | 511 |
512 | )} 513 | {job.status === 'failed' && job.error && ( 514 |
515 | Error: {job.error} 516 |
517 | )} 518 |
519 | ))} 520 |
521 |
522 | )} 523 |
524 | 525 | 526 |
527 |

Active Stacks

528 | 537 |
538 |
539 | {activeStacks.length === 0 ? ( 540 |
541 | No active stacks found. Add your first stack using the "Add Stack" tab. 542 |
543 | ) : ( 544 | activeStacks.map(stack => ( 545 | 546 |
547 |
548 |

Stack {stack.id}

549 |
550 |

551 | Phoenixd API:{' '} 552 | 558 | {stack.phoenixd_domain} 559 | 560 | 561 |

562 |

563 | LNbits:{' '} 564 | 570 | {stack.lnbits_domain} 571 | 572 | 573 |

574 |
575 |
576 | 584 |
585 |
586 | )) 587 | )} 588 |
589 | 590 | {/* Mostra job attivi anche nella tab di gestione */} 591 | {activeJobs.filter(job => job.status === 'pending' || job.status === 'running').length > 0 && ( 592 |
593 |

Stacks in creazione

594 |
595 | {activeJobs 596 | .filter(job => job.status === 'pending' || job.status === 'running') 597 | .map(job => ( 598 |
599 |
600 |
601 | Job: {job.id.substring(0, 8)}... 602 |
603 |
{getJobStatusLabel(job.status)}
604 |
605 |
606 | )) 607 | } 608 |
609 |
610 | )} 611 |
612 |
613 | 614 | {error && ( 615 | 616 | 617 | {error} 618 | 619 | )} 620 | 621 | {success && ( 622 | 623 | 624 | {success} 625 | 626 | )} 627 |
628 |
629 |
630 | ); 631 | }; 632 | 633 | export default LightstackDashboard; 634 | --------------------------------------------------------------------------------