diff --git a/package/secubox/luci-app-gitea/Makefile b/package/secubox/luci-app-gitea/Makefile new file mode 100644 index 00000000..ece89ac0 --- /dev/null +++ b/package/secubox/luci-app-gitea/Makefile @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2025 CyberMind.fr +# +# LuCI Gitea Dashboard + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-gitea +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_ARCH:=all + +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=CyberMind + +LUCI_TITLE:=LuCI Gitea Dashboard +LUCI_DESCRIPTION:=Modern dashboard for Gitea Platform management on OpenWrt +LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +rpcd +rpcd-mod-luci +secubox-app-gitea + +LUCI_PKGARCH:=all + +PKG_FILE_MODES:=/usr/libexec/rpcd/luci.gitea:root:root:755 + +include $(TOPDIR)/feeds/luci/luci.mk + +# Note: /etc/config/gitea is in secubox-app-gitea + +$(eval $(call BuildPackage,luci-app-gitea)) diff --git a/package/secubox/luci-app-gitea/htdocs/luci-static/resources/gitea/api.js b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/gitea/api.js new file mode 100644 index 00000000..74123517 --- /dev/null +++ b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/gitea/api.js @@ -0,0 +1,244 @@ +'use strict'; +'require rpc'; +'require baseclass'; + +/** + * Gitea Platform API Module + * RPCD interface for Gitea Platform + */ + +var callGetStatus = rpc.declare({ + object: 'luci.gitea', + method: 'get_status', + expect: { result: {} } +}); + +var callGetStats = rpc.declare({ + object: 'luci.gitea', + method: 'get_stats', + expect: { result: {} } +}); + +var callGetConfig = rpc.declare({ + object: 'luci.gitea', + method: 'get_config', + expect: { result: {} } +}); + +var callSaveConfig = rpc.declare({ + object: 'luci.gitea', + method: 'save_config', + params: ['http_port', 'ssh_port', 'http_host', 'data_path', 'memory_limit', 'enabled', 'app_name', 'domain', 'protocol', 'disable_registration', 'require_signin', 'landing_page'], + expect: { result: {} } +}); + +var callStart = rpc.declare({ + object: 'luci.gitea', + method: 'start', + expect: { result: {} } +}); + +var callStop = rpc.declare({ + object: 'luci.gitea', + method: 'stop', + expect: { result: {} } +}); + +var callRestart = rpc.declare({ + object: 'luci.gitea', + method: 'restart', + expect: { result: {} } +}); + +var callInstall = rpc.declare({ + object: 'luci.gitea', + method: 'install', + expect: { result: {} } +}); + +var callUninstall = rpc.declare({ + object: 'luci.gitea', + method: 'uninstall', + expect: { result: {} } +}); + +var callUpdate = rpc.declare({ + object: 'luci.gitea', + method: 'update', + expect: { result: {} } +}); + +var callGetLogs = rpc.declare({ + object: 'luci.gitea', + method: 'get_logs', + params: ['lines'], + expect: { result: {} } +}); + +var callListRepos = rpc.declare({ + object: 'luci.gitea', + method: 'list_repos', + expect: { result: {} } +}); + +var callGetRepo = rpc.declare({ + object: 'luci.gitea', + method: 'get_repo', + params: ['name', 'owner'], + expect: { result: {} } +}); + +var callListUsers = rpc.declare({ + object: 'luci.gitea', + method: 'list_users', + expect: { result: {} } +}); + +var callCreateAdmin = rpc.declare({ + object: 'luci.gitea', + method: 'create_admin', + params: ['username', 'password', 'email'], + expect: { result: {} } +}); + +var callCreateBackup = rpc.declare({ + object: 'luci.gitea', + method: 'create_backup', + expect: { result: {} } +}); + +var callListBackups = rpc.declare({ + object: 'luci.gitea', + method: 'list_backups', + expect: { result: {} } +}); + +var callRestoreBackup = rpc.declare({ + object: 'luci.gitea', + method: 'restore_backup', + params: ['file'], + expect: { result: {} } +}); + +var callGetInstallProgress = rpc.declare({ + object: 'luci.gitea', + method: 'get_install_progress', + expect: { result: {} } +}); + +return baseclass.extend({ + getStatus: function() { + return callGetStatus(); + }, + + getStats: function() { + return callGetStats(); + }, + + getConfig: function() { + return callGetConfig(); + }, + + saveConfig: function(config) { + return callSaveConfig( + config.http_port, + config.ssh_port, + config.http_host, + config.data_path, + config.memory_limit, + config.enabled, + config.app_name, + config.domain, + config.protocol, + config.disable_registration, + config.require_signin, + config.landing_page + ); + }, + + start: function() { + return callStart(); + }, + + stop: function() { + return callStop(); + }, + + restart: function() { + return callRestart(); + }, + + install: function() { + return callInstall(); + }, + + uninstall: function() { + return callUninstall(); + }, + + update: function() { + return callUpdate(); + }, + + getLogs: function(lines) { + return callGetLogs(lines || 100).then(function(res) { + return res.logs || []; + }); + }, + + listRepos: function() { + return callListRepos().then(function(res) { + return { + repos: res.repos || [], + repo_root: res.repo_root || '/srv/gitea/git/repositories' + }; + }); + }, + + getRepo: function(name, owner) { + return callGetRepo(name, owner); + }, + + listUsers: function() { + return callListUsers().then(function(res) { + return res.users || []; + }); + }, + + createAdmin: function(username, password, email) { + return callCreateAdmin(username, password, email); + }, + + createBackup: function() { + return callCreateBackup(); + }, + + listBackups: function() { + return callListBackups().then(function(res) { + return res.backups || []; + }); + }, + + restoreBackup: function(file) { + return callRestoreBackup(file); + }, + + getInstallProgress: function() { + return callGetInstallProgress(); + }, + + getDashboardData: function() { + var self = this; + return Promise.all([ + self.getStatus(), + self.getStats(), + self.getLogs(50) + ]).then(function(results) { + return { + status: results[0] || {}, + stats: results[1] || {}, + logs: results[2] || [] + }; + }); + } +}); diff --git a/package/secubox/luci-app-gitea/htdocs/luci-static/resources/gitea/dashboard.css b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/gitea/dashboard.css new file mode 100644 index 00000000..b238d0a9 --- /dev/null +++ b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/gitea/dashboard.css @@ -0,0 +1,469 @@ +/* Gitea Dashboard - Cyberpunk Theme */ + +.gitea-dashboard { + font-family: 'Courier New', monospace; + background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #0a0a0a 100%); + min-height: 100vh; + padding: 20px; + color: #e0e0e0; +} + +/* Header */ +.gt-header { + background: linear-gradient(90deg, rgba(255, 95, 31, 0.1) 0%, rgba(255, 95, 31, 0.05) 100%); + border: 1px solid #ff5f1f; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 0 20px rgba(255, 95, 31, 0.2); +} + +.gt-header-content { + display: flex; + align-items: center; + gap: 20px; +} + +.gt-logo { + font-size: 48px; + text-shadow: 0 0 10px #ff5f1f; +} + +.gt-title { + font-size: 24px; + font-weight: bold; + color: #ff5f1f; + text-transform: uppercase; + letter-spacing: 2px; + margin: 0; + text-shadow: 0 0 10px rgba(255, 95, 31, 0.5); +} + +.gt-subtitle { + color: #888; + margin: 5px 0 0 0; + font-size: 12px; +} + +/* Status Badge */ +.gt-status-badge { + margin-left: auto; + padding: 8px 16px; + border-radius: 20px; + font-size: 12px; + font-weight: bold; + text-transform: uppercase; +} + +.gt-status-badge.running { + background: rgba(0, 255, 136, 0.2); + border: 1px solid #00ff88; + color: #00ff88; + box-shadow: 0 0 10px rgba(0, 255, 136, 0.3); +} + +.gt-status-badge.stopped { + background: rgba(255, 68, 68, 0.2); + border: 1px solid #ff4444; + color: #ff4444; +} + +.gt-status-badge.not-installed { + background: rgba(255, 170, 0, 0.2); + border: 1px solid #ffaa00; + color: #ffaa00; +} + +/* Stats Grid */ +.gt-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 15px; + margin-bottom: 20px; +} + +.gt-stat-card { + background: rgba(20, 20, 30, 0.8); + border: 1px solid #333; + border-radius: 8px; + padding: 15px; + display: flex; + align-items: center; + gap: 12px; + transition: all 0.3s ease; +} + +.gt-stat-card:hover { + border-color: #ff5f1f; + box-shadow: 0 0 15px rgba(255, 95, 31, 0.2); +} + +.gt-stat-card.success { + border-color: #00ff88; +} + +.gt-stat-card.error { + border-color: #ff4444; +} + +.gt-stat-icon { + font-size: 28px; + opacity: 0.8; +} + +.gt-stat-content { + flex: 1; +} + +.gt-stat-value { + font-size: 20px; + font-weight: bold; + color: #fff; +} + +.gt-stat-label { + font-size: 11px; + color: #888; + text-transform: uppercase; +} + +/* Main Grid */ +.gt-main-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 20px; +} + +/* Cards */ +.gt-card { + background: rgba(20, 20, 30, 0.9); + border: 1px solid #333; + border-radius: 8px; + overflow: hidden; +} + +.gt-card-full { + grid-column: 1 / -1; +} + +.gt-card-header { + background: rgba(255, 95, 31, 0.1); + border-bottom: 1px solid #333; + padding: 12px 16px; +} + +.gt-card-title { + font-size: 14px; + font-weight: bold; + color: #ff5f1f; + text-transform: uppercase; + display: flex; + align-items: center; + gap: 8px; +} + +.gt-card-body { + padding: 16px; +} + +/* Buttons */ +.gt-btn-group { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.gt-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 18px; + border: 1px solid; + border-radius: 4px; + font-family: inherit; + font-size: 12px; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; + transition: all 0.3s ease; + background: transparent; +} + +.gt-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.gt-btn-primary { + border-color: #ff5f1f; + color: #ff5f1f; +} + +.gt-btn-primary:hover:not(:disabled) { + background: rgba(255, 95, 31, 0.2); + box-shadow: 0 0 15px rgba(255, 95, 31, 0.3); +} + +.gt-btn-success { + border-color: #00ff88; + color: #00ff88; +} + +.gt-btn-success:hover:not(:disabled) { + background: rgba(0, 255, 136, 0.2); + box-shadow: 0 0 15px rgba(0, 255, 136, 0.3); +} + +.gt-btn-danger { + border-color: #ff4444; + color: #ff4444; +} + +.gt-btn-danger:hover:not(:disabled) { + background: rgba(255, 68, 68, 0.2); + box-shadow: 0 0 15px rgba(255, 68, 68, 0.3); +} + +.gt-btn-warning { + border-color: #ffaa00; + color: #ffaa00; +} + +.gt-btn-warning:hover:not(:disabled) { + background: rgba(255, 170, 0, 0.2); + box-shadow: 0 0 15px rgba(255, 170, 0, 0.3); +} + +/* Info List */ +.gt-info-list { + list-style: none; + margin: 0; + padding: 0; +} + +.gt-info-list li { + display: flex; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px solid #222; +} + +.gt-info-list li:last-child { + border-bottom: none; +} + +.gt-info-label { + color: #888; +} + +.gt-info-value { + color: #fff; + font-weight: bold; +} + +.gt-info-value a { + color: #ff5f1f; + text-decoration: none; +} + +.gt-info-value a:hover { + text-decoration: underline; +} + +/* Logs */ +.gt-logs { + background: #0a0a0a; + border: 1px solid #222; + border-radius: 4px; + padding: 12px; + max-height: 300px; + overflow-y: auto; + font-size: 11px; + line-height: 1.6; +} + +.gt-logs-line { + color: #00ff88; + word-break: break-all; +} + +.gt-logs-line:nth-child(odd) { + color: #0cc; +} + +/* Progress */ +.gt-progress { + height: 8px; + background: #222; + border-radius: 4px; + overflow: hidden; + margin-top: 15px; +} + +.gt-progress-bar { + height: 100%; + background: linear-gradient(90deg, #ff5f1f, #ffaa00); + border-radius: 4px; + transition: width 0.5s ease; +} + +.gt-progress-text { + margin-top: 8px; + font-size: 12px; + color: #888; + text-align: center; +} + +/* Empty State */ +.gt-empty { + text-align: center; + padding: 40px 20px; + color: #666; +} + +.gt-empty-icon { + font-size: 48px; + margin-bottom: 10px; + opacity: 0.5; +} + +/* Table */ +.gt-table { + width: 100%; + border-collapse: collapse; +} + +.gt-table th, +.gt-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #222; +} + +.gt-table th { + background: rgba(255, 95, 31, 0.1); + color: #ff5f1f; + font-size: 11px; + text-transform: uppercase; +} + +.gt-table tr:hover { + background: rgba(255, 95, 31, 0.05); +} + +.gt-table .gt-repo-name { + color: #ff5f1f; + font-weight: bold; +} + +.gt-table .gt-clone-url { + font-family: 'Courier New', monospace; + font-size: 11px; + color: #888; + cursor: pointer; +} + +.gt-table .gt-clone-url:hover { + color: #fff; +} + +/* Form */ +.gt-form-group { + margin-bottom: 16px; +} + +.gt-form-label { + display: block; + margin-bottom: 6px; + color: #888; + font-size: 12px; + text-transform: uppercase; +} + +.gt-form-input, +.gt-form-select { + width: 100%; + padding: 10px 12px; + background: #0a0a0a; + border: 1px solid #333; + border-radius: 4px; + color: #fff; + font-family: inherit; + font-size: 14px; +} + +.gt-form-input:focus, +.gt-form-select:focus { + border-color: #ff5f1f; + outline: none; + box-shadow: 0 0 10px rgba(255, 95, 31, 0.2); +} + +.gt-form-checkbox { + display: flex; + align-items: center; + gap: 10px; +} + +.gt-form-checkbox input { + width: 18px; + height: 18px; + accent-color: #ff5f1f; +} + +.gt-form-hint { + font-size: 11px; + color: #666; + margin-top: 4px; +} + +/* Badges */ +.gt-badge { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 10px; + font-weight: bold; + text-transform: uppercase; +} + +.gt-badge-admin { + background: rgba(255, 95, 31, 0.2); + border: 1px solid #ff5f1f; + color: #ff5f1f; +} + +.gt-badge-user { + background: rgba(0, 204, 204, 0.2); + border: 1px solid #0cc; + color: #0cc; +} + +/* Quick Actions */ +.gt-quick-actions { + display: flex; + gap: 10px; + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #222; +} + +/* Responsive */ +@media (max-width: 768px) { + .gt-header-content { + flex-direction: column; + text-align: center; + } + + .gt-status-badge { + margin-left: 0; + } + + .gt-stats-grid { + grid-template-columns: 1fr 1fr; + } + + .gt-main-grid { + grid-template-columns: 1fr; + } +} diff --git a/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/overview.js b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/overview.js new file mode 100644 index 00000000..79f5b3ba --- /dev/null +++ b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/overview.js @@ -0,0 +1,488 @@ +'use strict'; +'require view'; +'require ui'; +'require dom'; +'require poll'; +'require gitea.api as api'; + +return view.extend({ + statusData: null, + statsData: null, + logsData: null, + + load: function() { + return this.refreshData(); + }, + + refreshData: function() { + var self = this; + return api.getDashboardData().then(function(data) { + self.statusData = data.status || {}; + self.statsData = data.stats || {}; + self.logsData = data.logs || []; + return data; + }); + }, + + render: function() { + var self = this; + + // Inject CSS + var cssLink = E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('gitea/dashboard.css') + }); + + var container = E('div', { 'class': 'gitea-dashboard' }, [ + cssLink, + this.renderHeader(), + this.renderStatsGrid(), + this.renderMainGrid() + ]); + + // Poll for updates + poll.add(function() { + return self.refreshData().then(function() { + self.updateDynamicContent(); + }); + }, 10); + + return container; + }, + + renderHeader: function() { + var status = this.statusData; + var statusClass = !status.installed ? 'not-installed' : (status.running ? 'running' : 'stopped'); + var statusText = !status.installed ? _('Not Installed') : (status.running ? _('Running') : _('Stopped')); + + return E('div', { 'class': 'gt-header' }, [ + E('div', { 'class': 'gt-header-content' }, [ + E('div', { 'class': 'gt-logo' }, '\uD83D\uDCE6'), + E('div', {}, [ + E('h1', { 'class': 'gt-title' }, _('GITEA PLATFORM')), + E('p', { 'class': 'gt-subtitle' }, _('Self-Hosted Git Service for SecuBox')) + ]), + E('div', { 'class': 'gt-status-badge ' + statusClass, 'id': 'gt-status-badge' }, [ + E('span', {}, statusClass === 'running' ? '\u25CF' : '\u25CB'), + ' ' + statusText + ]) + ]) + ]); + }, + + renderStatsGrid: function() { + var status = this.statusData; + var stats = this.statsData; + + var statItems = [ + { + icon: '\uD83D\uDD0C', + label: _('Status'), + value: status.running ? _('Online') : _('Offline'), + id: 'stat-status', + cardClass: status.running ? 'success' : 'error' + }, + { + icon: '\uD83D\uDCE6', + label: _('Repositories'), + value: stats.repo_count || status.repo_count || 0, + id: 'stat-repos' + }, + { + icon: '\uD83D\uDC65', + label: _('Users'), + value: stats.user_count || 0, + id: 'stat-users' + }, + { + icon: '\uD83D\uDCBE', + label: _('Disk Usage'), + value: stats.disk_usage || status.disk_usage || '0', + id: 'stat-disk' + }, + { + icon: '\uD83C\uDF10', + label: _('HTTP Port'), + value: status.http_port || '3000', + id: 'stat-http' + }, + { + icon: '\uD83D\uDD12', + label: _('SSH Port'), + value: status.ssh_port || '2222', + id: 'stat-ssh' + } + ]; + + return E('div', { 'class': 'gt-stats-grid' }, + statItems.map(function(stat) { + return E('div', { 'class': 'gt-stat-card ' + (stat.cardClass || '') }, [ + E('div', { 'class': 'gt-stat-icon' }, stat.icon), + E('div', { 'class': 'gt-stat-content' }, [ + E('div', { 'class': 'gt-stat-value', 'id': stat.id }, String(stat.value)), + E('div', { 'class': 'gt-stat-label' }, stat.label) + ]) + ]); + }) + ); + }, + + renderMainGrid: function() { + return E('div', { 'class': 'gt-main-grid' }, [ + this.renderControlCard(), + this.renderInfoCard(), + this.renderLogsCard() + ]); + }, + + renderControlCard: function() { + var self = this; + var status = this.statusData; + + var buttons = []; + + if (!status.installed) { + buttons.push( + E('button', { + 'class': 'gt-btn gt-btn-primary', + 'id': 'btn-install', + 'click': function() { self.handleInstall(); } + }, [E('span', {}, '\uD83D\uDCE5'), ' ' + _('Install')]) + ); + } else { + if (status.running) { + buttons.push( + E('button', { + 'class': 'gt-btn gt-btn-danger', + 'id': 'btn-stop', + 'click': function() { self.handleStop(); } + }, [E('span', {}, '\u23F9'), ' ' + _('Stop')]) + ); + buttons.push( + E('button', { + 'class': 'gt-btn gt-btn-warning', + 'id': 'btn-restart', + 'click': function() { self.handleRestart(); } + }, [E('span', {}, '\uD83D\uDD04'), ' ' + _('Restart')]) + ); + } else { + buttons.push( + E('button', { + 'class': 'gt-btn gt-btn-success', + 'id': 'btn-start', + 'click': function() { self.handleStart(); } + }, [E('span', {}, '\u25B6'), ' ' + _('Start')]) + ); + } + + buttons.push( + E('button', { + 'class': 'gt-btn gt-btn-primary', + 'id': 'btn-update', + 'click': function() { self.handleUpdate(); } + }, [E('span', {}, '\u2B06'), ' ' + _('Update')]) + ); + + buttons.push( + E('button', { + 'class': 'gt-btn gt-btn-primary', + 'id': 'btn-backup', + 'click': function() { self.handleBackup(); } + }, [E('span', {}, '\uD83D\uDCBE'), ' ' + _('Backup')]) + ); + + buttons.push( + E('button', { + 'class': 'gt-btn gt-btn-danger', + 'id': 'btn-uninstall', + 'click': function() { self.handleUninstall(); } + }, [E('span', {}, '\uD83D\uDDD1'), ' ' + _('Uninstall')]) + ); + } + + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\uD83C\uDFAE'), + ' ' + _('Controls') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-btn-group', 'id': 'gt-controls' }, buttons), + E('div', { 'class': 'gt-progress', 'id': 'gt-progress-container', 'style': 'display:none' }, [ + E('div', { 'class': 'gt-progress-bar', 'id': 'gt-progress-bar', 'style': 'width:0%' }) + ]), + E('div', { 'class': 'gt-progress-text', 'id': 'gt-progress-text', 'style': 'display:none' }) + ]) + ]); + }, + + renderInfoCard: function() { + var status = this.statusData; + + var infoItems = [ + { label: _('Container'), value: status.container_name || 'gitea' }, + { label: _('Version'), value: status.version || '1.22.x' }, + { label: _('Data Path'), value: status.data_path || '/srv/gitea' }, + { label: _('Memory Limit'), value: status.memory_limit || '512M' }, + { label: _('Web Interface'), value: status.http_url, isLink: true }, + { label: _('SSH Clone'), value: status.ssh_url } + ]; + + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\u2139\uFE0F'), + ' ' + _('Information') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + E('ul', { 'class': 'gt-info-list', 'id': 'gt-info-list' }, + infoItems.map(function(item) { + var valueEl; + if (item.isLink && item.value) { + valueEl = E('a', { 'href': item.value, 'target': '_blank' }, item.value); + } else { + valueEl = item.value || '-'; + } + return E('li', {}, [ + E('span', { 'class': 'gt-info-label' }, item.label), + E('span', { 'class': 'gt-info-value' }, valueEl) + ]); + }) + ) + ]) + ]); + }, + + renderLogsCard: function() { + var logs = this.logsData || []; + + return E('div', { 'class': 'gt-card gt-card-full' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\uD83D\uDCDC'), + ' ' + _('Recent Logs') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + logs.length > 0 ? + E('div', { 'class': 'gt-logs', 'id': 'gt-logs' }, + logs.slice(-20).map(function(line) { + return E('div', { 'class': 'gt-logs-line' }, line); + }) + ) : + E('div', { 'class': 'gt-empty' }, [ + E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDCED'), + E('div', {}, _('No logs available')) + ]) + ]) + ]); + }, + + updateDynamicContent: function() { + var status = this.statusData; + var stats = this.statsData; + + // Update status badge + var badge = document.getElementById('gt-status-badge'); + if (badge) { + var statusClass = !status.installed ? 'not-installed' : (status.running ? 'running' : 'stopped'); + var statusText = !status.installed ? _('Not Installed') : (status.running ? _('Running') : _('Stopped')); + badge.className = 'gt-status-badge ' + statusClass; + badge.innerHTML = ''; + badge.appendChild(E('span', {}, statusClass === 'running' ? '\u25CF' : '\u25CB')); + badge.appendChild(document.createTextNode(' ' + statusText)); + } + + // Update stats + var statStatus = document.getElementById('stat-status'); + if (statStatus) { + statStatus.textContent = status.running ? _('Online') : _('Offline'); + } + + var statRepos = document.getElementById('stat-repos'); + if (statRepos) { + statRepos.textContent = stats.repo_count || status.repo_count || 0; + } + + var statUsers = document.getElementById('stat-users'); + if (statUsers) { + statUsers.textContent = stats.user_count || 0; + } + + // Update logs + var logsContainer = document.getElementById('gt-logs'); + if (logsContainer && this.logsData) { + logsContainer.innerHTML = ''; + this.logsData.slice(-20).forEach(function(line) { + logsContainer.appendChild(E('div', { 'class': 'gt-logs-line' }, line)); + }); + } + }, + + handleInstall: function() { + var self = this; + var btn = document.getElementById('btn-install'); + if (btn) btn.disabled = true; + + ui.showModal(_('Installing Gitea Platform'), [ + E('p', {}, _('This will download Alpine Linux rootfs and the Gitea binary. This may take several minutes.')), + E('div', { 'class': 'gt-progress' }, [ + E('div', { 'class': 'gt-progress-bar', 'id': 'modal-progress', 'style': 'width:0%' }) + ]), + E('p', { 'id': 'modal-status' }, _('Starting installation...')) + ]); + + api.install().then(function(result) { + if (result && result.started) { + self.pollInstallProgress(); + } else { + ui.hideModal(); + ui.addNotification(null, E('p', {}, result.message || _('Installation failed')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, _('Installation failed: ') + err.message), 'error'); + }); + }, + + pollInstallProgress: function() { + var self = this; + + var checkProgress = function() { + api.getInstallProgress().then(function(result) { + var progressBar = document.getElementById('modal-progress'); + var statusText = document.getElementById('modal-status'); + + if (progressBar) { + progressBar.style.width = (result.progress || 0) + '%'; + } + if (statusText) { + statusText.textContent = result.message || ''; + } + + if (result.status === 'completed') { + ui.hideModal(); + ui.addNotification(null, E('p', {}, _('Gitea Platform installed successfully!')), 'success'); + self.refreshData(); + location.reload(); + } else if (result.status === 'error') { + ui.hideModal(); + ui.addNotification(null, E('p', {}, _('Installation failed: ') + result.message), 'error'); + } else if (result.status === 'running') { + setTimeout(checkProgress, 3000); + } else { + setTimeout(checkProgress, 3000); + } + }).catch(function() { + setTimeout(checkProgress, 5000); + }); + }; + + setTimeout(checkProgress, 2000); + }, + + handleStart: function() { + var self = this; + api.start().then(function(result) { + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Gitea Platform started')), 'success'); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Failed to start')), 'error'); + } + self.refreshData(); + }); + }, + + handleStop: function() { + var self = this; + api.stop().then(function(result) { + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Gitea Platform stopped')), 'info'); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Failed to stop')), 'error'); + } + self.refreshData(); + }); + }, + + handleRestart: function() { + var self = this; + api.restart().then(function(result) { + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Gitea Platform restarted')), 'success'); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Failed to restart')), 'error'); + } + self.refreshData(); + }); + }, + + handleUpdate: function() { + var self = this; + + ui.showModal(_('Updating Gitea'), [ + E('p', {}, _('Downloading and installing the latest Gitea binary...')), + E('div', { 'class': 'spinning' }) + ]); + + api.update().then(function(result) { + ui.hideModal(); + if (result && result.started) { + ui.addNotification(null, E('p', {}, _('Update started. The server will restart automatically.')), 'info'); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Update failed')), 'error'); + } + self.refreshData(); + }); + }, + + handleBackup: function() { + var self = this; + + ui.showModal(_('Creating Backup'), [ + E('p', {}, _('Backing up repositories and database...')), + E('div', { 'class': 'spinning' }) + ]); + + api.createBackup().then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Backup created: ') + (result.file || '')), 'success'); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Backup failed')), 'error'); + } + }); + }, + + handleUninstall: function() { + var self = this; + + ui.showModal(_('Confirm Uninstall'), [ + E('p', {}, _('Are you sure you want to uninstall Gitea Platform? Your repositories will be preserved.')), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + E('button', { + 'class': 'btn cbi-button-negative', + 'click': function() { + ui.hideModal(); + api.uninstall().then(function(result) { + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Gitea Platform uninstalled')), 'info'); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Uninstall failed')), 'error'); + } + self.refreshData(); + location.reload(); + }); + } + }, _('Uninstall')) + ]) + ]); + } +}); diff --git a/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/repos.js b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/repos.js new file mode 100644 index 00000000..3fd26de6 --- /dev/null +++ b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/repos.js @@ -0,0 +1,212 @@ +'use strict'; +'require view'; +'require ui'; +'require dom'; +'require poll'; +'require gitea.api as api'; + +return view.extend({ + reposData: null, + statusData: null, + + load: function() { + var self = this; + return Promise.all([ + api.getStatus(), + api.listRepos() + ]).then(function(results) { + self.statusData = results[0] || {}; + self.reposData = results[1] || {}; + return results; + }); + }, + + render: function() { + var self = this; + + // Inject CSS + var cssLink = E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('gitea/dashboard.css') + }); + + var container = E('div', { 'class': 'gitea-dashboard' }, [ + cssLink, + this.renderHeader(), + this.renderContent() + ]); + + // Poll for updates + poll.add(function() { + return api.listRepos().then(function(data) { + self.reposData = data; + self.updateRepoList(); + }); + }, 30); + + return container; + }, + + renderHeader: function() { + var repos = this.reposData.repos || []; + + return E('div', { 'class': 'gt-header' }, [ + E('div', { 'class': 'gt-header-content' }, [ + E('div', { 'class': 'gt-logo' }, '\uD83D\uDCE6'), + E('div', {}, [ + E('h1', { 'class': 'gt-title' }, _('REPOSITORIES')), + E('p', { 'class': 'gt-subtitle' }, _('Git Repository Browser')) + ]), + E('div', { 'class': 'gt-status-badge running' }, [ + E('span', {}, '\uD83D\uDCE6'), + ' ' + repos.length + ' ' + _('Repositories') + ]) + ]) + ]); + }, + + renderContent: function() { + var repos = this.reposData.repos || []; + var status = this.statusData; + + if (!status.installed) { + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-empty' }, [ + E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDCE6'), + E('div', {}, _('Gitea is not installed')), + E('p', {}, _('Install Gitea from the Overview page to manage repositories.')) + ]) + ]) + ]); + } + + if (repos.length === 0) { + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-empty' }, [ + E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDCED'), + E('div', {}, _('No repositories found')), + E('p', {}, _('Create your first repository through the Gitea web interface.')) + ]) + ]) + ]); + } + + return E('div', { 'class': 'gt-card gt-card-full' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\uD83D\uDCC2'), + ' ' + _('Repository List') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + this.renderRepoTable(repos) + ]) + ]); + }, + + renderRepoTable: function(repos) { + var self = this; + var status = this.statusData; + var lanIp = status.http_url ? status.http_url.replace(/^https?:\/\//, '').split(':')[0] : 'localhost'; + var httpPort = status.http_port || 3000; + var sshPort = status.ssh_port || 2222; + + return E('table', { 'class': 'gt-table', 'id': 'repo-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, _('Repository')), + E('th', {}, _('Owner')), + E('th', {}, _('Size')), + E('th', {}, _('Clone URL')) + ]) + ]), + E('tbody', {}, + repos.map(function(repo) { + var httpClone = 'http://' + lanIp + ':' + httpPort + '/' + repo.owner + '/' + repo.name + '.git'; + var sshClone = 'git@' + lanIp + ':' + sshPort + '/' + repo.owner + '/' + repo.name + '.git'; + + return E('tr', {}, [ + E('td', { 'class': 'gt-repo-name' }, repo.name), + E('td', {}, repo.owner || '-'), + E('td', {}, repo.size || '-'), + E('td', {}, [ + E('div', { + 'class': 'gt-clone-url', + 'title': _('Click to copy'), + 'click': function() { self.copyToClipboard(httpClone); } + }, httpClone), + E('div', { + 'class': 'gt-clone-url', + 'title': _('Click to copy SSH URL'), + 'click': function() { self.copyToClipboard(sshClone); }, + 'style': 'margin-top: 4px; font-size: 10px;' + }, sshClone) + ]) + ]); + }) + ) + ]); + }, + + updateRepoList: function() { + var table = document.getElementById('repo-table'); + if (!table) return; + + var repos = this.reposData.repos || []; + var tbody = table.querySelector('tbody'); + if (!tbody) return; + + // Update table content + var self = this; + var status = this.statusData; + var lanIp = status.http_url ? status.http_url.replace(/^https?:\/\//, '').split(':')[0] : 'localhost'; + var httpPort = status.http_port || 3000; + var sshPort = status.ssh_port || 2222; + + tbody.innerHTML = ''; + repos.forEach(function(repo) { + var httpClone = 'http://' + lanIp + ':' + httpPort + '/' + repo.owner + '/' + repo.name + '.git'; + var sshClone = 'git@' + lanIp + ':' + sshPort + '/' + repo.owner + '/' + repo.name + '.git'; + + var row = E('tr', {}, [ + E('td', { 'class': 'gt-repo-name' }, repo.name), + E('td', {}, repo.owner || '-'), + E('td', {}, repo.size || '-'), + E('td', {}, [ + E('div', { + 'class': 'gt-clone-url', + 'title': _('Click to copy'), + 'click': function() { self.copyToClipboard(httpClone); } + }, httpClone), + E('div', { + 'class': 'gt-clone-url', + 'title': _('Click to copy SSH URL'), + 'click': function() { self.copyToClipboard(sshClone); }, + 'style': 'margin-top: 4px; font-size: 10px;' + }, sshClone) + ]) + ]); + tbody.appendChild(row); + }); + }, + + copyToClipboard: function(text) { + if (navigator.clipboard) { + navigator.clipboard.writeText(text).then(function() { + ui.addNotification(null, E('p', {}, _('Copied to clipboard: ') + text), 'info'); + }); + } else { + // Fallback + var input = document.createElement('input'); + input.value = text; + document.body.appendChild(input); + input.select(); + document.execCommand('copy'); + document.body.removeChild(input); + ui.addNotification(null, E('p', {}, _('Copied to clipboard: ') + text), 'info'); + } + } +}); diff --git a/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/settings.js b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/settings.js new file mode 100644 index 00000000..405e2cdd --- /dev/null +++ b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/settings.js @@ -0,0 +1,351 @@ +'use strict'; +'require view'; +'require ui'; +'require dom'; +'require gitea.api as api'; + +return view.extend({ + configData: null, + statusData: null, + backupsData: null, + + load: function() { + var self = this; + return Promise.all([ + api.getStatus(), + api.getConfig(), + api.listBackups() + ]).then(function(results) { + self.statusData = results[0] || {}; + self.configData = results[1] || {}; + self.backupsData = results[2] || []; + return results; + }); + }, + + render: function() { + var self = this; + + // Inject CSS + var cssLink = E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('gitea/dashboard.css') + }); + + var container = E('div', { 'class': 'gitea-dashboard' }, [ + cssLink, + this.renderHeader(), + this.renderContent() + ]); + + return container; + }, + + renderHeader: function() { + return E('div', { 'class': 'gt-header' }, [ + E('div', { 'class': 'gt-header-content' }, [ + E('div', { 'class': 'gt-logo' }, '\u2699\uFE0F'), + E('div', {}, [ + E('h1', { 'class': 'gt-title' }, _('SETTINGS')), + E('p', { 'class': 'gt-subtitle' }, _('Gitea Platform Configuration')) + ]) + ]) + ]); + }, + + renderContent: function() { + var status = this.statusData; + + if (!status.installed) { + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-empty' }, [ + E('div', { 'class': 'gt-empty-icon' }, '\u2699\uFE0F'), + E('div', {}, _('Gitea is not installed')), + E('p', {}, _('Install Gitea from the Overview page to configure settings.')) + ]) + ]) + ]); + } + + return E('div', { 'class': 'gt-main-grid' }, [ + this.renderServerSettings(), + this.renderSecuritySettings(), + this.renderBackupCard() + ]); + }, + + renderServerSettings: function() { + var self = this; + var config = this.configData; + var main = config.main || {}; + + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\uD83D\uDDA5\uFE0F'), + ' ' + _('Server Settings') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-label' }, _('App Name')), + E('input', { + 'type': 'text', + 'class': 'gt-form-input', + 'id': 'cfg-app-name', + 'value': main.app_name || 'SecuBox Git' + }), + E('div', { 'class': 'gt-form-hint' }, _('Display name for the Gitea instance')) + ]), + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-label' }, _('Domain')), + E('input', { + 'type': 'text', + 'class': 'gt-form-input', + 'id': 'cfg-domain', + 'value': main.domain || 'git.local' + }), + E('div', { 'class': 'gt-form-hint' }, _('Domain name for URLs')) + ]), + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-label' }, _('HTTP Port')), + E('input', { + 'type': 'number', + 'class': 'gt-form-input', + 'id': 'cfg-http-port', + 'value': main.http_port || 3000 + }) + ]), + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-label' }, _('SSH Port')), + E('input', { + 'type': 'number', + 'class': 'gt-form-input', + 'id': 'cfg-ssh-port', + 'value': main.ssh_port || 2222 + }) + ]), + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-label' }, _('Memory Limit')), + E('select', { 'class': 'gt-form-select', 'id': 'cfg-memory' }, [ + E('option', { 'value': '256M', 'selected': main.memory_limit === '256M' }, '256 MB'), + E('option', { 'value': '512M', 'selected': main.memory_limit === '512M' || !main.memory_limit }, '512 MB'), + E('option', { 'value': '1G', 'selected': main.memory_limit === '1G' }, '1 GB'), + E('option', { 'value': '2G', 'selected': main.memory_limit === '2G' }, '2 GB') + ]) + ]), + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-checkbox' }, [ + E('input', { + 'type': 'checkbox', + 'id': 'cfg-enabled', + 'checked': main.enabled + }), + _('Enable service on boot') + ]) + ]), + E('button', { + 'class': 'gt-btn gt-btn-primary', + 'click': function() { self.handleSaveConfig(); } + }, [E('span', {}, '\uD83D\uDCBE'), ' ' + _('Save Settings')]) + ]) + ]); + }, + + renderSecuritySettings: function() { + var self = this; + var config = this.configData; + var server = config.server || {}; + + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\uD83D\uDD12'), + ' ' + _('Security Settings') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-checkbox' }, [ + E('input', { + 'type': 'checkbox', + 'id': 'cfg-disable-reg', + 'checked': server.disable_registration + }), + _('Disable user registration') + ]), + E('div', { 'class': 'gt-form-hint' }, _('Prevent new users from signing up')) + ]), + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-checkbox' }, [ + E('input', { + 'type': 'checkbox', + 'id': 'cfg-require-signin', + 'checked': server.require_signin + }), + _('Require sign-in to view') + ]), + E('div', { 'class': 'gt-form-hint' }, _('Require authentication to browse repositories')) + ]), + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-label' }, _('Landing Page')), + E('select', { 'class': 'gt-form-select', 'id': 'cfg-landing' }, [ + E('option', { 'value': 'explore', 'selected': server.landing_page === 'explore' }, _('Explore')), + E('option', { 'value': 'home', 'selected': server.landing_page === 'home' }, _('Home')), + E('option', { 'value': 'organizations', 'selected': server.landing_page === 'organizations' }, _('Organizations')), + E('option', { 'value': 'login', 'selected': server.landing_page === 'login' }, _('Login')) + ]) + ]), + E('p', { 'class': 'gt-form-hint', 'style': 'margin-top: 15px; color: #ff5f1f;' }, + _('Note: Changes to security settings require a service restart to take effect.')) + ]) + ]); + }, + + renderBackupCard: function() { + var self = this; + var backups = this.backupsData || []; + + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\uD83D\uDCBE'), + ' ' + _('Backup & Restore') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-btn-group' }, [ + E('button', { + 'class': 'gt-btn gt-btn-primary', + 'click': function() { self.handleBackup(); } + }, [E('span', {}, '\uD83D\uDCBE'), ' ' + _('Create Backup')]) + ]), + backups.length > 0 ? + E('div', { 'style': 'margin-top: 20px' }, [ + E('h4', { 'style': 'color: #888; font-size: 12px; margin-bottom: 10px;' }, _('Available Backups')), + E('table', { 'class': 'gt-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, _('Filename')), + E('th', {}, _('Size')), + E('th', {}, _('Date')), + E('th', {}, _('Actions')) + ]) + ]), + E('tbody', {}, + backups.map(function(backup) { + var date = backup.mtime ? new Date(backup.mtime * 1000).toLocaleString() : '-'; + return E('tr', {}, [ + E('td', {}, backup.name), + E('td', {}, backup.size || '-'), + E('td', {}, date), + E('td', {}, [ + E('button', { + 'class': 'gt-btn gt-btn-warning', + 'style': 'padding: 4px 8px; font-size: 10px;', + 'click': function() { self.handleRestore(backup.path); } + }, _('Restore')) + ]) + ]); + }) + ) + ]) + ]) : + E('div', { 'class': 'gt-empty', 'style': 'padding: 20px' }, [ + E('div', {}, _('No backups found')) + ]) + ]) + ]); + }, + + handleSaveConfig: function() { + var self = this; + + var config = { + app_name: document.getElementById('cfg-app-name').value, + domain: document.getElementById('cfg-domain').value, + http_port: parseInt(document.getElementById('cfg-http-port').value) || 3000, + ssh_port: parseInt(document.getElementById('cfg-ssh-port').value) || 2222, + memory_limit: document.getElementById('cfg-memory').value, + enabled: document.getElementById('cfg-enabled').checked ? '1' : '0', + disable_registration: document.getElementById('cfg-disable-reg').checked ? 'true' : 'false', + require_signin: document.getElementById('cfg-require-signin').checked ? 'true' : 'false', + landing_page: document.getElementById('cfg-landing').value + }; + + api.saveConfig(config).then(function(result) { + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Configuration saved. Restart the service for changes to take effect.')), 'success'); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Failed to save configuration')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', {}, _('Failed to save configuration: ') + err.message), 'error'); + }); + }, + + handleBackup: function() { + var self = this; + + ui.showModal(_('Creating Backup'), [ + E('p', {}, _('Backing up repositories and database...')), + E('div', { 'class': 'spinning' }) + ]); + + api.createBackup().then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Backup created successfully')), 'success'); + // Refresh backup list + return api.listBackups().then(function(data) { + self.backupsData = data; + location.reload(); + }); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Backup failed')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, _('Backup failed: ') + err.message), 'error'); + }); + }, + + handleRestore: function(file) { + var self = this; + + ui.showModal(_('Confirm Restore'), [ + E('p', {}, _('Are you sure you want to restore from this backup? This will overwrite current data.')), + E('p', { 'style': 'color: #ffaa00' }, file), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + E('button', { + 'class': 'btn cbi-button-negative', + 'click': function() { + ui.hideModal(); + ui.showModal(_('Restoring Backup'), [ + E('p', {}, _('Restoring data...')), + E('div', { 'class': 'spinning' }) + ]); + + api.restoreBackup(file).then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Restore completed successfully')), 'success'); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Restore failed')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, _('Restore failed: ') + err.message), 'error'); + }); + } + }, _('Restore')) + ]) + ]); + } +}); diff --git a/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/users.js b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/users.js new file mode 100644 index 00000000..55bcbfd2 --- /dev/null +++ b/package/secubox/luci-app-gitea/htdocs/luci-static/resources/view/gitea/users.js @@ -0,0 +1,262 @@ +'use strict'; +'require view'; +'require ui'; +'require dom'; +'require poll'; +'require gitea.api as api'; + +return view.extend({ + usersData: null, + statusData: null, + + load: function() { + var self = this; + return Promise.all([ + api.getStatus(), + api.listUsers() + ]).then(function(results) { + self.statusData = results[0] || {}; + self.usersData = results[1] || []; + return results; + }); + }, + + render: function() { + var self = this; + + // Inject CSS + var cssLink = E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('gitea/dashboard.css') + }); + + var container = E('div', { 'class': 'gitea-dashboard' }, [ + cssLink, + this.renderHeader(), + this.renderContent() + ]); + + // Poll for updates + poll.add(function() { + return api.listUsers().then(function(data) { + self.usersData = data; + self.updateUserList(); + }); + }, 30); + + return container; + }, + + renderHeader: function() { + var self = this; + var users = this.usersData || []; + + return E('div', { 'class': 'gt-header' }, [ + E('div', { 'class': 'gt-header-content' }, [ + E('div', { 'class': 'gt-logo' }, '\uD83D\uDC65'), + E('div', {}, [ + E('h1', { 'class': 'gt-title' }, _('USER MANAGEMENT')), + E('p', { 'class': 'gt-subtitle' }, _('Gitea User Administration')) + ]), + E('div', { 'class': 'gt-status-badge running' }, [ + E('span', {}, '\uD83D\uDC65'), + ' ' + users.length + ' ' + _('Users') + ]) + ]) + ]); + }, + + renderContent: function() { + var self = this; + var status = this.statusData; + var users = this.usersData || []; + + if (!status.installed) { + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-empty' }, [ + E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDC65'), + E('div', {}, _('Gitea is not installed')), + E('p', {}, _('Install Gitea from the Overview page to manage users.')) + ]) + ]) + ]); + } + + if (!status.running) { + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-empty' }, [ + E('div', { 'class': 'gt-empty-icon' }, '\u26A0\uFE0F'), + E('div', {}, _('Gitea is not running')), + E('p', {}, _('Start Gitea to manage users.')) + ]) + ]) + ]); + } + + return E('div', { 'class': 'gt-main-grid' }, [ + this.renderCreateAdminCard(), + this.renderUserListCard(users) + ]); + }, + + renderCreateAdminCard: function() { + var self = this; + + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\uD83D\uDC64'), + ' ' + _('Create Admin User') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-label' }, _('Username')), + E('input', { + 'type': 'text', + 'class': 'gt-form-input', + 'id': 'new-username', + 'placeholder': 'admin' + }) + ]), + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-label' }, _('Password')), + E('input', { + 'type': 'password', + 'class': 'gt-form-input', + 'id': 'new-password', + 'placeholder': '********' + }) + ]), + E('div', { 'class': 'gt-form-group' }, [ + E('label', { 'class': 'gt-form-label' }, _('Email')), + E('input', { + 'type': 'email', + 'class': 'gt-form-input', + 'id': 'new-email', + 'placeholder': 'admin@localhost' + }) + ]), + E('button', { + 'class': 'gt-btn gt-btn-success', + 'click': function() { self.handleCreateAdmin(); } + }, [E('span', {}, '\u2795'), ' ' + _('Create Admin')]) + ]) + ]); + }, + + renderUserListCard: function(users) { + if (users.length === 0) { + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\uD83D\uDC65'), + ' ' + _('User List') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + E('div', { 'class': 'gt-empty' }, [ + E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDC64'), + E('div', {}, _('No users found')), + E('p', {}, _('Create your first admin user above, or through the Gitea web interface.')) + ]) + ]) + ]); + } + + return E('div', { 'class': 'gt-card' }, [ + E('div', { 'class': 'gt-card-header' }, [ + E('div', { 'class': 'gt-card-title' }, [ + E('span', {}, '\uD83D\uDC65'), + ' ' + _('User List') + ]) + ]), + E('div', { 'class': 'gt-card-body' }, [ + E('table', { 'class': 'gt-table', 'id': 'user-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, _('Username')), + E('th', {}, _('Email')), + E('th', {}, _('Role')), + E('th', {}, _('Created')) + ]) + ]), + E('tbody', {}, + users.map(function(user) { + var created = user.created ? new Date(user.created * 1000).toLocaleDateString() : '-'; + return E('tr', {}, [ + E('td', { 'class': 'gt-repo-name' }, user.name), + E('td', {}, user.email || '-'), + E('td', {}, [ + user.is_admin ? + E('span', { 'class': 'gt-badge gt-badge-admin' }, 'Admin') : + E('span', { 'class': 'gt-badge gt-badge-user' }, 'User') + ]), + E('td', {}, created) + ]); + }) + ) + ]) + ]) + ]); + }, + + updateUserList: function() { + var table = document.getElementById('user-table'); + if (!table) return; + + var users = this.usersData || []; + var tbody = table.querySelector('tbody'); + if (!tbody) return; + + tbody.innerHTML = ''; + users.forEach(function(user) { + var created = user.created ? new Date(user.created * 1000).toLocaleDateString() : '-'; + var row = E('tr', {}, [ + E('td', { 'class': 'gt-repo-name' }, user.name), + E('td', {}, user.email || '-'), + E('td', {}, [ + user.is_admin ? + E('span', { 'class': 'gt-badge gt-badge-admin' }, 'Admin') : + E('span', { 'class': 'gt-badge gt-badge-user' }, 'User') + ]), + E('td', {}, created) + ]); + tbody.appendChild(row); + }); + }, + + handleCreateAdmin: function() { + var self = this; + var username = document.getElementById('new-username').value; + var password = document.getElementById('new-password').value; + var email = document.getElementById('new-email').value; + + if (!username || !password || !email) { + ui.addNotification(null, E('p', {}, _('Please fill in all fields')), 'error'); + return; + } + + api.createAdmin(username, password, email).then(function(result) { + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Admin user created: ') + username), 'success'); + // Clear form + document.getElementById('new-username').value = ''; + document.getElementById('new-password').value = ''; + document.getElementById('new-email').value = ''; + // Refresh user list + return api.listUsers().then(function(data) { + self.usersData = data; + self.updateUserList(); + }); + } else { + ui.addNotification(null, E('p', {}, result.message || _('Failed to create user')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', {}, _('Failed to create user: ') + err.message), 'error'); + }); + } +}); diff --git a/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea b/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea new file mode 100644 index 00000000..0b88b701 --- /dev/null +++ b/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea @@ -0,0 +1,723 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# LuCI RPC backend for Gitea Platform +# Copyright (C) 2025 CyberMind.fr + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +CONFIG="gitea" +LXC_NAME="gitea" +LXC_PATH="/srv/lxc" +DATA_PATH="/srv/gitea" +GITEA_VERSION="1.22.6" + +# JSON helpers +json_init_obj() { json_init; json_add_object "result"; } +json_close_obj() { json_close_object; json_dump; } + +json_error() { + json_init + json_add_object "error" + json_add_string "message" "$1" + json_close_object + json_dump +} + +json_success() { + json_init_obj + json_add_boolean "success" 1 + [ -n "$1" ] && json_add_string "message" "$1" + json_close_obj +} + +# Check if container is running +lxc_running() { + lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" +} + +# Check if container exists +lxc_exists() { + [ -f "$LXC_PATH/$LXC_NAME/config" ] && [ -d "$LXC_PATH/$LXC_NAME/rootfs" ] +} + +# Get service status +get_status() { + local enabled running installed uptime + local http_port ssh_port data_path memory_limit app_name domain + + config_load "$CONFIG" + config_get enabled main enabled "0" + config_get http_port main http_port "3000" + config_get ssh_port main ssh_port "2222" + config_get data_path main data_path "/srv/gitea" + config_get memory_limit main memory_limit "512M" + config_get app_name main app_name "SecuBox Git" + config_get domain main domain "git.local" + + running="false" + installed="false" + uptime="" + + if lxc_exists; then + installed="true" + fi + + if lxc_running; then + running="true" + uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1 | awk '{print $3}') + fi + + # Count repositories + local repo_count=0 + DATA_PATH="$data_path" + if [ -d "$DATA_PATH/git/repositories" ]; then + repo_count=$(find "$DATA_PATH/git/repositories" -name "*.git" -type d 2>/dev/null | wc -l) + fi + + # Get disk usage + local disk_usage="0" + if [ -d "$DATA_PATH" ]; then + disk_usage=$(du -sh "$DATA_PATH" 2>/dev/null | awk '{print $1}' || echo "0") + fi + + # Get LAN IP for URL + local lan_ip + lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") + + json_init_obj + json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )" + json_add_boolean "running" "$( [ "$running" = "true" ] && echo 1 || echo 0 )" + json_add_boolean "installed" "$( [ "$installed" = "true" ] && echo 1 || echo 0 )" + json_add_string "uptime" "$uptime" + json_add_int "http_port" "$http_port" + json_add_int "ssh_port" "$ssh_port" + json_add_string "data_path" "$data_path" + json_add_string "memory_limit" "$memory_limit" + json_add_string "app_name" "$app_name" + json_add_string "domain" "$domain" + json_add_int "repo_count" "$repo_count" + json_add_string "disk_usage" "$disk_usage" + json_add_string "http_url" "http://${lan_ip}:${http_port}" + json_add_string "ssh_url" "ssh://git@${lan_ip}:${ssh_port}" + json_add_string "container_name" "$LXC_NAME" + json_add_string "version" "$GITEA_VERSION" + json_close_obj +} + +# Get statistics +get_stats() { + local data_path + config_load "$CONFIG" + config_get data_path main data_path "/srv/gitea" + + local repo_count=0 + local user_count=0 + local disk_usage="0" + + # Count repositories + if [ -d "$data_path/git/repositories" ]; then + repo_count=$(find "$data_path/git/repositories" -name "*.git" -type d 2>/dev/null | wc -l) + fi + + # Get disk usage + if [ -d "$data_path" ]; then + disk_usage=$(du -sh "$data_path" 2>/dev/null | awk '{print $1}' || echo "0") + fi + + # Count users (if we can query the database) + if [ -f "$data_path/gitea.db" ] && command -v sqlite3 >/dev/null 2>&1; then + user_count=$(sqlite3 "$data_path/gitea.db" "SELECT COUNT(*) FROM user" 2>/dev/null || echo "0") + fi + + json_init_obj + json_add_int "repo_count" "$repo_count" + json_add_int "user_count" "$user_count" + json_add_string "disk_usage" "$disk_usage" + json_close_obj +} + +# Get configuration +get_config() { + local http_port ssh_port http_host data_path memory_limit enabled app_name domain + local protocol disable_registration require_signin landing_page + + config_load "$CONFIG" + + # Main settings + config_get http_port main http_port "3000" + config_get ssh_port main ssh_port "2222" + config_get http_host main http_host "0.0.0.0" + config_get data_path main data_path "/srv/gitea" + config_get memory_limit main memory_limit "512M" + config_get enabled main enabled "0" + config_get app_name main app_name "SecuBox Git" + config_get domain main domain "git.local" + + # Server settings + config_get protocol server protocol "http" + config_get disable_registration server disable_registration "false" + config_get require_signin server require_signin "false" + config_get landing_page server landing_page "explore" + + json_init_obj + json_add_object "main" + json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )" + json_add_int "http_port" "$http_port" + json_add_int "ssh_port" "$ssh_port" + json_add_string "http_host" "$http_host" + json_add_string "data_path" "$data_path" + json_add_string "memory_limit" "$memory_limit" + json_add_string "app_name" "$app_name" + json_add_string "domain" "$domain" + json_close_object + + json_add_object "server" + json_add_string "protocol" "$protocol" + json_add_boolean "disable_registration" "$( [ "$disable_registration" = "true" ] && echo 1 || echo 0 )" + json_add_boolean "require_signin" "$( [ "$require_signin" = "true" ] && echo 1 || echo 0 )" + json_add_string "landing_page" "$landing_page" + json_close_object + + json_close_obj +} + +# Save configuration +save_config() { + read -r input + + local http_port ssh_port http_host data_path memory_limit enabled app_name domain + local protocol disable_registration require_signin landing_page + + http_port=$(echo "$input" | jsonfilter -e '@.http_port' 2>/dev/null) + ssh_port=$(echo "$input" | jsonfilter -e '@.ssh_port' 2>/dev/null) + http_host=$(echo "$input" | jsonfilter -e '@.http_host' 2>/dev/null) + data_path=$(echo "$input" | jsonfilter -e '@.data_path' 2>/dev/null) + memory_limit=$(echo "$input" | jsonfilter -e '@.memory_limit' 2>/dev/null) + enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null) + app_name=$(echo "$input" | jsonfilter -e '@.app_name' 2>/dev/null) + domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null) + protocol=$(echo "$input" | jsonfilter -e '@.protocol' 2>/dev/null) + disable_registration=$(echo "$input" | jsonfilter -e '@.disable_registration' 2>/dev/null) + require_signin=$(echo "$input" | jsonfilter -e '@.require_signin' 2>/dev/null) + landing_page=$(echo "$input" | jsonfilter -e '@.landing_page' 2>/dev/null) + + [ -n "$http_port" ] && uci set "${CONFIG}.main.http_port=$http_port" + [ -n "$ssh_port" ] && uci set "${CONFIG}.main.ssh_port=$ssh_port" + [ -n "$http_host" ] && uci set "${CONFIG}.main.http_host=$http_host" + [ -n "$data_path" ] && uci set "${CONFIG}.main.data_path=$data_path" + [ -n "$memory_limit" ] && uci set "${CONFIG}.main.memory_limit=$memory_limit" + [ -n "$enabled" ] && uci set "${CONFIG}.main.enabled=$enabled" + [ -n "$app_name" ] && uci set "${CONFIG}.main.app_name=$app_name" + [ -n "$domain" ] && uci set "${CONFIG}.main.domain=$domain" + [ -n "$protocol" ] && uci set "${CONFIG}.server.protocol=$protocol" + [ -n "$disable_registration" ] && uci set "${CONFIG}.server.disable_registration=$disable_registration" + [ -n "$require_signin" ] && uci set "${CONFIG}.server.require_signin=$require_signin" + [ -n "$landing_page" ] && uci set "${CONFIG}.server.landing_page=$landing_page" + + uci commit "$CONFIG" + + json_success "Configuration saved" +} + +# Start service +start_service() { + if lxc_running; then + json_error "Service is already running" + return + fi + + if ! lxc_exists; then + json_error "Container not installed. Run install first." + return + fi + + /etc/init.d/gitea start >/dev/null 2>&1 & + + sleep 3 + if lxc_running; then + json_success "Service started" + else + json_error "Failed to start service" + fi +} + +# Stop service +stop_service() { + if ! lxc_running; then + json_error "Service is not running" + return + fi + + /etc/init.d/gitea stop >/dev/null 2>&1 + + sleep 2 + if ! lxc_running; then + json_success "Service stopped" + else + json_error "Failed to stop service" + fi +} + +# Restart service +restart_service() { + /etc/init.d/gitea restart >/dev/null 2>&1 & + + sleep 4 + if lxc_running; then + json_success "Service restarted" + else + json_error "Service restart failed" + fi +} + +# Install Gitea +install() { + if lxc_exists; then + json_error "Already installed. Use update to refresh." + return + fi + + # Run install in background + /usr/sbin/giteactl install >/var/log/gitea-install.log 2>&1 & + + json_init_obj + json_add_boolean "started" 1 + json_add_string "message" "Installation started in background" + json_add_string "log_file" "/var/log/gitea-install.log" + json_close_obj +} + +# Uninstall Gitea +uninstall() { + /usr/sbin/giteactl uninstall >/dev/null 2>&1 + + if ! lxc_exists; then + json_success "Uninstalled successfully" + else + json_error "Uninstall failed" + fi +} + +# Update Gitea +update() { + if ! lxc_exists; then + json_error "Not installed. Run install first." + return + fi + + # Run update in background + /usr/sbin/giteactl update >/var/log/gitea-update.log 2>&1 & + + json_init_obj + json_add_boolean "started" 1 + json_add_string "message" "Update started in background" + json_add_string "log_file" "/var/log/gitea-update.log" + json_close_obj +} + +# Get logs +get_logs() { + read -r input + local lines + lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null) + [ -z "$lines" ] && lines=100 + + json_init_obj + json_add_array "logs" + + # Get container logs if running + if lxc_running; then + local data_path + config_load "$CONFIG" + config_get data_path main data_path "/srv/gitea" + + if [ -f "$data_path/log/gitea.log" ]; then + tail -n "$lines" "$data_path/log/gitea.log" 2>/dev/null | while IFS= read -r line; do + json_add_string "" "$line" + done + fi + fi + + # Also check install/update logs + for logfile in /var/log/gitea-install.log /var/log/gitea-update.log; do + [ -f "$logfile" ] || continue + tail -n 50 "$logfile" 2>/dev/null | while IFS= read -r line; do + json_add_string "" "$line" + done + done + + json_close_array + json_close_obj +} + +# List repositories +list_repos() { + local data_path + config_load "$CONFIG" + config_get data_path main data_path "/srv/gitea" + + json_init_obj + json_add_array "repos" + + local repo_root="$data_path/git/repositories" + if [ -d "$repo_root" ]; then + find "$repo_root" -name "*.git" -type d 2>/dev/null | while read -r repo; do + local rel_path="${repo#$repo_root/}" + local name=$(basename "$repo" .git) + local owner=$(dirname "$rel_path") + local size=$(du -sh "$repo" 2>/dev/null | awk '{print $1}' || echo "0") + + # Get last commit time if possible + local mtime="" + if [ -f "$repo/refs/heads/master" ]; then + mtime=$(stat -c %Y "$repo/refs/heads/master" 2>/dev/null || echo "") + elif [ -f "$repo/refs/heads/main" ]; then + mtime=$(stat -c %Y "$repo/refs/heads/main" 2>/dev/null || echo "") + fi + + json_add_object "" + json_add_string "name" "$name" + json_add_string "owner" "$owner" + json_add_string "path" "$repo" + json_add_string "size" "$size" + [ -n "$mtime" ] && json_add_int "mtime" "$mtime" + json_close_object + done + fi + + json_close_array + json_add_string "repo_root" "$repo_root" + json_close_obj +} + +# Get repository details +get_repo() { + read -r input + local name owner + name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + owner=$(echo "$input" | jsonfilter -e '@.owner' 2>/dev/null) + + if [ -z "$name" ]; then + json_error "Missing repository name" + return + fi + + local data_path + config_load "$CONFIG" + config_get data_path main data_path "/srv/gitea" + + local repo_path="$data_path/git/repositories" + [ -n "$owner" ] && repo_path="$repo_path/$owner" + repo_path="$repo_path/${name}.git" + + if [ ! -d "$repo_path" ]; then + json_error "Repository not found" + return + fi + + local size=$(du -sh "$repo_path" 2>/dev/null | awk '{print $1}' || echo "0") + local branches=$(ls -1 "$repo_path/refs/heads" 2>/dev/null | wc -l) + + # Get LAN IP + local lan_ip + local http_port ssh_port + config_get http_port main http_port "3000" + config_get ssh_port main ssh_port "2222" + lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") + + local clone_url="http://${lan_ip}:${http_port}/${owner}/${name}.git" + local ssh_clone="git@${lan_ip}:${ssh_port}/${owner}/${name}.git" + + json_init_obj + json_add_string "name" "$name" + json_add_string "owner" "$owner" + json_add_string "path" "$repo_path" + json_add_string "size" "$size" + json_add_int "branches" "$branches" + json_add_string "clone_url" "$clone_url" + json_add_string "ssh_clone" "$ssh_clone" + json_close_obj +} + +# List users (from SQLite if available) +list_users() { + local data_path + config_load "$CONFIG" + config_get data_path main data_path "/srv/gitea" + + json_init_obj + json_add_array "users" + + local db_file="$data_path/gitea.db" + if [ -f "$db_file" ] && command -v sqlite3 >/dev/null 2>&1; then + sqlite3 -separator '|' "$db_file" \ + "SELECT id, name, lower_name, email, is_admin, created_unix FROM user" 2>/dev/null | \ + while IFS='|' read -r id name lower_name email is_admin created; do + json_add_object "" + json_add_int "id" "$id" + json_add_string "name" "$name" + json_add_string "email" "$email" + json_add_boolean "is_admin" "$is_admin" + [ -n "$created" ] && json_add_int "created" "$created" + json_close_object + done + fi + + json_close_array + json_close_obj +} + +# Create admin user +create_admin() { + read -r input + local username password email + username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null) + password=$(echo "$input" | jsonfilter -e '@.password' 2>/dev/null) + email=$(echo "$input" | jsonfilter -e '@.email' 2>/dev/null) + + if [ -z "$username" ] || [ -z "$password" ] || [ -z "$email" ]; then + json_error "Missing username, password, or email" + return + fi + + if ! lxc_running; then + json_error "Service must be running to create users" + return + fi + + lxc-attach -n "$LXC_NAME" -- su-exec git /usr/local/bin/gitea admin user create \ + --username "$username" \ + --password "$password" \ + --email "$email" \ + --admin \ + --config /data/custom/conf/app.ini >/dev/null 2>&1 + + if [ $? -eq 0 ]; then + json_success "Admin user created: $username" + else + json_error "Failed to create admin user" + fi +} + +# Create backup +create_backup() { + local result + result=$(/usr/sbin/giteactl backup 2>&1) + + if echo "$result" | grep -q "Backup created"; then + local backup_file=$(echo "$result" | grep -o '/srv/gitea/backups/[^ ]*') + json_init_obj + json_add_boolean "success" 1 + json_add_string "message" "Backup created" + json_add_string "file" "$backup_file" + json_close_obj + else + json_error "Backup failed" + fi +} + +# List backups +list_backups() { + local data_path + config_load "$CONFIG" + config_get data_path main data_path "/srv/gitea" + + json_init_obj + json_add_array "backups" + + local backup_dir="$data_path/backups" + if [ -d "$backup_dir" ]; then + ls -1 "$backup_dir"/*.tar.gz 2>/dev/null | while read -r backup; do + [ -f "$backup" ] || continue + local name=$(basename "$backup") + local size=$(ls -lh "$backup" 2>/dev/null | awk '{print $5}') + local mtime=$(stat -c %Y "$backup" 2>/dev/null || echo "0") + + json_add_object "" + json_add_string "name" "$name" + json_add_string "path" "$backup" + json_add_string "size" "$size" + json_add_int "mtime" "$mtime" + json_close_object + done + fi + + json_close_array + json_close_obj +} + +# Restore backup +restore_backup() { + read -r input + local file + file=$(echo "$input" | jsonfilter -e '@.file' 2>/dev/null) + + if [ -z "$file" ] || [ ! -f "$file" ]; then + json_error "Missing or invalid backup file" + return + fi + + /usr/sbin/giteactl restore "$file" >/dev/null 2>&1 + + if [ $? -eq 0 ]; then + json_success "Restore completed" + else + json_error "Restore failed" + fi +} + +# Check install progress +get_install_progress() { + local log_file="/var/log/gitea-install.log" + local status="unknown" + local progress=0 + local message="" + + if [ -f "$log_file" ]; then + # Check for completion markers + if grep -q "Installation complete" "$log_file" 2>/dev/null; then + status="completed" + progress=100 + message="Installation completed successfully" + elif grep -q "ERROR" "$log_file" 2>/dev/null; then + status="error" + message=$(grep "ERROR" "$log_file" | tail -1) + else + status="running" + # Estimate progress based on log content + if grep -q "LXC config created" "$log_file" 2>/dev/null; then + progress=90 + message="Finalizing setup..." + elif grep -q "Gitea binary installed" "$log_file" 2>/dev/null; then + progress=70 + message="Configuring container..." + elif grep -q "Downloading Gitea" "$log_file" 2>/dev/null; then + progress=50 + message="Downloading Gitea binary..." + elif grep -q "Rootfs created" "$log_file" 2>/dev/null; then + progress=40 + message="Setting up container..." + elif grep -q "Extracting rootfs" "$log_file" 2>/dev/null; then + progress=30 + message="Extracting container rootfs..." + elif grep -q "Downloading Alpine" "$log_file" 2>/dev/null; then + progress=20 + message="Downloading Alpine rootfs..." + elif grep -q "Installing Gitea" "$log_file" 2>/dev/null; then + progress=10 + message="Starting installation..." + else + progress=5 + message="Initializing..." + fi + fi + else + status="not_started" + message="Installation has not been started" + fi + + # Check if process is still running + if pgrep -f "giteactl install" >/dev/null 2>&1; then + status="running" + fi + + json_init_obj + json_add_string "status" "$status" + json_add_int "progress" "$progress" + json_add_string "message" "$message" + json_close_obj +} + +# Main RPC handler +case "$1" in + list) + cat <<-EOF + { + "get_status": {}, + "get_stats": {}, + "get_config": {}, + "save_config": {"http_port": 3000, "ssh_port": 2222, "http_host": "str", "data_path": "str", "memory_limit": "str", "enabled": "str", "app_name": "str", "domain": "str", "protocol": "str", "disable_registration": "str", "require_signin": "str", "landing_page": "str"}, + "start": {}, + "stop": {}, + "restart": {}, + "install": {}, + "uninstall": {}, + "update": {}, + "get_logs": {"lines": 100}, + "list_repos": {}, + "get_repo": {"name": "str", "owner": "str"}, + "list_users": {}, + "create_admin": {"username": "str", "password": "str", "email": "str"}, + "create_backup": {}, + "list_backups": {}, + "restore_backup": {"file": "str"}, + "get_install_progress": {} + } + EOF + ;; + call) + case "$2" in + get_status) + get_status + ;; + get_stats) + get_stats + ;; + get_config) + get_config + ;; + save_config) + save_config + ;; + start) + start_service + ;; + stop) + stop_service + ;; + restart) + restart_service + ;; + install) + install + ;; + uninstall) + uninstall + ;; + update) + update + ;; + get_logs) + get_logs + ;; + list_repos) + list_repos + ;; + get_repo) + get_repo + ;; + list_users) + list_users + ;; + create_admin) + create_admin + ;; + create_backup) + create_backup + ;; + list_backups) + list_backups + ;; + restore_backup) + restore_backup + ;; + get_install_progress) + get_install_progress + ;; + *) + json_error "Unknown method: $2" + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-gitea/root/usr/share/luci/menu.d/luci-app-gitea.json b/package/secubox/luci-app-gitea/root/usr/share/luci/menu.d/luci-app-gitea.json new file mode 100644 index 00000000..0206e78b --- /dev/null +++ b/package/secubox/luci-app-gitea/root/usr/share/luci/menu.d/luci-app-gitea.json @@ -0,0 +1,45 @@ +{ + "admin/services/gitea": { + "title": "Gitea", + "order": 87, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": ["luci-app-gitea"], + "uci": {"gitea": true} + } + }, + "admin/services/gitea/overview": { + "title": "Overview", + "order": 10, + "action": { + "type": "view", + "path": "gitea/overview" + } + }, + "admin/services/gitea/repos": { + "title": "Repositories", + "order": 20, + "action": { + "type": "view", + "path": "gitea/repos" + } + }, + "admin/services/gitea/users": { + "title": "Users", + "order": 30, + "action": { + "type": "view", + "path": "gitea/users" + } + }, + "admin/services/gitea/settings": { + "title": "Settings", + "order": 40, + "action": { + "type": "view", + "path": "gitea/settings" + } + } +} diff --git a/package/secubox/luci-app-gitea/root/usr/share/rpcd/acl.d/luci-app-gitea.json b/package/secubox/luci-app-gitea/root/usr/share/rpcd/acl.d/luci-app-gitea.json new file mode 100644 index 00000000..ed9fc1a7 --- /dev/null +++ b/package/secubox/luci-app-gitea/root/usr/share/rpcd/acl.d/luci-app-gitea.json @@ -0,0 +1,17 @@ +{ + "luci-app-gitea": { + "description": "Grant access to Gitea Platform management", + "read": { + "ubus": { + "luci.gitea": ["get_status", "get_config", "get_logs", "list_repos", "get_repo", "list_users", "get_stats", "get_install_progress", "list_backups"] + }, + "uci": ["gitea"] + }, + "write": { + "ubus": { + "luci.gitea": ["save_config", "start", "stop", "restart", "install", "uninstall", "update", "create_backup", "restore_backup", "create_admin"] + }, + "uci": ["gitea"] + } + } +} diff --git a/package/secubox/secubox-app-gitea/Makefile b/package/secubox/secubox-app-gitea/Makefile new file mode 100644 index 00000000..e59b515b --- /dev/null +++ b/package/secubox/secubox-app-gitea/Makefile @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (C) 2025 CyberMind.fr +# +# SecuBox Gitea App - Self-hosted Git platform + +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-gitea +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_ARCH:=all + +PKG_MAINTAINER:=CyberMind Studio +PKG_LICENSE:=MIT + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-gitea + SECTION:=utils + CATEGORY:=Utilities + PKGARCH:=all + SUBMENU:=SecuBox Apps + TITLE:=SecuBox Gitea Platform + DEPENDS:=+uci +libuci +jsonfilter +wget-ssl +tar +lxc +lxc-common +git +endef + +define Package/secubox-app-gitea/description +Gitea Git Platform - Self-hosted lightweight Git service + +Features: +- Run Gitea in LXC container +- Git HTTP and SSH support +- Repository management +- User management with web UI +- SQLite database (embedded) +- Backup and restore + +Runs in LXC container with Alpine Linux. +Configure in /etc/config/gitea. +endef + +define Package/secubox-app-gitea/conffiles +/etc/config/gitea +endef + +define Build/Compile +endef + +define Package/secubox-app-gitea/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/gitea $(1)/etc/config/gitea + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/gitea $(1)/etc/init.d/gitea + + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/giteactl $(1)/usr/sbin/giteactl +endef + +define Package/secubox-app-gitea/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + echo "" + echo "Gitea Platform installed." + echo "" + echo "To install and start Gitea:" + echo " giteactl install" + echo " /etc/init.d/gitea start" + echo "" + echo "Web interface: http://:3000" + echo "SSH Git access: ssh://git@:2222" + echo "" + echo "Create admin user: giteactl admin create-user --username admin --password secret --email admin@localhost" + echo "" +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-app-gitea)) diff --git a/package/secubox/secubox-app-gitea/files/etc/config/gitea b/package/secubox/secubox-app-gitea/files/etc/config/gitea new file mode 100644 index 00000000..83e4c9d6 --- /dev/null +++ b/package/secubox/secubox-app-gitea/files/etc/config/gitea @@ -0,0 +1,23 @@ +config gitea 'main' + option enabled '0' + option http_port '3000' + option ssh_port '2222' + option http_host '0.0.0.0' + option data_path '/srv/gitea' + option memory_limit '512M' + option app_name 'SecuBox Git' + option domain 'git.local' + +config server 'server' + option protocol 'http' + option disable_registration 'false' + option require_signin 'false' + option landing_page 'explore' + +config database 'database' + option type 'sqlite3' + option path '/data/gitea.db' + +config admin 'admin' + option username 'admin' + option email 'admin@localhost' diff --git a/package/secubox/secubox-app-gitea/files/etc/init.d/gitea b/package/secubox/secubox-app-gitea/files/etc/init.d/gitea new file mode 100644 index 00000000..38469757 --- /dev/null +++ b/package/secubox/secubox-app-gitea/files/etc/init.d/gitea @@ -0,0 +1,45 @@ +#!/bin/sh /etc/rc.common +# SecuBox Gitea Platform - Self-hosted Git service +# Copyright (C) 2025 CyberMind.fr + +START=95 +STOP=10 +USE_PROCD=1 + +PROG=/usr/sbin/giteactl +CONFIG=gitea + +start_service() { + local enabled + config_load "$CONFIG" + config_get enabled main enabled '0' + + [ "$enabled" = "1" ] || { + echo "Gitea is disabled. Enable with: uci set gitea.main.enabled=1" + return 0 + } + + procd_open_instance + procd_set_param command "$PROG" service-run + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +stop_service() { + "$PROG" service-stop +} + +service_triggers() { + procd_add_reload_trigger "$CONFIG" +} + +reload_service() { + stop + start +} + +status() { + "$PROG" status +} diff --git a/package/secubox/secubox-app-gitea/files/usr/sbin/giteactl b/package/secubox/secubox-app-gitea/files/usr/sbin/giteactl new file mode 100644 index 00000000..31dac024 --- /dev/null +++ b/package/secubox/secubox-app-gitea/files/usr/sbin/giteactl @@ -0,0 +1,727 @@ +#!/bin/sh +# SecuBox Gitea Platform Controller +# Copyright (C) 2025 CyberMind.fr +# +# Manages Gitea in LXC container + +CONFIG="gitea" +LXC_NAME="gitea" + +# Paths +LXC_PATH="/srv/lxc" +LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" +LXC_CONFIG="$LXC_PATH/$LXC_NAME/config" +DATA_PATH="/srv/gitea" +BACKUP_PATH="/srv/gitea/backups" +GITEA_VERSION="1.22.6" + +# Logging +log_info() { echo "[INFO] $*"; logger -t gitea "$*"; } +log_error() { echo "[ERROR] $*" >&2; logger -t gitea -p err "$*"; } +log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; } + +# Helpers +require_root() { + [ "$(id -u)" -eq 0 ] || { + log_error "This command requires root privileges" + exit 1 + } +} + +has_lxc() { command -v lxc-start >/dev/null 2>&1; } + +ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } + +uci_get() { uci -q get ${CONFIG}.$1; } +uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } + +# Load configuration +load_config() { + http_port="$(uci_get main.http_port)" || http_port="3000" + ssh_port="$(uci_get main.ssh_port)" || ssh_port="2222" + http_host="$(uci_get main.http_host)" || http_host="0.0.0.0" + data_path="$(uci_get main.data_path)" || data_path="/srv/gitea" + memory_limit="$(uci_get main.memory_limit)" || memory_limit="512M" + app_name="$(uci_get main.app_name)" || app_name="SecuBox Git" + domain="$(uci_get main.domain)" || domain="git.local" + + # Server settings + protocol="$(uci_get server.protocol)" || protocol="http" + disable_registration="$(uci_get server.disable_registration)" || disable_registration="false" + require_signin="$(uci_get server.require_signin)" || require_signin="false" + landing_page="$(uci_get server.landing_page)" || landing_page="explore" + + # Database settings + db_type="$(uci_get database.type)" || db_type="sqlite3" + db_path="$(uci_get database.path)" || db_path="/data/gitea.db" + + DATA_PATH="$data_path" + BACKUP_PATH="$data_path/backups" + + ensure_dir "$data_path" + ensure_dir "$data_path/git" + ensure_dir "$data_path/custom" + ensure_dir "$data_path/custom/conf" + ensure_dir "$BACKUP_PATH" +} + +# Usage +usage() { + cat < [options] + +Commands: + install Download Alpine rootfs and setup LXC container + uninstall Remove container (preserves repositories) + update Update Gitea binary to latest version + start Start Gitea service (via init) + stop Stop Gitea service (via init) + restart Restart Gitea service + status Show service status (JSON format) + logs Show container logs + shell Open shell in container + + backup Create backup of repos and database + restore Restore from backup + + admin create-user Create admin user + --username + --password + --email + + service-run Start service (used by init) + service-stop Stop service (used by init) + +Configuration: + /etc/config/gitea + +Data directory: + /srv/gitea + +EOF +} + +# Check prerequisites +lxc_check_prereqs() { + if ! has_lxc; then + log_error "LXC not installed. Install with: opkg install lxc lxc-common" + return 1 + fi + return 0 +} + +# Detect architecture for Gitea download +get_gitea_arch() { + local arch=$(uname -m) + case "$arch" in + x86_64) echo "linux-amd64" ;; + aarch64) echo "linux-arm64" ;; + armv7l) echo "linux-arm-6" ;; + *) log_error "Unsupported architecture: $arch"; return 1 ;; + esac +} + +# Create LXC rootfs from Alpine +lxc_create_rootfs() { + local rootfs="$LXC_ROOTFS" + local arch=$(uname -m) + + log_info "Creating Alpine rootfs for Gitea..." + + ensure_dir "$rootfs" + + # Use Alpine mini rootfs + local alpine_version="3.21" + case "$arch" in + x86_64) alpine_arch="x86_64" ;; + aarch64) alpine_arch="aarch64" ;; + armv7l) alpine_arch="armv7" ;; + *) log_error "Unsupported architecture: $arch"; return 1 ;; + esac + + local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v${alpine_version}/releases/${alpine_arch}/alpine-minirootfs-${alpine_version}.0-${alpine_arch}.tar.gz" + local tmpfile="/tmp/alpine-rootfs.tar.gz" + + log_info "Downloading Alpine ${alpine_version} rootfs..." + wget -q -O "$tmpfile" "$alpine_url" || { + log_error "Failed to download Alpine rootfs" + return 1 + } + + log_info "Extracting rootfs..." + tar -xzf "$tmpfile" -C "$rootfs" || { + log_error "Failed to extract rootfs" + return 1 + } + rm -f "$tmpfile" + + # Setup resolv.conf + cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \ + echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf" + + log_info "Rootfs created successfully" + return 0 +} + +# Download and install Gitea binary +install_gitea_binary() { + local rootfs="$LXC_ROOTFS" + local gitea_arch=$(get_gitea_arch) + + [ -z "$gitea_arch" ] && return 1 + + log_info "Downloading Gitea ${GITEA_VERSION}..." + local gitea_url="https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-${gitea_arch}" + + ensure_dir "$rootfs/usr/local/bin" + + wget -q -O "$rootfs/usr/local/bin/gitea" "$gitea_url" || { + log_error "Failed to download Gitea binary" + return 1 + } + + chmod +x "$rootfs/usr/local/bin/gitea" + log_info "Gitea binary installed" + return 0 +} + +# Install Alpine packages inside container +install_container_packages() { + local rootfs="$LXC_ROOTFS" + + log_info "Installing container packages..." + + # Create install script + cat > "$rootfs/tmp/install-deps.sh" << 'SCRIPT' +#!/bin/sh +apk update +apk add --no-cache git git-lfs openssh sqlite bash su-exec +# Create git user +adduser -D -s /bin/bash -h /data git 2>/dev/null || true +# Setup SSH directory +mkdir -p /data/ssh +chmod 700 /data/ssh +touch /tmp/.deps-installed +SCRIPT + chmod +x "$rootfs/tmp/install-deps.sh" + + # Run in a temporary container + lxc-execute -n "$LXC_NAME" -f "$LXC_CONFIG" -- /tmp/install-deps.sh 2>/dev/null || { + # Fallback: run via start/attach + lxc-start -n "$LXC_NAME" -f "$LXC_CONFIG" -d + sleep 2 + lxc-attach -n "$LXC_NAME" -- /tmp/install-deps.sh + lxc-stop -n "$LXC_NAME" -k 2>/dev/null + } + + rm -f "$rootfs/tmp/install-deps.sh" + log_info "Container packages installed" + return 0 +} + +# Create startup script +create_startup_script() { + local rootfs="$LXC_ROOTFS" + + cat > "$rootfs/opt/start-gitea.sh" << 'STARTUP' +#!/bin/sh +set -e + +export GITEA_WORK_DIR=/data +export USER=git + +# Ensure git user exists +id -u git >/dev/null 2>&1 || adduser -D -s /bin/bash -h /data git + +# Ensure directories have correct ownership +chown -R git:git /data/git 2>/dev/null || true +chown -R git:git /data/custom 2>/dev/null || true + +# Generate SSH host keys if needed +if [ ! -f /data/ssh/ssh_host_rsa_key ]; then + echo "Generating SSH host keys..." + mkdir -p /data/ssh + ssh-keygen -A + mv /etc/ssh/ssh_host_* /data/ssh/ 2>/dev/null || true + chown root:root /data/ssh/ssh_host_* + chmod 600 /data/ssh/ssh_host_*_key + chmod 644 /data/ssh/ssh_host_*_key.pub +fi + +# Create sshd config for git +cat > /data/ssh/sshd_config << 'SSHD' +Port ${GITEA_SSH_PORT:-2222} +ListenAddress 0.0.0.0 +HostKey /data/ssh/ssh_host_rsa_key +HostKey /data/ssh/ssh_host_ecdsa_key +HostKey /data/ssh/ssh_host_ed25519_key +PermitRootLogin no +PubkeyAuthentication yes +AuthorizedKeysFile /data/git/.ssh/authorized_keys +PasswordAuthentication no +ChallengeResponseAuthentication no +UsePAM no +PrintMotd no +AcceptEnv LANG LC_* +Subsystem sftp /usr/lib/ssh/sftp-server +SSHD + +# Start SSH server for git operations (optional, Gitea has built-in SSH) +# /usr/sbin/sshd -f /data/ssh/sshd_config + +# Generate app.ini if not exists +if [ ! -f /data/custom/conf/app.ini ]; then + mkdir -p /data/custom/conf + cat > /data/custom/conf/app.ini << EOF +[server] +APP_NAME = ${GITEA_APP_NAME:-SecuBox Git} +DOMAIN = ${GITEA_DOMAIN:-git.local} +HTTP_ADDR = ${GITEA_HTTP_HOST:-0.0.0.0} +HTTP_PORT = ${GITEA_HTTP_PORT:-3000} +ROOT_URL = http://${GITEA_DOMAIN:-git.local}:${GITEA_HTTP_PORT:-3000}/ +DISABLE_SSH = false +START_SSH_SERVER = true +SSH_PORT = ${GITEA_SSH_PORT:-2222} +SSH_LISTEN_HOST = 0.0.0.0 +LFS_START_SERVER = true + +[database] +DB_TYPE = sqlite3 +PATH = /data/gitea.db + +[repository] +ROOT = /data/git/repositories + +[security] +INSTALL_LOCK = true +SECRET_KEY = $(head -c 32 /dev/urandom | base64 | tr -d '\n') +INTERNAL_TOKEN = $(head -c 64 /dev/urandom | base64 | tr -d '\n') + +[service] +DISABLE_REGISTRATION = ${GITEA_DISABLE_REGISTRATION:-false} +REQUIRE_SIGNIN_VIEW = ${GITEA_REQUIRE_SIGNIN:-false} + +[log] +MODE = console +LEVEL = Info + +[ui] +DEFAULT_THEME = gitea-dark +EOF + chown git:git /data/custom/conf/app.ini +fi + +# Start Gitea +echo "Starting Gitea..." +cd /data +exec su-exec git /usr/local/bin/gitea web --config /data/custom/conf/app.ini +STARTUP + chmod +x "$rootfs/opt/start-gitea.sh" +} + +# Create LXC config +lxc_create_config() { + load_config + + ensure_dir "$(dirname "$LXC_CONFIG")" + + # Convert memory limit to bytes + local mem_bytes + case "$memory_limit" in + *G|*g) mem_bytes=$((${memory_limit%[Gg]} * 1024 * 1024 * 1024)) ;; + *M|*m) mem_bytes=$((${memory_limit%[Mm]} * 1024 * 1024)) ;; + *K|*k) mem_bytes=$((${memory_limit%[Kk]} * 1024)) ;; + *) mem_bytes="$memory_limit" ;; + esac + + cat > "$LXC_CONFIG" << EOF +# Gitea Platform LXC Configuration +lxc.uts.name = $LXC_NAME +lxc.rootfs.path = dir:$LXC_ROOTFS +lxc.arch = $(uname -m) + +# Network: use host network +lxc.net.0.type = none + +# Mount points +lxc.mount.auto = proc:mixed sys:ro cgroup:mixed +lxc.mount.entry = $data_path/git data/git none bind,create=dir 0 0 +lxc.mount.entry = $data_path/custom data/custom none bind,create=dir 0 0 +lxc.mount.entry = $data_path data none bind,create=dir 0 0 + +# Environment +lxc.environment = GITEA_HTTP_HOST=$http_host +lxc.environment = GITEA_HTTP_PORT=$http_port +lxc.environment = GITEA_SSH_PORT=$ssh_port +lxc.environment = GITEA_APP_NAME=$app_name +lxc.environment = GITEA_DOMAIN=$domain +lxc.environment = GITEA_DISABLE_REGISTRATION=$disable_registration +lxc.environment = GITEA_REQUIRE_SIGNIN=$require_signin + +# Security +lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio + +# Resource limits +lxc.cgroup.memory.limit_in_bytes = $mem_bytes + +# Init command +lxc.init.cmd = /opt/start-gitea.sh +EOF + + log_info "LXC config created" +} + +# Container control +lxc_running() { + lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" +} + +lxc_exists() { + [ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ] +} + +lxc_stop() { + if lxc_running; then + log_info "Stopping Gitea container..." + lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true + sleep 2 + fi +} + +lxc_run() { + load_config + lxc_stop + + if ! lxc_exists; then + log_error "Container not installed. Run: giteactl install" + return 1 + fi + + # Regenerate config in case settings changed + lxc_create_config + + log_info "Starting Gitea container..." + exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" +} + +# Commands +cmd_install() { + require_root + load_config + + log_info "Installing Gitea Platform..." + + lxc_check_prereqs || exit 1 + + # Create container + if ! lxc_exists; then + lxc_create_rootfs || exit 1 + fi + + # Install Gitea binary + install_gitea_binary || exit 1 + + # Create startup script + create_startup_script + + # Create config + lxc_create_config || exit 1 + + # Install container packages (do this separately as it needs a running container) + # We'll let the startup script handle package installation on first run instead + + # Enable service + uci_set main.enabled '1' + /etc/init.d/gitea enable 2>/dev/null || true + + log_info "Installation complete!" + log_info "" + log_info "Start with: /etc/init.d/gitea start" + log_info "Web interface: http://:$http_port" + log_info "SSH Git access: ssh://git@:$ssh_port" + log_info "" + log_info "Create admin: giteactl admin create-user --username admin --password secret --email admin@localhost" +} + +cmd_uninstall() { + require_root + + log_info "Uninstalling Gitea Platform..." + + # Stop service + /etc/init.d/gitea stop 2>/dev/null || true + /etc/init.d/gitea disable 2>/dev/null || true + + lxc_stop + + # Remove container (keep data) + if [ -d "$LXC_PATH/$LXC_NAME" ]; then + rm -rf "$LXC_PATH/$LXC_NAME" + log_info "Container removed" + fi + + uci_set main.enabled '0' + + log_info "Gitea Platform uninstalled" + log_info "Data preserved in: $(uci_get main.data_path)" +} + +cmd_update() { + require_root + load_config + + if ! lxc_exists; then + log_error "Container not installed. Run: giteactl install" + return 1 + fi + + log_info "Updating Gitea binary..." + + # Download new binary + install_gitea_binary || exit 1 + + # Restart if running + if [ "$(uci_get main.enabled)" = "1" ]; then + /etc/init.d/gitea restart + fi + + log_info "Update complete" +} + +cmd_status() { + load_config + + local enabled="$(uci_get main.enabled)" + local running="false" + local installed="false" + local uptime="" + + if lxc_exists; then + installed="true" + fi + + if lxc_running; then + running="true" + uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1 | awk '{print $3}') + fi + + # Get LAN IP for URL + local lan_ip + lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") + + # Count repositories + local repo_count=0 + if [ -d "$data_path/git/repositories" ]; then + repo_count=$(find "$data_path/git/repositories" -name "*.git" -type d 2>/dev/null | wc -l) + fi + + # Get disk usage + local disk_usage="0" + if [ -d "$data_path" ]; then + disk_usage=$(du -sh "$data_path" 2>/dev/null | awk '{print $1}' || echo "0") + fi + + cat << EOF +{ + "enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"), + "running": $running, + "installed": $installed, + "uptime": "$uptime", + "http_port": $http_port, + "ssh_port": $ssh_port, + "data_path": "$data_path", + "memory_limit": "$memory_limit", + "app_name": "$app_name", + "domain": "$domain", + "repo_count": $repo_count, + "disk_usage": "$disk_usage", + "http_url": "http://${lan_ip}:${http_port}", + "ssh_url": "ssh://git@${lan_ip}:${ssh_port}", + "container_name": "$LXC_NAME", + "version": "$GITEA_VERSION" +} +EOF +} + +cmd_logs() { + load_config + + local lines="${1:-100}" + + # Check for gitea logs + if lxc_running; then + log_info "Container logs (last $lines lines):" + lxc-attach -n "$LXC_NAME" -- cat /data/log/gitea.log 2>/dev/null | tail -n "$lines" || \ + echo "No logs available" + else + echo "Container not running" + fi + + # Also check install logs + for logfile in /var/log/gitea-install.log /var/log/gitea-update.log; do + if [ -f "$logfile" ]; then + echo "" + echo "=== $logfile ===" + tail -n 50 "$logfile" + fi + done +} + +cmd_shell() { + require_root + + if ! lxc_running; then + log_error "Container not running" + exit 1 + fi + + lxc-attach -n "$LXC_NAME" -- /bin/sh +} + +cmd_backup() { + require_root + load_config + + local backup_file="$BACKUP_PATH/gitea-backup-$(date +%Y%m%d-%H%M%S).tar.gz" + + log_info "Creating backup..." + ensure_dir "$BACKUP_PATH" + + # Stop service for consistent backup + local was_running=0 + if lxc_running; then + was_running=1 + lxc_stop + fi + + # Create backup + tar -czf "$backup_file" -C "$data_path" \ + git \ + custom \ + gitea.db 2>/dev/null || true + + if [ $was_running -eq 1 ]; then + /etc/init.d/gitea start & + fi + + local size=$(ls -lh "$backup_file" 2>/dev/null | awk '{print $5}') + log_info "Backup created: $backup_file ($size)" + echo "$backup_file" +} + +cmd_restore() { + require_root + load_config + + local backup_file="$1" + + if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then + log_error "Usage: giteactl restore " + log_error "Available backups:" + ls -la "$BACKUP_PATH"/*.tar.gz 2>/dev/null || echo "No backups found" + return 1 + fi + + log_info "Restoring from: $backup_file" + + # Stop service + local was_running=0 + if lxc_running; then + was_running=1 + lxc_stop + fi + + # Restore backup + tar -xzf "$backup_file" -C "$data_path" + + if [ $was_running -eq 1 ]; then + /etc/init.d/gitea start & + fi + + log_info "Restore complete" +} + +cmd_admin_create_user() { + require_root + load_config + + local username="" + local password="" + local email="" + + # Parse arguments + while [ $# -gt 0 ]; do + case "$1" in + --username) username="$2"; shift 2 ;; + --password) password="$2"; shift 2 ;; + --email) email="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [ -z "$username" ] || [ -z "$password" ] || [ -z "$email" ]; then + log_error "Usage: giteactl admin create-user --username --password --email " + return 1 + fi + + if ! lxc_running; then + log_error "Container must be running to create users" + return 1 + fi + + log_info "Creating admin user: $username" + + lxc-attach -n "$LXC_NAME" -- su-exec git /usr/local/bin/gitea admin user create \ + --username "$username" \ + --password "$password" \ + --email "$email" \ + --admin \ + --config /data/custom/conf/app.ini + + if [ $? -eq 0 ]; then + log_info "Admin user created successfully" + else + log_error "Failed to create admin user" + return 1 + fi +} + +cmd_service_run() { + require_root + load_config + + lxc_check_prereqs || exit 1 + lxc_run +} + +cmd_service_stop() { + require_root + lxc_stop +} + +# Main +case "${1:-}" in + install) shift; cmd_install "$@" ;; + uninstall) shift; cmd_uninstall "$@" ;; + update) shift; cmd_update "$@" ;; + start) /etc/init.d/gitea start ;; + stop) /etc/init.d/gitea stop ;; + restart) /etc/init.d/gitea restart ;; + status) shift; cmd_status "$@" ;; + logs) shift; cmd_logs "$@" ;; + shell) shift; cmd_shell "$@" ;; + backup) shift; cmd_backup "$@" ;; + restore) shift; cmd_restore "$@" ;; + admin) + shift + case "${1:-}" in + create-user) shift; cmd_admin_create_user "$@" ;; + *) echo "Usage: giteactl admin create-user --username --password --email "; exit 1 ;; + esac + ;; + service-run) shift; cmd_service_run "$@" ;; + service-stop) shift; cmd_service_stop "$@" ;; + *) usage ;; +esac