10
.gitignore
vendored
|
@ -13,4 +13,12 @@ state/*
|
|||
tipi.config.json
|
||||
|
||||
# Commit empty directories
|
||||
!nignx/.gitkeep
|
||||
!nignx/.gitkeep
|
||||
|
||||
media/data/movies/*
|
||||
media/data/tv/*
|
||||
!media/data/movies/.gitkeep
|
||||
!media/data/tv/.gitkeep
|
||||
|
||||
media/torrents/*
|
||||
!media/torrents/.gitkeep
|
|
@ -3,3 +3,5 @@
|
|||

|
||||

|
||||

|
||||
|
||||

|
||||
|
|
|
@ -5,9 +5,8 @@ packages:
|
|||
- iptables
|
||||
- coreutils
|
||||
- git
|
||||
- base-devel
|
||||
- docker
|
||||
- avahi
|
||||
- avahi-daemon
|
||||
- nodejs
|
||||
- npm
|
||||
|
||||
|
@ -15,4 +14,4 @@ username: nicolas
|
|||
|
||||
### ZSH Settings
|
||||
zsh_theme: "powerlevel10k/powerlevel10k"
|
||||
ohmyzsh_git_url: https://github.com/robbyrussell/oh-my-zsh
|
||||
ohmyzsh_git_url: https://github.com/robbyrussell/oh-my-zsh
|
||||
|
|
|
@ -7,10 +7,5 @@
|
|||
- import_tasks: ./tasks/common/zsh.yml
|
||||
- import_tasks: ./tasks/common/docker.yml
|
||||
- import_tasks: ./tasks/network/avahi.yml
|
||||
# - import_tasks: tasks/zsh.yml
|
||||
# - import_tasks: tasks/nginx.yml
|
||||
# - import_tasks: tasks/pi-hole.yml
|
||||
# - import_tasks: tasks/pi-vpn.yml
|
||||
# - import_tasks: tasks/nextcloud.yml
|
||||
# - name: Reboot machine
|
||||
# reboot:
|
||||
|
|
6
ansible/stop.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- hosts: tipi
|
||||
become: yes
|
||||
|
||||
tasks:
|
||||
- import_tasks: ./tasks/common/teardown.yml
|
|
@ -1,8 +1,33 @@
|
|||
- name: Install docker
|
||||
package:
|
||||
name: docker
|
||||
name:
|
||||
- docker
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- lsb-release
|
||||
state: latest
|
||||
|
||||
- name: Add docker gpg key
|
||||
shell: curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
|
||||
- name: Check lsb_release -cs
|
||||
shell: lsb_release -cs
|
||||
register: lsb_release
|
||||
|
||||
- name: Add deb for bookworm release
|
||||
shell: echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bullseye stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
when: lsb_release.stdout == "bookworm"
|
||||
|
||||
- name: Add deb for non-bookworm
|
||||
shell: echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
when: lsb_release.stdout != "bookworm"
|
||||
|
||||
- name: Update packages
|
||||
apt:
|
||||
update_cache: yes
|
||||
upgrade: yes
|
||||
|
||||
- name: Install essential packages
|
||||
package:
|
||||
name:
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
- name: Change machine hostname to tipi.local
|
||||
shell: hostnamectl set-hostname tipi.local
|
||||
|
||||
- name: Update packages
|
||||
become: yes
|
||||
pacman:
|
||||
update_cache: yes
|
||||
upgrade: yes
|
||||
|
||||
- name: Add user to root group
|
||||
user:
|
||||
name: "{{ username }}"
|
||||
group: root
|
||||
# - name: Update packages
|
||||
# apt:
|
||||
# update_cache: yes
|
||||
# upgrade: yes
|
||||
|
||||
- name: Install essential packages
|
||||
package:
|
||||
name: "{{ packages }}"
|
||||
state: latest
|
||||
|
||||
- name: Add user to root group
|
||||
user:
|
||||
name: "{{ username }}"
|
||||
group: root
|
||||
|
||||
- name: Disable SSH password auth
|
||||
lineinfile:
|
||||
dest: /etc/ssh/sshd_config
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- name: Install "pm2" package globally.
|
||||
community.general.npm:
|
||||
name: yarn
|
||||
name: pm2
|
||||
global: yes
|
||||
|
||||
- name: Run pm2 first time
|
||||
|
@ -29,15 +29,15 @@
|
|||
|
||||
- name: Check if app is already running
|
||||
become_user: "{{ username }}"
|
||||
shell: pm2 list
|
||||
shell: pm2 status system-api
|
||||
register: pm2_result
|
||||
|
||||
- name: Start app
|
||||
become_user: "{{ username }}"
|
||||
shell: cd {{ playbook_dir }}/../system-api && pm2 start npm --name "system-api" -- start
|
||||
when: pm2_result.stdout.find("system-api") == -1
|
||||
when: pm2_result.stdout.find("online") == -1
|
||||
|
||||
- name: Reload app
|
||||
become_user: "{{ username }}"
|
||||
shell: pm2 reload system-api
|
||||
when: pm2_result.stdout.find("system-api") != -1
|
||||
when: pm2_result.stdout.find("online") != -1
|
||||
|
|
9
ansible/tasks/common/teardown.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
- name: Check if app is already running
|
||||
become_user: "{{ username }}"
|
||||
shell: pm2 list
|
||||
register: pm2_result
|
||||
|
||||
- name: Stop app
|
||||
become_user: "{{ username }}"
|
||||
shell: pm2 stop "system-api"
|
||||
when: pm2_result.stdout.find("system-api") != -1
|
|
@ -7,10 +7,10 @@
|
|||
<port>80</port>
|
||||
</service>
|
||||
</service-group>
|
||||
<service-group>
|
||||
<!-- <service-group>
|
||||
<name replace-wildcards="yes">%h</name>
|
||||
<service>
|
||||
<type>_http._tcp</type>
|
||||
<port>443</port>
|
||||
</service>
|
||||
</service-group>
|
||||
</service-group> -->
|
45
apps/anonaddy/config.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "Anonaddy",
|
||||
"port": 8084,
|
||||
"id": "anonaddy",
|
||||
"description": "",
|
||||
"short_desc": "Anonymous email forwarding",
|
||||
"author": "",
|
||||
"source": "https://github.com/anonaddy/anonaddy",
|
||||
"image": "https://avatars.githubusercontent.com/u/51450862?s=200&v=4",
|
||||
"requirements": {
|
||||
"ports": [25]
|
||||
},
|
||||
"form_fields": {
|
||||
"username": {
|
||||
"type": "text",
|
||||
"label": "Username",
|
||||
"required": true,
|
||||
"env_variable": "ANONADDY_USERNAME"
|
||||
},
|
||||
"key": {
|
||||
"type": "text",
|
||||
"label": "App key",
|
||||
"hint": "Application key for encrypter service. Generate one with : echo \"base64:$(openssl rand -base64 32)\"",
|
||||
"required": true,
|
||||
"env_variable": "ANONADDY_KEY"
|
||||
},
|
||||
"domain": {
|
||||
"type": "fqdn",
|
||||
"label": "Your email domain (eg. example.com)",
|
||||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "ANONADDY_DOMAIN"
|
||||
},
|
||||
"secret": {
|
||||
"type": "text",
|
||||
"label": "App secret",
|
||||
"hint": "Long random string used when hashing data for the anonymous replies",
|
||||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "ANONADDY_SECRET"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,29 +32,32 @@ services:
|
|||
container_name: anonaddy
|
||||
ports:
|
||||
- 25:25
|
||||
- ${APP_ANONADDY_PORT}:8000
|
||||
- ${APP_PORT}:8000
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
volumes:
|
||||
- "${APP_DATA_DIR}/data:/data"
|
||||
environment:
|
||||
TZ: ${TZ}
|
||||
DB_HOST: db-anonaddy
|
||||
DB_PASSWORD: anonaddy
|
||||
REDIS_HOST: redis-anonaddy
|
||||
APP_KEY: ${APP_ANONADDY_KEY}
|
||||
ANONADDY_DOMAIN: ${APP_ANONADDY_DOMAIN}
|
||||
ANONADDY_SECRET: ${APP_ANONADDY_SECRET}
|
||||
APP_KEY: ${ANONADDY_KEY}
|
||||
ANONADDY_DOMAIN: ${ANONADDY_DOMAIN}
|
||||
ANONADDY_SECRET: ${ANONADDY_SECRET}
|
||||
ANONADDY_ADMIN_USERNAME: ${ANONADDY_USERNAME}
|
||||
POSTFIX_DEBUG: true
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.anonaddy.rule: Host(`anonaddy.tipi.home`)
|
||||
traefik.http.routers.anonaddy.tls: true
|
||||
traefik.http.routers.anonaddy.entrypoints: websecure
|
||||
traefik.http.routers.anonaddy.service: anonaddy
|
||||
traefik.http.services.anonaddy.loadbalancer.server.port: 8000
|
||||
# labels:
|
||||
# traefik.enable: true
|
||||
# traefik.http.routers.anonaddy.rule: Host(`anonaddy.tipi.home`)
|
||||
# traefik.http.routers.anonaddy.tls: true
|
||||
# traefik.http.routers.anonaddy.entrypoints: websecure
|
||||
# traefik.http.routers.anonaddy.service: anonaddy
|
||||
# traefik.http.services.anonaddy.loadbalancer.server.port: 8000
|
||||
# labels:
|
||||
# traefik.enable: true
|
||||
# traefik.http.routers.anonaddy.rule: PathPrefix(`/anonaddy`)
|
||||
|
|
|
@ -2,5 +2,4 @@ version: "3.7"
|
|||
|
||||
networks:
|
||||
tipi_main_network:
|
||||
external:
|
||||
name: runtipi_tipi_main_network
|
12
apps/filebrowser/config.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "File Browser",
|
||||
"port": 8096,
|
||||
"id": "filebrowser",
|
||||
"description": "Reliable and Performant File Management Desktop Sync and File Sharing",
|
||||
"short_desc": "Access your homeserver files from your browser",
|
||||
"author": "",
|
||||
"website": "https://filebrowser.org/",
|
||||
"source": "https://github.com/filebrowser/filebrowser",
|
||||
"image": "https://avatars.githubusercontent.com/u/35781395?s=200&v=4",
|
||||
"form_fields": {}
|
||||
}
|
8
apps/filebrowser/data/settings.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"port": 80,
|
||||
"baseURL": "",
|
||||
"address": "",
|
||||
"log": "stdout",
|
||||
"database": "/database/filebrowser.db",
|
||||
"root": "/srv"
|
||||
}
|
15
apps/filebrowser/docker-compose.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
services:
|
||||
filebrowser:
|
||||
container_name: filebrowser
|
||||
image: filebrowser/filebrowser:s6
|
||||
ports:
|
||||
- ${APP_PORT}:80
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- ${ROOT_FOLDER}:/srv
|
||||
- ${APP_DATA_DIR}/data/filebrowser.db:/database/filebrowser.db
|
||||
- ${APP_DATA_DIR}/data/settings.json:/config/settings.json
|
||||
networks:
|
||||
- tipi_main_network
|
11
apps/filerun/config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "FileRun",
|
||||
"port": 8087,
|
||||
"id": "filerun",
|
||||
"description": "Reliable and Performant File Management Desktop Sync and File Sharing",
|
||||
"short_desc": "Access your homeserver files from your browser",
|
||||
"author": "FileRun, LDA - Portugal",
|
||||
"source": "https://www.filerun.com/",
|
||||
"image": "https://avatars.githubusercontent.com/u/6422152?v=4",
|
||||
"form_fields": {}
|
||||
}
|
38
apps/filerun/docker-compose.yml
Normal file
|
@ -0,0 +1,38 @@
|
|||
services:
|
||||
filerun-db:
|
||||
container_name: filerun-db
|
||||
user: 1000:1000
|
||||
image: mariadb:10.1
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: tipi
|
||||
MYSQL_USER: tipi
|
||||
MYSQL_PASSWORD: tipi
|
||||
MYSQL_DATABASE: tipi
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/db:/var/lib/mysql
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
filerun:
|
||||
container_name: filerun
|
||||
image: filerun/filerun:arm64v8
|
||||
environment:
|
||||
FR_DB_HOST: filerun-db
|
||||
FR_DB_PORT: 3306
|
||||
FR_DB_NAME: tipi
|
||||
FR_DB_USER: tipi
|
||||
FR_DB_PASS: tipi
|
||||
APACHE_RUN_USER: 1000
|
||||
APACHE_RUN_GROUP: 1000
|
||||
APACHE_RUN_USER_ID: 33
|
||||
APACHE_RUN_GROUP_ID: 33
|
||||
depends_on:
|
||||
- db
|
||||
links:
|
||||
- db:db
|
||||
ports:
|
||||
- ${APP_PORT}:80
|
||||
volumes:
|
||||
- ${ROOT_FOLDER}/app-data/medias:/user-files
|
||||
networks:
|
||||
- tipi_main_network
|
11
apps/freshrss/config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "FreshRSS",
|
||||
"port": 8086,
|
||||
"id": "freshrss",
|
||||
"description": "FreshRSS is a self-hosted RSS feed aggregator like Leed or Kriss Feed.\nIt is lightweight, easy to work with, powerful, and customizable.\n\nIt is a multi-user application with an anonymous reading mode. It supports custom tags. There is an API for (mobile) clients, and a Command-Line Interface.\n\nThanks to the WebSub standard (formerly PubSubHubbub), FreshRSS is able to receive instant push notifications from compatible sources, such as Mastodon, Friendica, WordPress, Blogger, FeedBurner, etc.\n\nFreshRSS natively supports basic Web scraping, based on XPath, for Web sites not providing any RSS / Atom feed.\n\nFinally, it supports extensions for further tuning.",
|
||||
"short_desc": "A free, self-hostable aggregator… ",
|
||||
"author": "https://freshrss.org/",
|
||||
"source": "https://github.com/FreshRSS/FreshRSS",
|
||||
"image": "https://avatars.githubusercontent.com/u/9414285?s=200&v=4",
|
||||
"form_fields": {}
|
||||
}
|
|
@ -6,19 +6,19 @@ services:
|
|||
image: freshrss/freshrss:arm
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${APP_FRESHRSS_PORT}:80"
|
||||
- ${APP_PORT}:80
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/:/var/www/FreshRSS/data
|
||||
- ${APP_DATA_DIR}/extensions/:/var/www/FreshRSS/extensions
|
||||
- ${APP_DATA_DIR}/data/freshrss:/var/www/FreshRSS/data
|
||||
- ${APP_DATA_DIR}/data/extensions/:/var/www/FreshRSS/extensions
|
||||
environment:
|
||||
CRON_MIN: '*/20'
|
||||
TZ: $TZ
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.freshrss.rule: Host(`freshrss.tipi.home`)
|
||||
traefik.http.routers.freshrss.service: freshrss
|
||||
traefik.http.routers.freshrss.tls: true
|
||||
traefik.http.routers.freshrss.entrypoints: websecure
|
||||
traefik.http.services.freshrss.loadbalancer.server.port: 80
|
||||
# labels:
|
||||
# traefik.enable: true
|
||||
# traefik.http.routers.freshrss.rule: Host(`freshrss.tipi.home`)
|
||||
# traefik.http.routers.freshrss.service: freshrss
|
||||
# traefik.http.routers.freshrss.tls: true
|
||||
# traefik.http.routers.freshrss.entrypoints: websecure
|
||||
# traefik.http.services.freshrss.loadbalancer.server.port: 80
|
11
apps/invidious/config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "Invidious",
|
||||
"port": 8095,
|
||||
"id": "invidious",
|
||||
"description": "",
|
||||
"short_desc": "",
|
||||
"author": "",
|
||||
"source": "https://github.com/iv-org/invidious",
|
||||
"image": "https://raw.githubusercontent.com/iv-org/invidious/master/assets/invidious-colored-vector.svg",
|
||||
"form_fields": {}
|
||||
}
|
45
apps/invidious/docker-compose.yml
Normal file
|
@ -0,0 +1,45 @@
|
|||
version: "3"
|
||||
services:
|
||||
invidious:
|
||||
user: 1000:1000
|
||||
container_name: invidious
|
||||
image: quay.io/invidious/invidious:latest-arm64
|
||||
# image: quay.io/invidious/invidious:latest-arm64 # ARM64/AArch64 devices
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${APP_PORT}:3000"
|
||||
environment:
|
||||
# Please read the following file for a comprehensive list of all available
|
||||
# configuration options and their associated syntax:
|
||||
# https://github.com/iv-org/invidious/blob/master/config/config.example.yml
|
||||
INVIDIOUS_CONFIG: |
|
||||
db:
|
||||
dbname: invidious
|
||||
user: tipi
|
||||
password: tipi
|
||||
host: invidious-db
|
||||
port: 5432
|
||||
check_tables: true
|
||||
healthcheck:
|
||||
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/comments/jNQXAC9IVRw || exit 1
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
depends_on:
|
||||
- invidious-db
|
||||
|
||||
invidious-db:
|
||||
user: 1000:1000
|
||||
container_name: invidious-db
|
||||
image: docker.io/library/postgres:14
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/postgres:/var/lib/postgresql/data
|
||||
- ${APP_DATA_DIR}/data/sql:/config/sql
|
||||
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
|
||||
environment:
|
||||
POSTGRES_DB: invidious
|
||||
POSTGRES_USER: tipi
|
||||
POSTGRES_PASSWORD: tipi
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
13
apps/jackett/config.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Jackett",
|
||||
"port": 8097,
|
||||
"id": "jackett",
|
||||
"description": "Jackett works as a proxy server: it translates queries from apps (Sonarr, Radarr, SickRage, CouchPotato, Mylar3, Lidarr, DuckieTV, qBittorrent, Nefarious etc.) into tracker-site-specific http queries, parses the html or json response, and then sends results back to the requesting software. This allows for getting recent uploads (like RSS) and performing searches.",
|
||||
"short_desc": "API Support for your favorite torrent trackers ",
|
||||
"author": "",
|
||||
"source": "https://github.com/Jackett/Jackett",
|
||||
"image": "https://avatars.githubusercontent.com/u/15383019?s=200&v=4",
|
||||
"form_fields": {
|
||||
|
||||
}
|
||||
}
|
20
apps/jackett/docker-compose.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
version: "3.7"
|
||||
services:
|
||||
jackett:
|
||||
image: lscr.io/linuxserver/jackett
|
||||
container_name: jackett
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${TZ}
|
||||
- AUTO_UPDATE=true
|
||||
dns:
|
||||
- ${DNS_IP}
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data:/config
|
||||
- ${ROOT_FOLDER}/media/torrents:/downloads
|
||||
ports:
|
||||
- ${APP_PORT}:9117
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tipi_main_network
|
11
apps/jellyfin/config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "Jellyfin",
|
||||
"port": 8091,
|
||||
"id": "jellyfin",
|
||||
"description": "",
|
||||
"short_desc": "",
|
||||
"author": "",
|
||||
"source": "",
|
||||
"image": "https://avatars.githubusercontent.com/u/45698031?s=200&v=4",
|
||||
"form_fields": {}
|
||||
}
|
18
apps/jellyfin/docker-compose.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
jellyfin:
|
||||
image: lscr.io/linuxserver/jellyfin
|
||||
container_name: jellyfin
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/config:/config
|
||||
- ${ROOT_FOLDER}/media/data:/data/media
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${TZ}
|
||||
restart: "unless-stopped"
|
||||
ports:
|
||||
- ${APP_PORT}:8096
|
||||
networks:
|
||||
- tipi_main_network
|
13
apps/joplin/README.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in Markdown format.
|
||||
Notes exported from Evernote can be imported into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
|
||||
|
||||
The notes can be securely synchronised using end-to-end encryption with various cloud services including Nextcloud, Dropbox, OneDrive and Joplin Cloud.
|
||||
|
||||
Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.
|
||||
|
||||
The application is available for Windows, Linux, macOS, Android and iOS. A Web Clipper, to save web pages and screenshots from your browser, is also available for Firefox and Chrome.
|
||||
|
||||
## Credentials
|
||||
|
||||
Username: admin@localhost
|
||||
Password: admin
|
12
apps/joplin/config.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "Joplin Server",
|
||||
"port": 8099,
|
||||
"id": "joplin",
|
||||
"description": "",
|
||||
"short_desc": "Note taking and to-do application with synchronisation",
|
||||
"author": "https://github.com/laurent22",
|
||||
"source": "https://github.com/laurent22/joplin",
|
||||
"website": "https://joplinapp.org",
|
||||
"image": "https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png",
|
||||
"form_fields": {}
|
||||
}
|
38
apps/joplin/docker-compose.yml
Normal file
|
@ -0,0 +1,38 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
db-joplin:
|
||||
container_name: db-joplin
|
||||
image: postgres:14.2
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/postgres:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=tipi
|
||||
- POSTGRES_USER=tipi
|
||||
- POSTGRES_DB=joplin
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
joplin:
|
||||
container_name: joplin
|
||||
image: florider89/joplin-server:2.7.4
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db-joplin
|
||||
ports:
|
||||
- ${APP_PORT}:22300
|
||||
dns:
|
||||
- ${DNS_IP}
|
||||
environment:
|
||||
- APP_PORT=22300
|
||||
- APP_BASE_URL=http://${INTERNAL_IP}:${APP_PORT}
|
||||
- DB_CLIENT=pg
|
||||
- POSTGRES_PASSWORD=tipi
|
||||
- POSTGRES_USER=tipi
|
||||
- POSTGRES_DATABASE=joplin
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_HOST=db-joplin
|
||||
- MAX_TIME_DRIFT=0
|
||||
networks:
|
||||
- tipi_main_network
|
12
apps/n8n/config.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"port": 8094,
|
||||
"id": "n8n",
|
||||
"description": "n8n is an extendable workflow automation tool. With a fair-code distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything.",
|
||||
"short_desc": "Workflow Automation Tool. Alternative to Zapier",
|
||||
"author": "n8n.io",
|
||||
"source": "https://github.com/n8n-io/n8n",
|
||||
"website": "https://n8n.io/",
|
||||
"image": "https://avatars.githubusercontent.com/u/45487711?s=200&v=4",
|
||||
"form_fields": {}
|
||||
}
|
36
apps/n8n/docker-compose.yml
Normal file
|
@ -0,0 +1,36 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
db-n8n:
|
||||
container_name: db-n8n
|
||||
image: postgres:14.2
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/db:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=tipi
|
||||
- POSTGRES_USER=tipi
|
||||
- POSTGRES_DB=n8n
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
n8n:
|
||||
container_name: n8n
|
||||
image: n8nio/n8n:0.174.0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${APP_PORT}:5678
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/n8n:/home/node/.n8n
|
||||
command: /bin/sh -c "sleep 5; n8n start"
|
||||
environment:
|
||||
- DB-TYPE=postgresdb
|
||||
- DB_POSTGRESDB_DATABASE=n8n
|
||||
- DB_POSTGRESDB_HOST=db-n8n
|
||||
- DB_POSTGRESDB_PORT=5432
|
||||
- DB_POSTGRESDB_USER=tipi
|
||||
- DB_POSTGRESDB_PASSWORD=tipi
|
||||
depends_on:
|
||||
- db-n8n
|
||||
networks:
|
||||
- tipi_main_network
|
|
@ -1,6 +1,12 @@
|
|||
{
|
||||
"name": "Nextcloud",
|
||||
"port": 8083,
|
||||
"id": "nextcloud",
|
||||
"description": "Nextcloud is a self-hosted, open source, and fully-featured cloud storage solution for your personal files, office documents, and photos.",
|
||||
"short_desc": "Productivity platform that keeps you in control",
|
||||
"author": "Nextcloud GmbH",
|
||||
"source": "https://github.com/nextcloud/server",
|
||||
"image": "https://avatars.githubusercontent.com/u/19211038?s=200&v=4",
|
||||
"form_fields": {
|
||||
"username": {
|
||||
"type": "text",
|
||||
|
@ -8,7 +14,7 @@
|
|||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "NEXTCLOUD_USERNAME"
|
||||
"env_variable": "NEXTCLOUD_ADMIN_USER"
|
||||
},
|
||||
"password": {
|
||||
"type": "password",
|
||||
|
@ -16,7 +22,7 @@
|
|||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "NEXTCLOUD_PASSWORD"
|
||||
"env_variable": "NEXTCLOUD_ADMIN_PASSWORD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,24 +3,21 @@ version: "3.7"
|
|||
services:
|
||||
db-nextcloud:
|
||||
container_name: db-nextcloud
|
||||
# user: '1000:1000'
|
||||
image: mariadb:10.5.12
|
||||
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
|
||||
image: postgres:14.2
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/db:/var/lib/mysql
|
||||
- ${APP_DATA_DIR}/data/db:/var/lib/postgresql/data
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=password
|
||||
- MYSQL_PASSWORD=password
|
||||
- MYSQL_DATABASE=nextcloud
|
||||
- MYSQL_USER=nextcloud
|
||||
- POSTGRES_PASSWORD=tipi
|
||||
- POSTGRES_USER=tipi
|
||||
- POSTGRES_DB=nextcloud
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
redis-nextcloud:
|
||||
container_name: redis-nextcloud
|
||||
# user: '1000:1000'
|
||||
image: redis:6.2.2-buster
|
||||
user: "1000:1000"
|
||||
image: redis:6.2.6
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- "${APP_DATA_DIR}/data/redis:/data"
|
||||
|
@ -28,7 +25,7 @@ services:
|
|||
- tipi_main_network
|
||||
|
||||
cron:
|
||||
image: nextcloud:22.0.0-apache
|
||||
image: nextcloud:23.0.3-apache
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/nextcloud:/var/www/html
|
||||
|
@ -40,23 +37,22 @@ services:
|
|||
- tipi_main_network
|
||||
|
||||
nextcloud:
|
||||
user: root
|
||||
container_name: nextcloud
|
||||
image: nextcloud:22.1.1-apache
|
||||
image: nextcloud:23.0.3-apache
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${APP_NEXTCLOUD_PORT}:80
|
||||
- ${APP_PORT}:80
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/nextcloud:/var/www/html
|
||||
environment:
|
||||
- MYSQL_HOST=db-nextcloud
|
||||
- POSTGRES_HOST=db-nextcloud
|
||||
- REDIS_HOST=redis-nextcloud
|
||||
- MYSQL_PASSWORD=password
|
||||
- MYSQL_DATABASE=nextcloud
|
||||
- MYSQL_USER=nextcloud
|
||||
- POSTGRES_PASSWORD=tipi
|
||||
- POSTGRES_USER=tipi
|
||||
- POSTGRES_DB=nextcloud
|
||||
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
|
||||
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
|
||||
- NEXTCLOUD_TRUSTED_DOMAINS=tipi.local
|
||||
- NEXTCLOUD_TRUSTED_DOMAINS=${DEVICE_IP}:${APP_PORT}
|
||||
depends_on:
|
||||
- db-nextcloud
|
||||
- redis-nextcloud
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
## DNS Over TLS, Simple ENCRYPTED recursive caching DNS, TCP port 853
|
||||
## unbound.conf, original at https://calomel.org/unbound_dns.html
|
||||
# tweaks by bartonbytes.com
|
||||
server:
|
||||
access-control: 127.0.0.0/8 allow
|
||||
cache-max-ttl: 14400
|
||||
cache-min-ttl: 600
|
||||
do-tcp: yes
|
||||
hide-identity: yes
|
||||
hide-version: yes
|
||||
interface: 127.0.0.1
|
||||
minimal-responses: yes
|
||||
prefetch: yes
|
||||
qname-minimisation: yes
|
||||
rrset-roundrobin: yes
|
||||
ssl-upstream: yes
|
||||
use-caps-for-id: yes
|
||||
verbosity: 1
|
||||
port: 5533
|
||||
#
|
||||
forward-zone:
|
||||
name: "."
|
||||
forward-addr: 194.242.2.3@853 # Mullvad primary
|
||||
forward-addr: 193.19.108.3@853 # Mullvad secondary
|
|
@ -1,34 +0,0 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
unbound:
|
||||
user: '1000:1000'
|
||||
image: "klutchell/unbound:latest"
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/unbound:/etc/unbound
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
pihole:
|
||||
image: pihole/pihole
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 53:53
|
||||
- 53:53/udp
|
||||
- ${APP_PI_HOLE_PORT}:80
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/pihole:/etc/pihole/
|
||||
- ${APP_DATA_DIR}/data/dnsmasq:/etc/dnsmasq.d/
|
||||
environment:
|
||||
- VIRTUAL_HOST="pihole.${DOMAIN}"
|
||||
- WEBPASSWORD=${APP_PASSWORD}
|
||||
- PIHOLE_DNS=unbound
|
||||
depends_on:
|
||||
- unbound
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.traefik.rule: Host(`pihole.${DOMAIN}`)
|
||||
traefik.http.services.traefik.loadbalancer.server.port: $APP_PI_HOLE_PORT
|
||||
|
23
apps/pihole/config.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "PiHole",
|
||||
"port": 8081,
|
||||
"requirements": {
|
||||
"ports": [53]
|
||||
},
|
||||
"id": "pihole",
|
||||
"description": "",
|
||||
"short_desc": "",
|
||||
"author": "",
|
||||
"source": "",
|
||||
"image": "https://avatars.githubusercontent.com/u/16827203?s=200&v=4",
|
||||
"form_fields": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"label": "Password",
|
||||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "APP_PASSWORD"
|
||||
}
|
||||
}
|
||||
}
|
6
apps/pihole/data/unbound/a-records.conf
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
# A Record
|
||||
#local-data: "somecomputer.local. A 192.168.1.1"
|
||||
|
||||
# PTR Record
|
||||
#local-data-ptr: "192.168.1.1 somecomputer.local."
|
92
apps/pihole/data/unbound/root.hints
Normal file
|
@ -0,0 +1,92 @@
|
|||
; This file holds the information on root name servers needed to
|
||||
; initialize cache of Internet domain name servers
|
||||
; (e.g. reference this file in the "cache . <file>"
|
||||
; configuration file of BIND domain name servers).
|
||||
;
|
||||
; This file is made available by InterNIC
|
||||
; under anonymous FTP as
|
||||
; file /domain/named.cache
|
||||
; on server FTP.INTERNIC.NET
|
||||
; -OR- RS.INTERNIC.NET
|
||||
;
|
||||
; last update: December 07, 2021
|
||||
; related version of root zone: 2021120701
|
||||
;
|
||||
; FORMERLY NS.INTERNIC.NET
|
||||
;
|
||||
. 3600000 NS A.ROOT-SERVERS.NET.
|
||||
A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4
|
||||
A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30
|
||||
;
|
||||
; FORMERLY NS1.ISI.EDU
|
||||
;
|
||||
. 3600000 NS B.ROOT-SERVERS.NET.
|
||||
B.ROOT-SERVERS.NET. 3600000 A 199.9.14.201
|
||||
B.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:200::b
|
||||
;
|
||||
; FORMERLY C.PSI.NET
|
||||
;
|
||||
. 3600000 NS C.ROOT-SERVERS.NET.
|
||||
C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12
|
||||
C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c
|
||||
;
|
||||
; FORMERLY TERP.UMD.EDU
|
||||
;
|
||||
. 3600000 NS D.ROOT-SERVERS.NET.
|
||||
D.ROOT-SERVERS.NET. 3600000 A 199.7.91.13
|
||||
D.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2d::d
|
||||
;
|
||||
; FORMERLY NS.NASA.GOV
|
||||
;
|
||||
. 3600000 NS E.ROOT-SERVERS.NET.
|
||||
E.ROOT-SERVERS.NET. 3600000 A 192.203.230.10
|
||||
E.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:a8::e
|
||||
;
|
||||
; FORMERLY NS.ISC.ORG
|
||||
;
|
||||
. 3600000 NS F.ROOT-SERVERS.NET.
|
||||
F.ROOT-SERVERS.NET. 3600000 A 192.5.5.241
|
||||
F.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2f::f
|
||||
;
|
||||
; FORMERLY NS.NIC.DDN.MIL
|
||||
;
|
||||
. 3600000 NS G.ROOT-SERVERS.NET.
|
||||
G.ROOT-SERVERS.NET. 3600000 A 192.112.36.4
|
||||
G.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:12::d0d
|
||||
;
|
||||
; FORMERLY AOS.ARL.ARMY.MIL
|
||||
;
|
||||
. 3600000 NS H.ROOT-SERVERS.NET.
|
||||
H.ROOT-SERVERS.NET. 3600000 A 198.97.190.53
|
||||
H.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:1::53
|
||||
;
|
||||
; FORMERLY NIC.NORDU.NET
|
||||
;
|
||||
. 3600000 NS I.ROOT-SERVERS.NET.
|
||||
I.ROOT-SERVERS.NET. 3600000 A 192.36.148.17
|
||||
I.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fe::53
|
||||
;
|
||||
; OPERATED BY VERISIGN, INC.
|
||||
;
|
||||
. 3600000 NS J.ROOT-SERVERS.NET.
|
||||
J.ROOT-SERVERS.NET. 3600000 A 192.58.128.30
|
||||
J.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:c27::2:30
|
||||
;
|
||||
; OPERATED BY RIPE NCC
|
||||
;
|
||||
. 3600000 NS K.ROOT-SERVERS.NET.
|
||||
K.ROOT-SERVERS.NET. 3600000 A 193.0.14.129
|
||||
K.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fd::1
|
||||
;
|
||||
; OPERATED BY ICANN
|
||||
;
|
||||
. 3600000 NS L.ROOT-SERVERS.NET.
|
||||
L.ROOT-SERVERS.NET. 3600000 A 199.7.83.42
|
||||
L.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:9f::42
|
||||
;
|
||||
; OPERATED BY WIDE
|
||||
;
|
||||
. 3600000 NS M.ROOT-SERVERS.NET.
|
||||
M.ROOT-SERVERS.NET. 3600000 A 202.12.27.33
|
||||
M.ROOT-SERVERS.NET. 3600000 AAAA 2001:dc3::35
|
||||
; End of file
|
9
apps/pihole/data/unbound/root.key
Normal file
|
@ -0,0 +1,9 @@
|
|||
; autotrust trust anchor file
|
||||
;;id: . 1
|
||||
;;last_queried: 1650921300 ;;Mon Apr 25 21:15:00 2022
|
||||
;;last_success: 1650921300 ;;Mon Apr 25 21:15:00 2022
|
||||
;;next_probe_time: 1650962281 ;;Tue Apr 26 08:38:01 2022
|
||||
;;query_failed: 0
|
||||
;;query_interval: 43200
|
||||
;;retry_time: 8640
|
||||
. 86400 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU= ;{id = 20326 (ksk), size = 2048b} ;;state=2 [ VALID ] ;;count=0 ;;lastchange=1650921210 ;;Mon Apr 25 21:13:30 2022
|
136
apps/pihole/data/unbound/unbound.conf
Normal file
|
@ -0,0 +1,136 @@
|
|||
# https://linux.die.net/man/5/unbound.conf
|
||||
# https://docs.pi-hole.net/guides/unbound/
|
||||
|
||||
server:
|
||||
# Enable or disable whether the unbound server forks into the background
|
||||
# as a daemon. Default is yes.
|
||||
do-daemonize: no
|
||||
|
||||
# If given, after binding the port the user privileges are dropped.
|
||||
# Default is "unbound". If you give username: "" no user change is performed.
|
||||
username: ""
|
||||
|
||||
# No need to chroot as this container has been stripped of all other binaries.
|
||||
chroot: ""
|
||||
|
||||
# If "" is given, logging goes to stderr, or nowhere once daemonized.
|
||||
logfile: ""
|
||||
|
||||
# The process id is written to the file. Not required since we are running
|
||||
# in a container with one process.
|
||||
pidfile: ""
|
||||
|
||||
# The verbosity number, level 0 means no verbosity, only errors.
|
||||
# Level 1 gives operational information.
|
||||
# Level 2 gives detailed operational information.
|
||||
# Level 3 gives query level information, output per query.
|
||||
# Level 4 gives algorithm level information.
|
||||
# Level 5 logs client identification for cache misses.
|
||||
# Default is level 1. The verbosity can also be increased from the commandline.
|
||||
verbosity: 1
|
||||
|
||||
# Listen on all ipv4 interfaces, answer queries from the local subnet.
|
||||
interface: 0.0.0.0
|
||||
|
||||
# The port number, default 53, on which the server responds to queries.
|
||||
port: 53
|
||||
|
||||
do-ip4: yes
|
||||
do-udp: yes
|
||||
do-tcp: yes
|
||||
do-ip6: no
|
||||
|
||||
# You want to leave this to no unless you have *native* IPv6. With 6to4 and
|
||||
# Terredo tunnels your web browser should favor IPv4 for the same reasons
|
||||
prefer-ip6: no
|
||||
|
||||
# Trust glue only if it is within the server's authority
|
||||
harden-glue: yes
|
||||
|
||||
# Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
|
||||
harden-dnssec-stripped: yes
|
||||
|
||||
# Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes
|
||||
# see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378 for further details
|
||||
use-caps-for-id: no
|
||||
|
||||
# Reduce EDNS reassembly buffer size (see also https://docs.pi-hole.net/guides/dns/unbound/ )
|
||||
# IP fragmentation is unreliable on the Internet today, and can cause
|
||||
# transmission failures when large DNS messages are sent via UDP. Even
|
||||
# when fragmentation does work, it may not be secure; it is theoretically
|
||||
# possible to spoof parts of a fragmented DNS message, without easy
|
||||
# detection at the receiving end. Recently, there was an excellent study
|
||||
# >>> Defragmenting DNS - Determining the optimal maximum UDP response size for DNS <<<
|
||||
# by Axel Koolhaas, and Tjeerd Slokker (https://indico.dns-oarc.net/event/36/contributions/776/)
|
||||
# in collaboration with NLnet Labs explored DNS using real world data from the
|
||||
# the RIPE Atlas probes and the researchers suggested different values for
|
||||
# IPv4 and IPv6 and in different scenarios. They advise that servers should
|
||||
# be configured to limit DNS messages sent over UDP to a size that will not
|
||||
# trigger fragmentation on typical network links. DNS servers can switch
|
||||
# from UDP to TCP when a DNS response is too big to fit in this limited
|
||||
# buffer size. This value has also been suggested in DNS Flag Day 2020.
|
||||
edns-buffer-size: 1232
|
||||
|
||||
# Perform prefetching of close to expired message cache entries
|
||||
# This only applies to domains that have been frequently queried
|
||||
prefetch: yes
|
||||
|
||||
# One thread should be sufficient, can be increased on beefy machines.
|
||||
# In reality for most users running on small networks or on a single machine,
|
||||
# it should be unnecessary to seek performance enhancement by increasing num-threads above 1.
|
||||
num-threads: 1
|
||||
|
||||
# Ensure kernel buffer is large enough to not lose messages in traffic spikes
|
||||
# (requires CAP_NET_ADMIN or privileged)
|
||||
# so-rcvbuf: 1m
|
||||
|
||||
# The netblock is given as an IP4 or IP6 address with /size appended for a
|
||||
# classless network block. The action can be deny, refuse, allow or allow_snoop.
|
||||
access-control: 127.0.0.1/32 allow
|
||||
access-control: 192.168.0.0/16 allow
|
||||
access-control: 172.16.0.0/12 allow
|
||||
access-control: 10.0.0.0/8 allow
|
||||
access-control: 100.64.0.0/10 allow
|
||||
access-control: 10.21.21.0/24 allow
|
||||
|
||||
# Ensure privacy of local IP ranges
|
||||
private-address: 192.168.0.0/16
|
||||
private-address: 169.254.0.0/16
|
||||
private-address: 172.16.0.0/12
|
||||
private-address: 10.0.0.0/8
|
||||
private-address: fd00::/8
|
||||
private-address: fe80::/10
|
||||
|
||||
# Read the root hints from this file. Default is nothing, using built in
|
||||
# hints for the IN class. The file has the format of zone files, with root
|
||||
# nameserver names and addresses only. The default may become outdated,
|
||||
# when servers change, therefore it is good practice to use a root-hints
|
||||
# file. get one from https://www.internic.net/domain/named.root
|
||||
root-hints: /etc/unbound/root.hints
|
||||
|
||||
# File with trust anchor for one zone, which is tracked with RFC5011 probes.
|
||||
# The probes are several times per month, thus the machine must be online frequently.
|
||||
# The initial file can be one with contents as described in trust-anchor-file.
|
||||
# The file is written to when the anchor is updated, so the unbound user must
|
||||
# have write permission.
|
||||
auto-trust-anchor-file: /etc/unbound/root.key
|
||||
|
||||
# Number of ports to open. This number of file descriptors can be opened per thread.
|
||||
# Must be at least 1. Default depends on compile options. Larger numbers need extra
|
||||
# resources from the operating system. For performance a very large value is best,
|
||||
# use libevent to make this possible.
|
||||
outgoing-range: 8192
|
||||
|
||||
# The number of queries that every thread will service simultaneously. If more queries
|
||||
# arrive that need servicing, and no queries can be jostled out (see jostle-timeout),
|
||||
# then the queries are dropped. This forces the client to resend after a timeout;
|
||||
# allowing the server time to work on the existing queries. Default depends on
|
||||
# compile options, 512 or 1024.
|
||||
num-queries-per-thread: 4096
|
||||
|
||||
include: /etc/unbound/a-records.conf
|
||||
|
||||
# forward-zone:
|
||||
# name: "."
|
||||
# forward-addr: 194.242.2.3@853 # Mullvad primary
|
||||
# forward-addr: 193.19.108.3@853 # Mullvad secondary
|
39
apps/pihole/docker-compose.yml
Normal file
|
@ -0,0 +1,39 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
unbound:
|
||||
image: "klutchell/unbound"
|
||||
container_name: unbound
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- "${APP_DATA_DIR}/data/unbound:/etc/unbound"
|
||||
networks:
|
||||
tipi_main_network:
|
||||
ipv4_address: 10.21.21.200
|
||||
|
||||
pihole:
|
||||
depends_on: [unbound]
|
||||
container_name: pihole
|
||||
image: pihole/pihole:latest
|
||||
restart: unless-stopped
|
||||
hostname: pihole
|
||||
dns:
|
||||
- 127.0.0.1
|
||||
- 10.21.21.200 # Points to unbound
|
||||
ports:
|
||||
- 53:53/tcp
|
||||
- 53:53/udp
|
||||
- ${APP_PORT}:80
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/pihole:/etc/pihole
|
||||
- ${APP_DATA_DIR}/data/dnsmasq:/etc/dnsmasq.d
|
||||
environment:
|
||||
TZ: ${TZ}
|
||||
WEBPASSWORD: ${APP_PASSWORD}
|
||||
PIHOLE_DNS_: 10.21.21.200 # Points to unbound
|
||||
FTLCONF_REPLY_ADDR4: 10.21.21.201
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
networks:
|
||||
tipi_main_network:
|
||||
ipv4_address: 10.21.21.201
|
20
apps/radarr/config.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "Radarr",
|
||||
"port": 8088,
|
||||
"id": "radarr",
|
||||
"description": "",
|
||||
"short_desc": "",
|
||||
"author": "",
|
||||
"source": "",
|
||||
"image": "https://avatars.githubusercontent.com/u/25025331?s=200&v=4",
|
||||
"form_fields": {
|
||||
"torrent-client": {
|
||||
"type": "text",
|
||||
"label": "Torrent Client",
|
||||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "TORRENT_CLIENT"
|
||||
}
|
||||
}
|
||||
}
|
20
apps/radarr/docker-compose.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
version: "3.7"
|
||||
services:
|
||||
radarr:
|
||||
image: lscr.io/linuxserver/radarr
|
||||
container_name: radarr
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${TZ}
|
||||
dns:
|
||||
- ${DNS_IP}
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data:/config
|
||||
- ${ROOT_FOLDER}/media/data/movies:/movies #optional
|
||||
- ${ROOT_FOLDER}/media/torrents:/downloads #optional
|
||||
ports:
|
||||
- ${APP_PORT}:7878
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tipi_main_network
|
11
apps/simple-torrent/config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "Simple Torrent",
|
||||
"port": 8085,
|
||||
"id": "simple-torrent",
|
||||
"description": "SimpleTorrent is a a self-hosted remote torrent client, written in Go (golang). Started torrents remotely, download sets of files on the local disk of the server, which are then retrievable or streamable via HTTP.",
|
||||
"short_desc": "A self-hosted remote torrent client",
|
||||
"author": "",
|
||||
"source": "https://github.com/boypt/simple-torrent",
|
||||
"image": "https://getumbrel.github.io/umbrel-apps-gallery/simple-torrent/icon.svg",
|
||||
"form_fields": {}
|
||||
}
|
|
@ -6,9 +6,9 @@ services:
|
|||
image: boypt/cloud-torrent:1.3.9
|
||||
restart: on-failure
|
||||
ports:
|
||||
- "${APP_SIMPLETORRENT_PORT}:${APP_SIMPLETORRENT_PORT}"
|
||||
- ${APP_PORT}:${APP_PORT}
|
||||
command: >
|
||||
--port=${APP_SIMPLETORRENT_PORT}
|
||||
--port=${APP_PORT}
|
||||
--config-path /config/simple-torrent.json
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/torrents:/torrents
|
||||
|
|
13
apps/sonarr/config.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Sonarr",
|
||||
"port": 8098,
|
||||
"id": "sonarr",
|
||||
"description": "",
|
||||
"short_desc": "",
|
||||
"author": "",
|
||||
"source": "",
|
||||
"image": "https://avatars.githubusercontent.com/u/1082903?s=200&v=4",
|
||||
"form_fields": {
|
||||
|
||||
}
|
||||
}
|
20
apps/sonarr/docker-compose.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
version: "3.7"
|
||||
services:
|
||||
radarr:
|
||||
image: lscr.io/linuxserver/sonarr
|
||||
container_name: sonarr
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${TZ}
|
||||
dns:
|
||||
- ${DNS_IP}
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data:/config
|
||||
- ${ROOT_FOLDER}/media/data/tv:/tv #optional
|
||||
- ${ROOT_FOLDER}/media/torrents:/downloads #optional
|
||||
ports:
|
||||
- ${APP_PORT}:8989
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tipi_main_network
|
12
apps/syncthing/config.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "Syncthing",
|
||||
"port": 8090,
|
||||
"id": "syncthing",
|
||||
"description": "Syncthing is a peer-to-peer continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.\n\nInstall the Syncthing app on your Umbrel and pair it with the Syncthing app on your phone or computer for a self hosted peer-to-peer backup solution.",
|
||||
"short_desc": "Peer-to-peer file synchronization between your devices",
|
||||
"author": "The Syncthing Foundation",
|
||||
"source": "https://github.com/syncthing",
|
||||
"website": "https://syncthing.net",
|
||||
"image": "https://avatars.githubusercontent.com/u/7628018?s=200&v=4",
|
||||
"form_fields": {}
|
||||
}
|
21
apps/syncthing/docker-compose.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
syncthing:
|
||||
container_name: syncthing
|
||||
image: syncthing/syncthing:1.19
|
||||
stop_grace_period: 1m
|
||||
hostname: tipi
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data:/var/syncthing
|
||||
ports:
|
||||
- ${APP_PORT}:8384
|
||||
- 22000:22000/tcp # TCP file transfers
|
||||
- 22000:22000/udp # QUIC file transfers
|
||||
- 21027:21027/udp # Receive local discovery broadcasts
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tipi_main_network
|
12
apps/tailscale/config.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "Tailscale",
|
||||
"port": 8093,
|
||||
"id": "tailscale",
|
||||
"description": "",
|
||||
"short_desc": "",
|
||||
"author": "",
|
||||
"source": "https://github.com/tailscale/tailscale",
|
||||
"website": "https://tailscale.com/",
|
||||
"image": "https://avatars.githubusercontent.com/u/48932923?s=200&v=4",
|
||||
"form_fields": {}
|
||||
}
|
14
apps/tailscale/docker-compose.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
version: "2"
|
||||
|
||||
services:
|
||||
tailscale:
|
||||
container_name: tailscale
|
||||
network_mode: "host" # TODO: Find a way to remove this
|
||||
image: tailscale/tailscale:v1.24.0
|
||||
privileged: true
|
||||
restart: on-failure
|
||||
stop_grace_period: 1m
|
||||
command: "sh -c 'tailscale web --listen 0.0.0.0:${APP_PORT} & exec tailscaled --tun=userspace-networking'"
|
||||
volumes:
|
||||
- /var/lib:/var/lib
|
||||
- /dev/net/tun:/dev/net/tun
|
|
@ -1,6 +0,0 @@
|
|||
FROM ubuntu:latest
|
||||
|
||||
# Install curl
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
|
||||
ENTRYPOINT ["tail", "-f", "/dev/null"]
|
|
@ -1,8 +0,0 @@
|
|||
version: '3.7'
|
||||
services:
|
||||
test:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
networks:
|
||||
- tipi_main_network
|
31
apps/transmission/config.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "Transmission",
|
||||
"port": 8089,
|
||||
"requirements": {
|
||||
"ports": [51413]
|
||||
},
|
||||
"id": "transmission",
|
||||
"description": "",
|
||||
"short_desc": "",
|
||||
"author": "",
|
||||
"source": "https://transmissionbt.com",
|
||||
"image": "https://avatars.githubusercontent.com/u/223312?s=200&v=4",
|
||||
"form_fields": {
|
||||
"username": {
|
||||
"type": "text",
|
||||
"label": "Username",
|
||||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "TRANSMISSION_USERNAME"
|
||||
},
|
||||
"password": {
|
||||
"type": "password",
|
||||
"label": "Password",
|
||||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "TRANSMISSION_PASSWORD"
|
||||
}
|
||||
}
|
||||
}
|
24
apps/transmission/docker-compose.yml
Normal file
|
@ -0,0 +1,24 @@
|
|||
version: "3.7"
|
||||
services:
|
||||
transmission:
|
||||
image: lscr.io/linuxserver/transmission
|
||||
container_name: transmission
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${TZ}
|
||||
- USER=${TRANSMISSION_USERNAME}
|
||||
- PASS=${TRANSMISSION_PASSWORD}
|
||||
# - WHITELIST=iplist #optional
|
||||
# - PEERPORT=peerport #optional
|
||||
# - HOST_WHITELIST=dnsnane list #optional
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/config:/config
|
||||
- ${ROOT_FOLDER}/media/torrents:/downloads
|
||||
ports:
|
||||
- ${APP_PORT}:9091
|
||||
- 51413:51413
|
||||
- 51413:51413/udp
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tipi_main_network
|
29
apps/ttyd/config.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "TTYD - Web terminal",
|
||||
"port": 8092,
|
||||
"id": "ttyd",
|
||||
"description": "",
|
||||
"short_desc": "A utility that allows you to access a command line from your web browser",
|
||||
"author": "",
|
||||
"source": "",
|
||||
"image": "",
|
||||
"form_fields": {
|
||||
"username": {
|
||||
"type": "text",
|
||||
"label": "Username",
|
||||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "TRANSMISSION_USERNAME"
|
||||
},
|
||||
"password": {
|
||||
"type": "password",
|
||||
"label": "Password",
|
||||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "TRANSMISSION_PASSWORD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
0
apps/ttyd/docker-compose.yml
Normal file
35
apps/wg-easy/config.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "Wireguard",
|
||||
"port": 8082,
|
||||
"requirements": {
|
||||
"ports": [51820]
|
||||
},
|
||||
"id": "wg-easy",
|
||||
"description": "Access your homeserver from anywhere even on your mobile device. Wireguard-easy is a simple tool to configure and manage Wireguard VPN servers. It is written in Go and uses the official Wireguard client. You have to open and redirect port 51820 to your homeserver in order to connect.",
|
||||
"short_desc": "VPN server for your homeserver",
|
||||
"author": "WeeJeWel",
|
||||
"source": "https://github.com/WeeJeWel/wg-easy/",
|
||||
"image": "https://avatars.githubusercontent.com/u/13991055?s=200&v=4",
|
||||
"form_fields": {
|
||||
"host": {
|
||||
"type": "fqdnip",
|
||||
"label": "Your public IP address or domain name",
|
||||
"required": true,
|
||||
"env_variable": "WIREGUARD_HOST"
|
||||
},
|
||||
"password": {
|
||||
"type": "password",
|
||||
"label": "Password",
|
||||
"max": 50,
|
||||
"min": 3,
|
||||
"required": true,
|
||||
"env_variable": "WIREGUARD_PASSWORD"
|
||||
},
|
||||
"dns": {
|
||||
"type": "ip",
|
||||
"label": "Default DNS server",
|
||||
"required": false,
|
||||
"env_variable": "WIREGUARD_DNS"
|
||||
}
|
||||
}
|
||||
}
|
0
apps/wg-easy/data/.gitkeep
Normal file
|
@ -1,29 +1,36 @@
|
|||
version: '3.7'
|
||||
version: "3.7"
|
||||
services:
|
||||
wg-easy:
|
||||
container_name: wg-easy
|
||||
image: 'weejewel/wg-easy:latest'
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}:/etc/wireguard
|
||||
ports:
|
||||
- 51820:51820
|
||||
- ${APP_WGEASY_PORT}:51821
|
||||
environment:
|
||||
WG_HOST: '${WIREGUARD_HOST}'
|
||||
PASSWORD: '${WIREGUARD_PASSWORD}'
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
sysctls:
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
- net.ipv4.ip_forward=1
|
||||
networks:
|
||||
- tipi_main_network
|
||||
# labels:
|
||||
# traefik.enable: true
|
||||
# traefik.http.routers.wireguard.rule: Host(`wireguard.tipi.home`)
|
||||
# traefik.http.routers.wireguard.service: wireguard
|
||||
# traefik.http.routers.wireguard.tls: true
|
||||
# traefik.http.routers.wireguard.entrypoints: websecure
|
||||
# traefik.http.services.wireguard.loadbalancer.server.port: 51821
|
||||
container_name: wg-easy
|
||||
image: "meienberger/wg-easy:latest"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data:/etc/wireguard
|
||||
- /lib/modules:/lib/modules
|
||||
ports:
|
||||
- 51822:51820/udp
|
||||
- ${APP_PORT}:51821/tcp
|
||||
environment:
|
||||
WG_HOST: "${WIREGUARD_HOST}"
|
||||
PASSWORD: "${WIREGUARD_PASSWORD}"
|
||||
WG_ALLOWED_IPS: 0.0.0.0/0,::/0
|
||||
WG_PORT: 51822
|
||||
WG_DEFAULT_DNS: "${WIREGUARD_DNS:-8.8.8.8}"
|
||||
WG_FWMARK: 51820
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
dns:
|
||||
- "${WIREGUARD_DNS:-8.8.8.8}"
|
||||
sysctls:
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
- net.ipv4.ip_forward=1
|
||||
networks:
|
||||
- tipi_main_network
|
||||
# labels:
|
||||
# traefik.enable: true
|
||||
# traefik.http.routers.wireguard.rule: Host(`wireguard.tipi.home`)
|
||||
# traefik.http.routers.wireguard.service: wireguard
|
||||
# traefik.http.routers.wireguard.tls: true
|
||||
# traefik.http.routers.wireguard.entrypoints: websecure
|
||||
# traefik.http.services.wireguard.loadbalancer.server.port: 51821
|
||||
|
|
2
dashboard/.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.config.js
|
||||
.eslintrc.js
|
17
dashboard/.eslintrc.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
module.exports = {
|
||||
extends: ['next/core-web-vitals', 'airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
rules: {
|
||||
'arrow-body-style': 0,
|
||||
'no-restricted-exports': 0,
|
||||
'max-len': [1, { code: 200 }],
|
||||
'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
|
||||
},
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
6
dashboard/.prettierrc.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: true,
|
||||
trailingComma: "all",
|
||||
printWidth: 200,
|
||||
};
|
|
@ -9,6 +9,9 @@ RUN yarn
|
|||
|
||||
COPY ./ ./
|
||||
|
||||
ARG INTERNAL_IP_ARG
|
||||
ENV INTERNAL_IP $INTERNAL_IP_ARG
|
||||
|
||||
RUN yarn build
|
||||
|
||||
CMD ["yarn", "start"]
|
12
dashboard/Dockerfile.dev
Normal file
|
@ -0,0 +1,12 @@
|
|||
FROM node:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package.json ./
|
||||
COPY ./yarn.lock ./
|
||||
|
||||
RUN yarn
|
||||
|
||||
COPY ./ ./
|
||||
|
||||
CMD ["yarn", "dev"]
|
|
@ -1,6 +1,11 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const { NODE_ENV, INTERNAL_IP } = process.env;
|
||||
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
}
|
||||
env: {
|
||||
INTERNAL_IP: NODE_ENV === 'development' ? 'localhost' : INTERNAL_IP,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = nextConfig;
|
||||
|
|
|
@ -12,20 +12,36 @@
|
|||
"@chakra-ui/react": "^1.8.7",
|
||||
"@emotion/react": "^11",
|
||||
"@emotion/styled": "^11",
|
||||
"@fontsource/open-sans": "^4.5.8",
|
||||
"axios": "^0.26.1",
|
||||
"clsx": "^1.1.1",
|
||||
"final-form": "^4.20.6",
|
||||
"framer-motion": "^6",
|
||||
"immer": "^9.0.12",
|
||||
"js-cookie": "^3.0.1",
|
||||
"next": "12.1.4",
|
||||
"react": "18.0.0",
|
||||
"react-dom": "18.0.0",
|
||||
"react-final-form": "^6.5.9",
|
||||
"react-icons": "^4.3.1",
|
||||
"swr": "^1.3.0",
|
||||
"systeminformation": "^5.11.9",
|
||||
"validator": "^13.7.0"
|
||||
"validator": "^13.7.0",
|
||||
"zustand": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/node": "17.0.23",
|
||||
"@types/react": "17.0.43",
|
||||
"@types/react-dom": "17.0.14",
|
||||
"@types/validator": "^13.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"eslint": "8.12.0",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "12.1.4",
|
||||
"postcss": "^8.4.12",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"typescript": "4.6.3"
|
||||
}
|
||||
}
|
||||
|
|
6
dashboard/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
dashboard/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
dashboard/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
dashboard/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
9
dashboard/public/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
dashboard/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
dashboard/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
|
@ -1,4 +0,0 @@
|
|||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
BIN
dashboard/public/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
20
dashboard/public/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2263 5057 c-67 -34 -125 -65 -128 -68 -3 -4 53 -126 125 -271 l132
|
||||
-263 -122 -275 c-66 -151 -130 -295 -142 -320 -11 -25 -37 -83 -58 -130 -21
|
||||
-47 -347 -706 -725 -1465 -378 -759 -786 -1578 -906 -1820 l-219 -440 2337 -3
|
||||
c1285 -1 2338 0 2340 2 2 2 -397 809 -888 1792 -706 1417 -931 1879 -1085
|
||||
2224 l-194 434 134 267 133 267 -135 66 c-103 51 -137 64 -143 54 -5 -7 -41
|
||||
-79 -81 -160 -40 -81 -75 -147 -78 -148 -3 0 -27 44 -54 98 -101 203 -111 222
|
||||
-116 221 -3 0 -60 -29 -127 -62z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1,003 B |
19
dashboard/public/site.webmanifest
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
BIN
dashboard/public/tipi.png
Normal file
After Width: | Height: | Size: 11 KiB |
23
dashboard/src/components/AppTile/AppStatus.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { FiPauseCircle, FiPlayCircle } from 'react-icons/fi';
|
||||
import { AppStatus as TAppStatus } from '../../core/types';
|
||||
|
||||
const AppStatus: React.FC<{ status: TAppStatus }> = ({ status }) => {
|
||||
if (status === 'running') {
|
||||
return (
|
||||
<>
|
||||
<FiPlayCircle className="text-green-500 mr-1" size={20} />
|
||||
<span className="text-gray-400 text-sm">Running</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FiPauseCircle className="text-red-500 mr-1" size={20} />
|
||||
<span className="text-gray-400 text-sm">Stopped</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppStatus;
|
32
dashboard/src/components/AppTile/index.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { Box, SlideFade, Image, useColorModeValue } from '@chakra-ui/react';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { FiChevronRight } from 'react-icons/fi';
|
||||
import { AppConfig } from '../../core/types';
|
||||
import AppStatus from './AppStatus';
|
||||
|
||||
const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {
|
||||
const bg = useColorModeValue('white', '#1a202c');
|
||||
|
||||
return (
|
||||
<Link href={`/apps/${app.id}`} passHref>
|
||||
<SlideFade in className="flex flex-1" offsetY="20px">
|
||||
<Box minWidth={400} bg={bg} className="flex flex-1 border-2 drop-shadow-sm rounded-lg p-3 items-center cursor-pointer group hover:drop-shadow-md transition-all">
|
||||
<Image alt={`${app.name} logo`} className="rounded-md drop-shadow mr-3 group-hover:scale-105 transition-all" src={app.image} width={100} height={100} />
|
||||
<div className="mr-3 flex-1">
|
||||
<h3 className="font-bold text-xl">{app.name}</h3>
|
||||
<span>{app.short_desc}</span>
|
||||
{app.installed && (
|
||||
<div className="flex mt-1">
|
||||
<AppStatus status={app.status} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FiChevronRight className="text-slate-300" size={30} />
|
||||
</Box>
|
||||
</SlideFade>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTile;
|
25
dashboard/src/components/Form/FormInput.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface IProps {
|
||||
placeholder?: string;
|
||||
error?: string;
|
||||
type?: Parameters<typeof Input>[0]['type'];
|
||||
label?: string;
|
||||
className?: string;
|
||||
isInvalid?: boolean;
|
||||
size?: Parameters<typeof Input>[0]['size'];
|
||||
}
|
||||
|
||||
const FormInput: React.FC<IProps> = ({ placeholder, error, type, label, className, isInvalid, size, ...rest }) => {
|
||||
return (
|
||||
<div className={clsx('transition-all', className)}>
|
||||
{label && <label>{label}</label>}
|
||||
<Input type={type} placeholder={placeholder} isInvalid={isInvalid} size={size} {...rest} />
|
||||
{isInvalid && <span className="text-red-500 text-sm">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormInput;
|
28
dashboard/src/components/Form/validators.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import validator from 'validator';
|
||||
import { AppConfig, FieldTypes } from '../../core/types';
|
||||
|
||||
export const validateAppConfig = (values: Record<string, string>, fields: (AppConfig['form_fields'][0] & { id: string })[]) => {
|
||||
const errors: any = {};
|
||||
|
||||
fields.forEach((field) => {
|
||||
if (field.required && !values[field.id]) {
|
||||
errors[field.id] = 'Field required';
|
||||
} else if (values[field.id] && field.min && values[field.id].length < field.min) {
|
||||
errors[field.id] = `Field must be at least ${field.min} characters long`;
|
||||
} else if (values[field.id] && field.max && values[field.id].length > field.max) {
|
||||
errors[field.id] = `Field must be at most ${field.max} characters long`;
|
||||
} else if (values[field.id] && field.type === FieldTypes.number && !validator.isNumeric(values[field.id])) {
|
||||
errors[field.id] = 'Field must be a number';
|
||||
} else if (values[field.id] && field.type === FieldTypes.email && !validator.isEmail(values[field.id])) {
|
||||
errors[field.id] = 'Field must be a valid email';
|
||||
} else if (values[field.id] && field.type === FieldTypes.fqdn && !validator.isFQDN(values[field.id] || '')) {
|
||||
errors[field.id] = 'Field must be a valid domain';
|
||||
} else if (values[field.id] && field.type === FieldTypes.ip && !validator.isIP(values[field.id])) {
|
||||
errors[field.id] = 'Field must be a valid IP address';
|
||||
} else if (values[field.id] && field.type === FieldTypes.fqdnip && !validator.isFQDN(values[field.id] || '') && !validator.isIP(values[field.id])) {
|
||||
errors[field.id] = 'Field must be a valid domain or IP address';
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
|
@ -1,27 +1,22 @@
|
|||
import React from "react";
|
||||
import Img from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Button, Flex, useBreakpointValue } from "@chakra-ui/react";
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { FiMenu } from 'react-icons/fi';
|
||||
|
||||
interface IProps {
|
||||
onClickMenu: () => void;
|
||||
}
|
||||
|
||||
const Header: React.FC<IProps> = ({ onClickMenu }) => {
|
||||
const buttonVisibility = useBreakpointValue<"visible" | "hidden">({
|
||||
base: "visible",
|
||||
md: "hidden",
|
||||
});
|
||||
|
||||
return (
|
||||
<header>
|
||||
<Flex alignItems="center" bg="tomato" paddingLeft={5} paddingRight={5}>
|
||||
<Flex position="absolute" visibility={buttonVisibility || "visible"}>
|
||||
<Button onClick={onClickMenu}>O</Button>
|
||||
</Flex>
|
||||
<header style={{ width: '100%' }} className="flex h-12 md:h-0">
|
||||
<Flex className="items-center border-b-2 bg-graycool px-5 flex-1 py-2">
|
||||
<div onClick={onClickMenu} className="visible md:invisible absolute cursor-pointer py-2">
|
||||
<FiMenu color="black" />
|
||||
</div>
|
||||
<Flex justifyContent="center" flex="1">
|
||||
<Link href="/" passHref>
|
||||
<Img src="/logo.svg" alt="Tipi" width={100} height={60} />
|
||||
<img src="/tipi.png" alt="Tipi Logo" width={30} height={30} />
|
||||
</Link>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
|
|
@ -1,33 +1,70 @@
|
|||
import {
|
||||
Button,
|
||||
Flex,
|
||||
useBreakpointValue,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import React from "react";
|
||||
import Header from "./Header";
|
||||
import Menu from "./Menu";
|
||||
import MenuDrawer from "./MenuDrawer";
|
||||
import { Flex, useDisclosure, Spinner, Breadcrumb, BreadcrumbItem, useColorModeValue, Box } from '@chakra-ui/react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { FiChevronRight } from 'react-icons/fi';
|
||||
import Header from './Header';
|
||||
import Menu from './Menu';
|
||||
import MenuDrawer from './MenuDrawer';
|
||||
|
||||
const Layout: React.FC = ({ children }) => {
|
||||
const menuWidth = useBreakpointValue({ base: 0, md: 200 });
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
interface IProps {
|
||||
loading?: boolean;
|
||||
breadcrumbs?: { name: string; href: string; current?: boolean }[];
|
||||
}
|
||||
|
||||
const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
const menubg = useColorModeValue('#F1F3F4', '#202736');
|
||||
const bg = useColorModeValue('white', '#1a202c');
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<Flex className="justify-center flex-1">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
const renderBreadcrumbs = () => {
|
||||
return (
|
||||
<Breadcrumb spacing="8px" separator={<FiChevronRight color="gray.500" />}>
|
||||
{breadcrumbs?.map((breadcrumb, index) => {
|
||||
return (
|
||||
<BreadcrumbItem className="hover:underline" isCurrentPage={breadcrumb.current} key={index}>
|
||||
<Link href={breadcrumb.href}>{breadcrumb.name}</Link>
|
||||
</BreadcrumbItem>
|
||||
);
|
||||
})}
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex height="100vh" bg="green.500" direction="column">
|
||||
<MenuDrawer isOpen={isOpen} onClose={onClose}>
|
||||
<Menu />
|
||||
</MenuDrawer>
|
||||
<Header onClickMenu={onOpen} />
|
||||
<Flex flex="1">
|
||||
<Flex width={menuWidth} bg="blue.500">
|
||||
<>
|
||||
<Head>
|
||||
<title>Tipi</title>
|
||||
</Head>
|
||||
|
||||
<Flex height="100vh" direction="column">
|
||||
<MenuDrawer isOpen={isOpen} onClose={onClose}>
|
||||
<Menu />
|
||||
</Flex>
|
||||
<Flex flex="1" padding={5} bg="yellow.300">
|
||||
{children}
|
||||
</MenuDrawer>
|
||||
<Header onClickMenu={onOpen} />
|
||||
<Flex flex={1}>
|
||||
<Flex height="100vh" bg={menubg} className="sticky top-0 invisible md:visible w-0 md:w-64">
|
||||
<Menu />
|
||||
</Flex>
|
||||
<Box bg={bg} className="flex-1 px-4 py-4 md:px-10 md:py-8">
|
||||
{renderBreadcrumbs()}
|
||||
{renderContent()}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,71 @@
|
|||
import React from "react";
|
||||
import { AiOutlineDashboard, AiOutlineSetting, AiOutlineAppstore } from 'react-icons/ai';
|
||||
import { FaRegMoon } from 'react-icons/fa';
|
||||
import { FiLogOut } from 'react-icons/fi';
|
||||
import Package from '../../../package.json';
|
||||
import { Box, Divider, Flex, List, ListItem, Switch, useColorMode } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import clsx from 'clsx';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IconType } from 'react-icons';
|
||||
import { useAuthStore } from '../../state/authStore';
|
||||
|
||||
const SideMenu: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { colorMode, setColorMode } = useColorMode();
|
||||
const { logout } = useAuthStore();
|
||||
const path = router.pathname.split('/')[1];
|
||||
|
||||
const renderMenuItem = (title: string, name: string, Icon: IconType) => {
|
||||
const selected = path === name;
|
||||
|
||||
const itemClass = clsx('mx-3 border-transparent rounded-lg p-3 transition-colors border-1', {
|
||||
'drop-shadow-sm border-gray-200': selected && colorMode === 'light',
|
||||
'bg-white': selected && colorMode === 'light',
|
||||
});
|
||||
|
||||
return (
|
||||
<Link href={`/${name}`} passHref>
|
||||
<div className={itemClass}>
|
||||
<ListItem className={'flex items-center cursor-pointer hover:font-bold'}>
|
||||
<Icon size={20} className={clsx('mr-3', { 'text-red-600': selected && colorMode === 'light', 'text-red-200': selected && colorMode === 'dark' })} />
|
||||
<p className={clsx({ 'font-bold': selected, 'text-red-600': selected && colorMode === 'light', 'text-red-200': selected && colorMode === 'dark' })}>{title}</p>
|
||||
</ListItem>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const handleChangeColorMode = (checked: boolean) => {
|
||||
setColorMode(checked ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
const Menu: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Menu</h1>
|
||||
</div>
|
||||
<Box className="flex-1 flex flex-col p-0 md:p-4">
|
||||
<img className="self-center mb-5 logo mt-0 md:mt-5" src="/tipi.png" width={512} height={512} />
|
||||
<List spacing={3} className="pt-5">
|
||||
{renderMenuItem('Dashboard', '', AiOutlineDashboard)}
|
||||
{renderMenuItem('Apps', 'apps', AiOutlineAppstore)}
|
||||
{renderMenuItem('Settings', 'settings', AiOutlineSetting)}
|
||||
</List>
|
||||
<Divider className="my-3" />
|
||||
<Flex flex="1" />
|
||||
<List>
|
||||
<div className="mx-3">
|
||||
<ListItem onClick={logout} className="cursor-pointer hover:font-bold flex items-center mb-5">
|
||||
<FiLogOut size={20} className="mr-3" />
|
||||
<p className="flex-1">Log out</p>
|
||||
</ListItem>
|
||||
<ListItem className="flex items-center">
|
||||
<FaRegMoon size={20} className="mr-3" />
|
||||
<p className="flex-1">Dark mode</p>
|
||||
<Switch checked={colorMode === 'dark'} onChange={(event) => handleChangeColorMode(event.target.checked)} />
|
||||
</ListItem>
|
||||
</div>
|
||||
</List>
|
||||
<div className="pb-1 text-center text-sm text-gray-400 mt-5">Tipi version {Package.version}</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
export default SideMenu;
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
import {
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
} from "@chakra-ui/react";
|
||||
import React from "react";
|
||||
import { Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerHeader, DrawerOverlay, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
isOpen: boolean;
|
||||
|
@ -15,16 +7,15 @@ interface IProps {
|
|||
}
|
||||
|
||||
const MenuDrawer: React.FC<IProps> = ({ children, isOpen, onClose }) => {
|
||||
const menubg = useColorModeValue('#F1F3F4', '#202736');
|
||||
|
||||
return (
|
||||
<Drawer isOpen={isOpen} placement="left" onClose={onClose}>
|
||||
<Drawer size="xs" isOpen={isOpen} placement="left" onClose={onClose}>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerContent bg={menubg}>
|
||||
<DrawerCloseButton />
|
||||
<DrawerHeader>Create your account</DrawerHeader>
|
||||
<DrawerBody>{children}</DrawerBody>
|
||||
<DrawerFooter>
|
||||
<div>Github</div>
|
||||
</DrawerFooter>
|
||||
<DrawerHeader>My Tipi</DrawerHeader>
|
||||
<DrawerBody display="flex">{children}</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { default } from "./Layout";
|
||||
export { default } from './Layout';
|
||||
|
|
12
dashboard/src/components/LoadingScreen.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Flex, Spinner } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
const LoadingScreen = () => {
|
||||
return (
|
||||
<Flex height="100vh" alignItems="center" justifyContent="center">
|
||||
<Spinner size="lg" />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
|
@ -1,4 +1,4 @@
|
|||
import validator from "validator";
|
||||
import validator from 'validator';
|
||||
|
||||
interface IFormField {
|
||||
name: string;
|
||||
|
@ -20,81 +20,79 @@ interface IAppConfig {
|
|||
}
|
||||
|
||||
const APP_ANONADDY: IAppConfig = {
|
||||
id: "anonaddy",
|
||||
name: "Anonaddy",
|
||||
description: "Create Unlimited Email Aliases For Free",
|
||||
url: "https://anonaddy.com/",
|
||||
color: "#00a8ff",
|
||||
logo: "https://anonaddy.com/favicon.ico",
|
||||
id: 'anonaddy',
|
||||
name: 'Anonaddy',
|
||||
description: 'Create Unlimited Email Aliases For Free',
|
||||
url: 'https://anonaddy.com/',
|
||||
color: '#00a8ff',
|
||||
logo: 'https://anonaddy.com/favicon.ico',
|
||||
install_form: {
|
||||
fields: [
|
||||
{
|
||||
name: "API Key",
|
||||
type: "text",
|
||||
placeholder: "API Key",
|
||||
name: 'API Key',
|
||||
type: 'text',
|
||||
placeholder: 'API Key',
|
||||
required: true,
|
||||
validate: (value: string) => validator.isBase64(value),
|
||||
},
|
||||
{
|
||||
name: "Return Path",
|
||||
type: "text",
|
||||
description: "The email address that bounces will be sent to",
|
||||
placeholder: "Return Path",
|
||||
name: 'Return Path',
|
||||
type: 'text',
|
||||
description: 'The email address that bounces will be sent to',
|
||||
placeholder: 'Return Path',
|
||||
required: false,
|
||||
validate: (value: string) => validator.isEmail(value),
|
||||
},
|
||||
{
|
||||
name: "Admin Username",
|
||||
type: "text",
|
||||
description: "The username of the admin user",
|
||||
placeholder: "Admin Username",
|
||||
name: 'Admin Username',
|
||||
type: 'text',
|
||||
description: 'The username of the admin user',
|
||||
placeholder: 'Admin Username',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "Enable Registration",
|
||||
type: "boolean",
|
||||
description: "Allow users to register",
|
||||
placeholder: "Enable Registration",
|
||||
name: 'Enable Registration',
|
||||
type: 'boolean',
|
||||
description: 'Allow users to register',
|
||||
placeholder: 'Enable Registration',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "Domain",
|
||||
type: "text",
|
||||
description: "The domain that will be used for the email address",
|
||||
placeholder: "Domain",
|
||||
name: 'Domain',
|
||||
type: 'text',
|
||||
description: 'The domain that will be used for the email address',
|
||||
placeholder: 'Domain',
|
||||
required: true,
|
||||
validate: (value: string) => validator.isFQDN(value),
|
||||
},
|
||||
{
|
||||
name: "Hostname",
|
||||
type: "text",
|
||||
description: "The hostname that will be used for the email address",
|
||||
placeholder: "Hostname",
|
||||
name: 'Hostname',
|
||||
type: 'text',
|
||||
description: 'The hostname that will be used for the email address',
|
||||
placeholder: 'Hostname',
|
||||
required: true,
|
||||
validate: (value: string) => validator.isFQDN(value),
|
||||
},
|
||||
{
|
||||
name: "Secret",
|
||||
type: "text",
|
||||
description: "The secret that will be used for the email address",
|
||||
placeholder: "Secret",
|
||||
name: 'Secret',
|
||||
type: 'text',
|
||||
description: 'The secret that will be used for the email address',
|
||||
placeholder: 'Secret',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "From Name",
|
||||
type: "text",
|
||||
description: "The name that will be used for the email address",
|
||||
placeholder: "From Name",
|
||||
name: 'From Name',
|
||||
type: 'text',
|
||||
description: 'The name that will be used for the email address',
|
||||
placeholder: 'From Name',
|
||||
required: true,
|
||||
validate: (value: string) =>
|
||||
validator.isLength(value, { min: 1, max: 64 }),
|
||||
validate: (value: string) => validator.isLength(value, { min: 1, max: 64 }),
|
||||
},
|
||||
{
|
||||
name: "From Address",
|
||||
type: "text",
|
||||
description:
|
||||
"The email address that will be used for the email address",
|
||||
placeholder: "From Address",
|
||||
name: 'From Address',
|
||||
type: 'text',
|
||||
description: 'The email address that will be used for the email address',
|
||||
placeholder: 'From Address',
|
||||
required: true,
|
||||
validate: (value: string) => validator.isEmail(value),
|
||||
},
|
||||
|
|
32
dashboard/src/core/api.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import axios, { Method } from 'axios';
|
||||
|
||||
export const BASE_URL = `http://${process.env.INTERNAL_IP}:3001`;
|
||||
|
||||
interface IFetchParams {
|
||||
endpoint: string;
|
||||
method?: Method;
|
||||
params?: JSON;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const api = async <T = unknown>(fetchParams: IFetchParams): Promise<T> => {
|
||||
const { endpoint, method = 'GET', params, data } = fetchParams;
|
||||
|
||||
const response = await axios.request<T & { error?: string }>({
|
||||
method,
|
||||
params,
|
||||
data,
|
||||
url: `${BASE_URL}${endpoint}`,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
if (response.data.error) {
|
||||
throw new Error(response.data.error);
|
||||
}
|
||||
|
||||
if (response.data) return response.data;
|
||||
|
||||
throw new Error(`Network request error. status : ${response.status}`);
|
||||
};
|
||||
|
||||
export default { fetch: api };
|
9
dashboard/src/core/fetcher.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { BareFetcher } from 'swr';
|
||||
import axios from 'axios';
|
||||
import { BASE_URL } from './api';
|
||||
|
||||
const fetcher: BareFetcher<any> = (url: string) => {
|
||||
return axios.get(url, { baseURL: BASE_URL }).then((res) => res.data);
|
||||
};
|
||||
|
||||
export default fetcher;
|
57
dashboard/src/core/types.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
export enum FieldTypes {
|
||||
text = 'text',
|
||||
password = 'password',
|
||||
email = 'email',
|
||||
number = 'number',
|
||||
fqdn = 'fqdn',
|
||||
ip = 'ip',
|
||||
fqdnip = 'fqdnip',
|
||||
}
|
||||
|
||||
interface FormField {
|
||||
type: FieldTypes;
|
||||
label: string;
|
||||
max?: number;
|
||||
min?: number;
|
||||
hint?: string;
|
||||
required?: boolean;
|
||||
env_variable: string;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
id: string;
|
||||
port: number;
|
||||
requirements?: {
|
||||
ports?: number[];
|
||||
};
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
image: string;
|
||||
form_fields: Record<string, FormField>;
|
||||
short_desc: string;
|
||||
author: string;
|
||||
source: string;
|
||||
installed: boolean;
|
||||
status: AppStatus;
|
||||
}
|
||||
|
||||
export enum RequestStatus {
|
||||
SUCCESS = 'SUCCESS',
|
||||
ERROR = 'ERROR',
|
||||
LOADING = 'LOADING',
|
||||
}
|
||||
|
||||
export enum AppStatus {
|
||||
RUNNING = 'running',
|
||||
STOPPED = 'stopped',
|
||||
INSTALLING = 'installing',
|
||||
UNINSTALLING = 'uninstalling',
|
||||
STOPPING = 'stopping',
|
||||
STARTING = 'starting',
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
70
dashboard/src/modules/Apps/components/AppActions.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { Button } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { FiExternalLink, FiPause, FiPlay, FiSettings, FiTrash2 } from 'react-icons/fi';
|
||||
import { AppConfig, AppStatus } from '../../../core/types';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
onInstall: () => void;
|
||||
onUninstall: () => void;
|
||||
onStart: () => void;
|
||||
onStop: () => void;
|
||||
onOpen: () => void;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate }) => {
|
||||
const hasSettings = Object.keys(app.form_fields).length > 0;
|
||||
|
||||
if (app?.installed && app.status === AppStatus.STOPPED) {
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center">
|
||||
<Button onClick={onStart} width={160} colorScheme="green" className="mt-3 mr-2">
|
||||
Start
|
||||
<FiPlay className="ml-1" />
|
||||
</Button>
|
||||
<Button onClick={onUninstall} width={160} colorScheme="gray" className="mt-3 mr-2">
|
||||
Remove
|
||||
<FiTrash2 className="ml-1" />
|
||||
</Button>
|
||||
{hasSettings && (
|
||||
<Button onClick={onUpdate} width={160} colorScheme="gray" className="mt-3 mr-2">
|
||||
Settings
|
||||
<FiSettings className="ml-1" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (app?.installed && app.status === AppStatus.RUNNING) {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={onOpen} width={160} colorScheme="gray" className="mt-3 mr-2">
|
||||
Open
|
||||
<FiExternalLink className="ml-1" />
|
||||
</Button>
|
||||
<Button onClick={onStop} width={160} colorScheme="red" className="mt-3">
|
||||
Stop
|
||||
<FiPause className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else if (app.status === AppStatus.INSTALLING || app.status === AppStatus.UNINSTALLING || app.status === AppStatus.STARTING || app.status === AppStatus.STOPPING) {
|
||||
return (
|
||||
<div className="flex items-center flex-col md:flex-row">
|
||||
<Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">
|
||||
Install
|
||||
<FiPlay className="ml-1" />
|
||||
</Button>
|
||||
<span className="text-gray-500 text-sm ml-2 mt-3">{`App is ${app.status} please wait and don't refresh page...`}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={onInstall} width={160} colorScheme="green" className="mt-3">
|
||||
Install
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppActions;
|