feat(gitea): Add self-hosted Git platform for OpenWrt

Add secubox-app-gitea and luci-app-gitea packages:

secubox-app-gitea:
- LXC container with Alpine 3.21 rootfs
- Gitea 1.22.6 binary (auto-detect amd64/arm64/armv7)
- HTTP (3000) and SSH (2222) ports
- SQLite database (embedded)
- giteactl: install/uninstall/update/backup/restore

luci-app-gitea:
- Cyberpunk themed dashboard
- Repository browser with clone URLs
- User management interface
- Server and security settings
- Backup/restore functionality
- 18 RPCD methods

Resource requirements: 256MB RAM minimum, ~100MB storage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-24 10:03:31 +01:00
parent 814a85754d
commit d43a02a397
14 changed files with 3715 additions and 0 deletions

View File

@ -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 <contact@cybermind.fr>
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))

View File

@ -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] || []
};
});
}
});

View File

@ -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;
}
}

View File

@ -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'))
])
]);
}
});

View File

@ -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');
}
}
});

View File

@ -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'))
])
]);
}
});

View File

@ -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');
});
}
});

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"]
}
}
}

View File

@ -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 <contact@cybermind.fr>
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://<router-ip>:3000"
echo "SSH Git access: ssh://git@<router-ip>: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))

View File

@ -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'

View File

@ -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
}

View File

@ -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 <<EOF
SecuBox Gitea Platform Controller
Usage: $(basename $0) <command> [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 <file> Restore from backup
admin create-user Create admin user
--username <name>
--password <pass>
--email <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://<router-ip>:$http_port"
log_info "SSH Git access: ssh://git@<router-ip>:$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 <backup-file>"
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 <name> --password <pass> --email <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 <name> --password <pass> --email <email>"; exit 1 ;;
esac
;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
*) usage ;;
esac