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:
parent
814a85754d
commit
d43a02a397
29
package/secubox/luci-app-gitea/Makefile
Normal file
29
package/secubox/luci-app-gitea/Makefile
Normal 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))
|
||||
@ -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] || []
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
723
package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea
Normal file
723
package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea
Normal 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
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
80
package/secubox/secubox-app-gitea/Makefile
Normal file
80
package/secubox/secubox-app-gitea/Makefile
Normal 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))
|
||||
23
package/secubox/secubox-app-gitea/files/etc/config/gitea
Normal file
23
package/secubox/secubox-app-gitea/files/etc/config/gitea
Normal 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'
|
||||
45
package/secubox/secubox-app-gitea/files/etc/init.d/gitea
Normal file
45
package/secubox/secubox-app-gitea/files/etc/init.d/gitea
Normal 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
|
||||
}
|
||||
727
package/secubox/secubox-app-gitea/files/usr/sbin/giteactl
Normal file
727
package/secubox/secubox-app-gitea/files/usr/sbin/giteactl
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user