tambahakan banyak
This commit is contained in:
19
bin/g3n.ts
19
bin/g3n.ts
@@ -5,7 +5,8 @@ import path from "path";
|
||||
import { generateEnvTypes } from "../generate/env.generate";
|
||||
import checkPort from "./src/port";
|
||||
import route from "./src/route";
|
||||
import not3 from "./src/not3";
|
||||
import compose from "./src/compose";
|
||||
import generateDockerfile from "./src/docker-file";
|
||||
|
||||
interface CheckPortResult {
|
||||
port: number;
|
||||
@@ -21,6 +22,7 @@ Commands:
|
||||
env Generate env.d.ts from .env file
|
||||
scan-port Scan port range (default 3000-4000)
|
||||
route Generate routes.ts from AppRoutes.tsx
|
||||
compose Generate compose.yml from name
|
||||
Options:
|
||||
--env Path ke file .env (default: .env)
|
||||
--out Path file output (default: types/env.d.ts)
|
||||
@@ -32,7 +34,7 @@ Examples:
|
||||
g3n env --env .env.local --out src/types/env.d.ts
|
||||
g3n scan-port --start 7700 --end 7800 --host 127.0.0.1
|
||||
g3n route
|
||||
`;
|
||||
g3n compose <name>`;
|
||||
|
||||
(async () => {
|
||||
const cmd = args._[0];
|
||||
@@ -75,8 +77,17 @@ Examples:
|
||||
return;
|
||||
}
|
||||
|
||||
if(cmd === "note") {
|
||||
not3()
|
||||
if (cmd === "compose") {
|
||||
if (!args._[1]) {
|
||||
console.error("❌ Name is required");
|
||||
return;
|
||||
}
|
||||
compose(args._[1] as string);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === "docker-file") {
|
||||
generateDockerfile();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
275
bin/not3.ts
275
bin/not3.ts
@@ -1,275 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const CONFIG_FILE = path.join(os.homedir(), '.note.conf');
|
||||
|
||||
interface Config {
|
||||
TOKEN?: string;
|
||||
REPO?: string;
|
||||
URL?: string;
|
||||
}
|
||||
|
||||
// --- Check dependencies ---
|
||||
function checkDependency(cmd: string): boolean {
|
||||
try {
|
||||
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function checkDependencies(): void {
|
||||
const deps = ['curl', 'jq'];
|
||||
for (const cmd of deps) {
|
||||
if (!checkDependency(cmd)) {
|
||||
console.error(`❌ Missing dependency: ${cmd} (please install it)`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Config management ---
|
||||
function createDefaultConfig(): void {
|
||||
const defaultConfig = `TOKEN=
|
||||
REPO=
|
||||
URL=https://cld-dkr-makuro-seafile.wibudev.com/api2
|
||||
`;
|
||||
fs.writeFileSync(CONFIG_FILE, defaultConfig);
|
||||
}
|
||||
|
||||
function editConfig(): void {
|
||||
if (!fs.existsSync(CONFIG_FILE)) {
|
||||
createDefaultConfig();
|
||||
}
|
||||
|
||||
const editor = process.env.EDITOR || 'vim';
|
||||
try {
|
||||
execSync(`${editor} "${CONFIG_FILE}"`, { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to open editor');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfig(): Config {
|
||||
if (!fs.existsSync(CONFIG_FILE)) {
|
||||
console.error(`⚠️ Config file not found at ${CONFIG_FILE}`);
|
||||
console.error('Run: node note.ts config to create/edit it.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
const config: Config = {};
|
||||
|
||||
for (const line of configContent.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key && valueParts.length > 0) {
|
||||
const value = valueParts.join('=');
|
||||
config[key as keyof Config] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.TOKEN || !config.REPO) {
|
||||
console.error(`❌ Config invalid. Please set TOKEN=... and REPO=... inside ${CONFIG_FILE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Set default URL if not provided
|
||||
if (!config.URL) {
|
||||
console.error(`❌ Config invalid. Please set URL=... inside ${CONFIG_FILE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// --- HTTP helpers ---
|
||||
function curlCommand(config: Config, options: string): string {
|
||||
try {
|
||||
return execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options}`,
|
||||
{ encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
console.error('❌ Request failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function curlCommandSilent(config: Config, options: string): void {
|
||||
try {
|
||||
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options} >/dev/null`,
|
||||
{ stdio: 'ignore' });
|
||||
} catch (error) {
|
||||
console.error('❌ Request failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Commands ---
|
||||
function listFiles(config: Config): void {
|
||||
const response = curlCommand(config, `"${config.URL}/${config.REPO}/dir/?p=/"`);
|
||||
try {
|
||||
const files = JSON.parse(response);
|
||||
for (const file of files) {
|
||||
console.log(file.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to parse response');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function catFile(config: Config, fileName: string): void {
|
||||
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
|
||||
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
|
||||
const content = curlCommand(config, `"${downloadUrl}"`);
|
||||
console.log(content);
|
||||
}
|
||||
|
||||
function uploadFile(config: Config, localFile: string, remoteFile?: string): void {
|
||||
if (!fs.existsSync(localFile)) {
|
||||
console.error(`❌ File not found: ${localFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const remoteName = remoteFile || path.basename(localFile);
|
||||
const uploadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/upload-link/?p=/"`);
|
||||
const uploadUrl = uploadUrlResponse.replace(/"/g, '');
|
||||
|
||||
try {
|
||||
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" \
|
||||
-F "file=@${localFile}" \
|
||||
-F "filename=${remoteName}" \
|
||||
-F "parent_dir=/" \
|
||||
"${uploadUrl}" >/dev/null`, { stdio: 'ignore' });
|
||||
console.log(`✅ Uploaded ${localFile} → ${remoteName}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Upload failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile(config: Config, fileName: string): void {
|
||||
curlCommandSilent(config, `-X DELETE "${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
|
||||
console.log(`🗑️ Removed ${fileName}`);
|
||||
}
|
||||
|
||||
function moveFile(config: Config, oldName: string, newName: string): void {
|
||||
try {
|
||||
execSync(`curl -s -X POST -H "Authorization: Token ${config.TOKEN}" \
|
||||
-d "operation=rename" \
|
||||
-d "newname=${newName}" \
|
||||
"${config.URL}/${config.REPO}/file/?p=/${oldName}" >/dev/null`, { stdio: 'ignore' });
|
||||
console.log(`✏️ Renamed ${oldName} → ${newName}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Rename failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile(config: Config, remoteFile: string, localFile?: string): void {
|
||||
const localName = localFile || remoteFile;
|
||||
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${remoteFile}"`);
|
||||
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
|
||||
|
||||
try {
|
||||
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" "${downloadUrl}" -o "${localName}"`,
|
||||
{ stdio: 'ignore' });
|
||||
console.log(`⬇️ Downloaded ${remoteFile} → ${localName}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Download failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function showHelp(): void {
|
||||
console.log(`note - simple CLI for Seafile notes
|
||||
Usage:
|
||||
node note.ts ls List files
|
||||
node note.ts cat <file> Show file content
|
||||
node note.ts cp <local> [remote] Upload file
|
||||
node note.ts rm <remote> Remove file
|
||||
node note.ts mv <old> <new> Rename/move file
|
||||
node note.ts get <remote> [local] Download file
|
||||
node note.ts config Edit config (~/.note.conf)
|
||||
|
||||
Config (~/.note.conf):
|
||||
TOKEN=your_seafile_token
|
||||
REPO=repos/<repo-id>
|
||||
URL=your_seafile_url/api2`);
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
function not3(): void {
|
||||
const args = process.argv.slice(2);
|
||||
const cmd = args[0] || 'help';
|
||||
|
||||
// Handle config command without dependency check
|
||||
if (cmd === 'config') {
|
||||
editConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check dependencies for other commands
|
||||
checkDependencies();
|
||||
|
||||
// Load config for other commands
|
||||
const config = loadConfig();
|
||||
|
||||
switch (cmd) {
|
||||
case 'ls':
|
||||
listFiles(config);
|
||||
break;
|
||||
|
||||
case 'cat':
|
||||
if (!args[1]) {
|
||||
console.error('Usage: node note.ts cat <file>');
|
||||
process.exit(1);
|
||||
}
|
||||
catFile(config, args[1]);
|
||||
break;
|
||||
|
||||
case 'cp':
|
||||
if (!args[1]) {
|
||||
console.error('Usage: node note.ts cp <local_file> [remote_file]');
|
||||
process.exit(1);
|
||||
}
|
||||
uploadFile(config, args[1], args[2]);
|
||||
break;
|
||||
|
||||
case 'rm':
|
||||
if (!args[1]) {
|
||||
console.error('Usage: node note.ts rm <remote_file>');
|
||||
process.exit(1);
|
||||
}
|
||||
removeFile(config, args[1]);
|
||||
break;
|
||||
|
||||
case 'mv':
|
||||
if (!args[1] || !args[2]) {
|
||||
console.error('Usage: node note.ts mv <old_name> <new_name>');
|
||||
process.exit(1);
|
||||
}
|
||||
moveFile(config, args[1], args[2]);
|
||||
break;
|
||||
|
||||
case 'get':
|
||||
if (!args[1]) {
|
||||
console.error('Usage: node note.ts get <remote_file> [local_file]');
|
||||
process.exit(1);
|
||||
}
|
||||
downloadFile(config, args[1], args[2]);
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
default:
|
||||
showHelp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
79
bin/src/assets/Dockerfile
Normal file
79
bin/src/assets/Dockerfile
Normal file
@@ -0,0 +1,79 @@
|
||||
FROM ubuntu:22.04 AS dev
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# --- Install runtime dependencies ---
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl git unzip ca-certificates openssh-server bash tini vim docker.io tmux \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# --- Install Node.js 22 ---
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Bun
|
||||
RUN curl -fsSL https://bun.sh/install | bash \
|
||||
&& cp /root/.bun/bin/bun /usr/local/bin/bun \
|
||||
&& cp /root/.bun/bin/bunx /usr/local/bin/bunx \
|
||||
&& bun --version
|
||||
|
||||
# --- Create non-root user `bip` ---
|
||||
ARG SSH_USER=bip
|
||||
RUN useradd -ms /bin/bash $SSH_USER \
|
||||
&& mkdir -p /home/$SSH_USER/.ssh \
|
||||
&& chmod 700 /home/$SSH_USER/.ssh \
|
||||
&& chown -R $SSH_USER:$SSH_USER /home/$SSH_USER/.ssh
|
||||
|
||||
# --- Configure SSH ---
|
||||
RUN mkdir -p /var/run/sshd \
|
||||
&& sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config \
|
||||
&& sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config \
|
||||
&& echo "AllowUsers $SSH_USER" >> /etc/ssh/sshd_config
|
||||
|
||||
# Copy deploy script (milik user bip)
|
||||
COPY --chown=$SSH_USER:$SSH_USER deploy /usr/local/bin/deploy
|
||||
RUN chmod +x /usr/local/bin/deploy
|
||||
|
||||
# Authorized keys mount point
|
||||
VOLUME ["/home/$SSH_USER/.ssh"]
|
||||
|
||||
# Expose SSH port
|
||||
EXPOSE 22
|
||||
|
||||
# Use Tini as entrypoint for signal handling
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
|
||||
# Start SSH daemon in foreground
|
||||
CMD ["/usr/sbin/sshd", "-D"]
|
||||
|
||||
FROM ubuntu:22.04 AS prod
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# --- Install runtime dependencies ---
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl git unzip ca-certificates bash tini \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# --- Install Node.js 22 ---
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Bun
|
||||
RUN curl -fsSL https://bun.sh/install | bash \
|
||||
&& cp /root/.bun/bin/bun /usr/local/bin/bun \
|
||||
&& cp /root/.bun/bin/bunx /usr/local/bin/bunx \
|
||||
&& bun --version
|
||||
|
||||
# --- Set working dir ---
|
||||
WORKDIR /app/current
|
||||
|
||||
# Expose port (ubah sesuai app)
|
||||
EXPOSE 3000
|
||||
|
||||
# Use Tini as entrypoint for signal handling
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
|
||||
CMD ["bun", "run", "start"]
|
||||
237
bin/src/assets/deploy
Normal file
237
bin/src/assets/deploy
Normal file
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =====================================================
|
||||
# Deploy Script WhatsApp Server (dijalankan di dalam container prod)
|
||||
# =====================================================
|
||||
|
||||
CONFIG_FILE="/app/.deploy.conf"
|
||||
ENV_FILE="/app/.env"
|
||||
DOCKER_HOST_PROXY=""
|
||||
|
||||
# --- Default config ---
|
||||
GIT_URL_DEFAULT=""
|
||||
BRANCH_DEFAULT=""
|
||||
SERVICE_DEFAULT=""
|
||||
|
||||
# =====================================================
|
||||
# Utils
|
||||
# =====================================================
|
||||
log() {
|
||||
echo "[ $(date '+%Y-%m-%d %H:%M:%S') ] $*"
|
||||
}
|
||||
|
||||
# =====================================================
|
||||
# Load config
|
||||
# =====================================================
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
# Kalau ada env DEPLOY_SERVICE, jadikan default SERVICE
|
||||
SERVICE_DEFAULT="${DEPLOY_SERVICE:-}"
|
||||
cat > "$CONFIG_FILE" <<EOF
|
||||
GIT_URL="$GIT_URL_DEFAULT"
|
||||
BRANCH="$BRANCH_DEFAULT"
|
||||
SERVICE="$SERVICE_DEFAULT"
|
||||
EOF
|
||||
log "⚠️ File .deploy.conf belum ada ($CONFIG_FILE), membuat dengan default"
|
||||
fi
|
||||
|
||||
# Source config
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONFIG_FILE"
|
||||
|
||||
# Kalau SERVICE kosong, fallback ke DEPLOY_SERVICE
|
||||
if [ -z "${SERVICE:-}" ] && [ -n "${DEPLOY_SERVICE:-}" ]; then
|
||||
SERVICE="$DEPLOY_SERVICE"
|
||||
fi
|
||||
|
||||
# Set docker proxy kalau SERVICE sudah ada
|
||||
if [ -n "${SERVICE:-}" ]; then
|
||||
DOCKER_HOST_PROXY="tcp://$SERVICE-docker-proxy:2375"
|
||||
fi
|
||||
|
||||
ensure_config() {
|
||||
if [ -z "${GIT_URL:-}" ] || [ -z "${BRANCH:-}" ] || [ -z "${SERVICE:-}" ]; then
|
||||
log "⚠️ Config tidak lengkap, pastikan ada di $CONFIG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# =====================================================
|
||||
# Fungsi Edit Config
|
||||
# =====================================================
|
||||
edit_config() {
|
||||
vi "$CONFIG_FILE"
|
||||
log "✅ Perubahan tersimpan di $CONFIG_FILE"
|
||||
}
|
||||
|
||||
# =====================================================
|
||||
# Fungsi Edit .env
|
||||
# =====================================================
|
||||
edit_env() {
|
||||
vi "$ENV_FILE"
|
||||
log "✅ Perubahan tersimpan di $ENV_FILE"
|
||||
}
|
||||
|
||||
# =====================================================
|
||||
# Fungsi Show .env
|
||||
# =====================================================
|
||||
show_env() {
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "=== 📄 Isi $ENV_FILE ==="
|
||||
cat "$ENV_FILE"
|
||||
echo "========================="
|
||||
else
|
||||
echo "⚠️ File .env belum ada ($ENV_FILE)"
|
||||
fi
|
||||
}
|
||||
|
||||
show_log() {
|
||||
docker -H "$DOCKER_HOST_PROXY" logs "$SERVICE-prod"
|
||||
}
|
||||
|
||||
# =====================================================
|
||||
# Direktori Deploy
|
||||
# =====================================================
|
||||
WORKDIR="/app"
|
||||
NEW_DIR="$WORKDIR/current-new"
|
||||
OLD_DIR="$WORKDIR/current-old"
|
||||
CURR_DIR="$WORKDIR/current"
|
||||
|
||||
# --- Default Flags ---
|
||||
IS_DB_PUSH=false
|
||||
IS_DB_SEED=false
|
||||
IS_BUILD=true
|
||||
IS_CACHE=true
|
||||
|
||||
# --- Parse args ---
|
||||
COMMAND=${1:-""}
|
||||
shift || true
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--db-push) IS_DB_PUSH=true ;;
|
||||
--seed) IS_DB_SEED=true ;;
|
||||
--build) IS_BUILD=true ;;
|
||||
--no-build) IS_BUILD=false ;;
|
||||
--cache) IS_CACHE=true ;;
|
||||
--no-cache) IS_CACHE=false ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# =====================================================
|
||||
# Fungsi Deploy Start
|
||||
# =====================================================
|
||||
deploy_start() {
|
||||
ensure_config
|
||||
|
||||
log "🚀 Mulai proses deploy..."
|
||||
log "Gunakan repo: $GIT_URL (branch: $BRANCH, service: $SERVICE)"
|
||||
|
||||
if [ -d "$NEW_DIR" ]; then
|
||||
log "hapus folder current-new ..."
|
||||
rm -rf "$NEW_DIR"
|
||||
fi
|
||||
|
||||
log "git clone ..."
|
||||
git clone --depth 1 --branch "$BRANCH" "$GIT_URL" "$NEW_DIR"
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
log "copy .env ..."
|
||||
cp "$ENV_FILE" "$NEW_DIR/"
|
||||
fi
|
||||
|
||||
if [ "$IS_CACHE" = "true" ] && [ -d "$OLD_DIR/node_modules" ]; then
|
||||
log "copy node_modules ..."
|
||||
cp -r "$OLD_DIR/node_modules" "$NEW_DIR/"
|
||||
fi
|
||||
|
||||
if [ "$IS_CACHE" = "true" ] && [ -d "$OLD_DIR/generated/prisma" ]; then
|
||||
log "copy prisma ..."
|
||||
mkdir -p "$NEW_DIR/generated"
|
||||
cp -r "$OLD_DIR/generated/prisma" "$NEW_DIR/generated/"
|
||||
fi
|
||||
|
||||
cd "$NEW_DIR"
|
||||
|
||||
if [ "$IS_CACHE" = "true" ] && [ -f "$OLD_DIR/bun.lock" ]; then
|
||||
log "using cached dependencies ..."
|
||||
bun install --frozen-lockfile
|
||||
else
|
||||
bun install
|
||||
fi
|
||||
|
||||
if [ "$IS_DB_PUSH" = "true" ]; then
|
||||
log "prisma db push ..."
|
||||
bun x prisma db push
|
||||
fi
|
||||
|
||||
if [ "$IS_DB_SEED" = "true" ]; then
|
||||
log "seeding database ..."
|
||||
bun x prisma db seed
|
||||
fi
|
||||
|
||||
if [ "$IS_BUILD" = "true" ]; then
|
||||
log "building app ..."
|
||||
bun run build
|
||||
fi
|
||||
|
||||
if [ -d "$OLD_DIR" ]; then
|
||||
log "hapus current-old ..."
|
||||
rm -rf "$OLD_DIR"
|
||||
fi
|
||||
|
||||
if [ -d "$CURR_DIR" ]; then
|
||||
log "backup current -> current-old ..."
|
||||
mv "$CURR_DIR" "$OLD_DIR"
|
||||
fi
|
||||
|
||||
log "deploy current-new -> current ..."
|
||||
mv "$NEW_DIR" "$CURR_DIR"
|
||||
|
||||
log "restart service ..."
|
||||
docker -H "$DOCKER_HOST_PROXY" restart "$SERVICE-prod"
|
||||
}
|
||||
|
||||
# =====================================================
|
||||
# Fungsi Deploy Restart
|
||||
# =====================================================
|
||||
deploy_restart() {
|
||||
ensure_config
|
||||
log "restart service ..."
|
||||
docker -H "$DOCKER_HOST_PROXY" restart "$SERVICE-prod"
|
||||
}
|
||||
|
||||
# =====================================================
|
||||
# Main
|
||||
# =====================================================
|
||||
case "$COMMAND" in
|
||||
start)
|
||||
deploy_start
|
||||
;;
|
||||
restart)
|
||||
deploy_restart
|
||||
;;
|
||||
config)
|
||||
edit_config
|
||||
;;
|
||||
env)
|
||||
edit_env
|
||||
;;
|
||||
log)
|
||||
show_log
|
||||
;;
|
||||
show-env)
|
||||
show_env
|
||||
;;
|
||||
show-config)
|
||||
echo "📌 Git URL : $GIT_URL"
|
||||
echo "📌 Branch : $BRANCH"
|
||||
echo "📌 Service : $SERVICE"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: deploy {start|restart|config|env|show-env|show-config} [--db-push] [--seed] [--build|--no-build] [--cache|--no-cache]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
134
bin/src/compose.ts
Normal file
134
bin/src/compose.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import fs from "fs/promises";
|
||||
|
||||
|
||||
const text = (name: string) => {
|
||||
return `
|
||||
services:
|
||||
${name}-docker-proxy:
|
||||
image: tecnativa/docker-socket-proxy
|
||||
container_name: ${name}-docker-proxy
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
CONTAINERS: 1
|
||||
POST: 1
|
||||
PING: 1
|
||||
networks:
|
||||
- ${name}
|
||||
${name}-dev:
|
||||
image: bip/dev:latest
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
target: dev
|
||||
container_name: ${name}-dev
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/app:/app
|
||||
- ./data/ssh/authorized_keys:/home/bip/.ssh/authorized_keys:ro
|
||||
networks:
|
||||
- ${name}
|
||||
depends_on:
|
||||
${name}-postgres:
|
||||
condition: service_healthy
|
||||
${name}-prod:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
target: prod
|
||||
image: bip/prod:latest
|
||||
container_name: ${name}-prod
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/app:/app
|
||||
networks:
|
||||
- ${name}
|
||||
depends_on:
|
||||
${name}-postgres:
|
||||
condition: service_healthy
|
||||
${name}-postgres:
|
||||
image: postgres:16
|
||||
container_name: ${name}-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=bip
|
||||
- POSTGRES_PASSWORD=Production_123
|
||||
- POSTGRES_DB=${name}
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
networks:
|
||||
- ${name}
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U bip -d ${name}']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
${name}-frpc:
|
||||
image: snowdreamtech/frpc:latest
|
||||
container_name: ${name}-frpc
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data/frpc/frpc.toml:/etc/frp/frpc.toml:ro
|
||||
networks:
|
||||
- ${name}
|
||||
networks:
|
||||
${name}:
|
||||
driver: bridge
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
const generate = (name: string) => {
|
||||
return `
|
||||
#!/bin/bash
|
||||
|
||||
echo "Generating directory..."
|
||||
mkdir -p data data/app data/postgres data/frpc data/ssh
|
||||
|
||||
echo "Generating authorized_keys..."
|
||||
touch data/ssh/authorized_keys
|
||||
|
||||
echo "Generating frpc.toml..."
|
||||
touch data/frpc/frpc.toml
|
||||
|
||||
echo "Generating frpc.toml content..."
|
||||
cat > data/frpc/frpc.toml <<EOF
|
||||
[common]
|
||||
server_addr = "85.31.224.xxx"
|
||||
server_port = 7000
|
||||
transport.tcp_mux = true
|
||||
transport.pool_count = 5
|
||||
transport.tls.enable = true
|
||||
|
||||
auth_token = ""
|
||||
|
||||
[ssh-cld-dkr-staging-${name}.wibudev.com]
|
||||
type = tcp
|
||||
local_ip = ${name}-dev
|
||||
local_port = 22
|
||||
remote_port = 5102
|
||||
|
||||
[postgres-cld-dkr-staging-${name}.wibudev.com]
|
||||
type = tcp
|
||||
local_ip = ${name}-postgres
|
||||
local_port = 5432
|
||||
remote_port = 5202
|
||||
|
||||
[cld-dkr-staging-${name}.wibudev.com]
|
||||
type = http
|
||||
local_ip = ${name}-prod
|
||||
local_port = 3000
|
||||
custom_domains = "cld-dkr-staging-${name}.wibudev.com"
|
||||
EOF
|
||||
`
|
||||
}
|
||||
|
||||
async function compose(name: string) {
|
||||
const composeFile = text(name);
|
||||
await fs.writeFile(`./compose.yml`, composeFile);
|
||||
Bun.spawnSync(["bash", "-c", generate(name)]);
|
||||
}
|
||||
|
||||
export default compose
|
||||
|
||||
17
bin/src/docker-file.ts
Normal file
17
bin/src/docker-file.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const dockerfile = Bun.file(path.join(__dirname, "./assets/Dockerfile"));
|
||||
const deployFile = Bun.file(path.join(__dirname, "./assets/deploy"));
|
||||
|
||||
async function generateDockerfile() {
|
||||
const dockerfileText = await dockerfile.text();
|
||||
const deployText = await deployFile.text();
|
||||
|
||||
await fs.writeFile("./Dockerfile", dockerfileText);
|
||||
await fs.writeFile("./deploy", deployText);
|
||||
}
|
||||
|
||||
export default generateDockerfile;
|
||||
|
||||
|
||||
277
bin/src/not3.ts
277
bin/src/not3.ts
@@ -1,277 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const CONFIG_FILE = path.join(os.homedir(), '.note.conf');
|
||||
|
||||
interface Config {
|
||||
TOKEN?: string;
|
||||
REPO?: string;
|
||||
URL?: string;
|
||||
}
|
||||
|
||||
// --- Check dependencies ---
|
||||
function checkDependency(cmd: string): boolean {
|
||||
try {
|
||||
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function checkDependencies(): void {
|
||||
const deps = ['curl', 'jq'];
|
||||
for (const cmd of deps) {
|
||||
if (!checkDependency(cmd)) {
|
||||
console.error(`❌ Missing dependency: ${cmd} (please install it)`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Config management ---
|
||||
function createDefaultConfig(): void {
|
||||
const defaultConfig = `TOKEN=
|
||||
REPO=
|
||||
URL=https://cld-dkr-makuro-seafile.wibudev.com/api2
|
||||
`;
|
||||
fs.writeFileSync(CONFIG_FILE, defaultConfig);
|
||||
}
|
||||
|
||||
function editConfig(): void {
|
||||
if (!fs.existsSync(CONFIG_FILE)) {
|
||||
createDefaultConfig();
|
||||
}
|
||||
|
||||
const editor = process.env.EDITOR || 'vim';
|
||||
try {
|
||||
execSync(`${editor} "${CONFIG_FILE}"`, { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to open editor');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfig(): Config {
|
||||
if (!fs.existsSync(CONFIG_FILE)) {
|
||||
console.error(`⚠️ Config file not found at ${CONFIG_FILE}`);
|
||||
console.error('Run: node note.ts config to create/edit it.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
const config: Config = {};
|
||||
|
||||
for (const line of configContent.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key && valueParts.length > 0) {
|
||||
const value = valueParts.join('=');
|
||||
config[key as keyof Config] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.TOKEN || !config.REPO) {
|
||||
console.error(`❌ Config invalid. Please set TOKEN=... and REPO=... inside ${CONFIG_FILE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Set default URL if not provided
|
||||
if (!config.URL) {
|
||||
console.error(`❌ Config invalid. Please set URL=... inside ${CONFIG_FILE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// --- HTTP helpers ---
|
||||
function curlCommand(config: Config, options: string): string {
|
||||
try {
|
||||
return execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options}`,
|
||||
{ encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
console.error('❌ Request failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function curlCommandSilent(config: Config, options: string): void {
|
||||
try {
|
||||
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options} >/dev/null`,
|
||||
{ stdio: 'ignore' });
|
||||
} catch (error) {
|
||||
console.error('❌ Request failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Commands ---
|
||||
function listFiles(config: Config): void {
|
||||
const response = curlCommand(config, `"${config.URL}/${config.REPO}/dir/?p=/"`);
|
||||
try {
|
||||
const files = JSON.parse(response);
|
||||
for (const file of files) {
|
||||
console.log(file.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to parse response');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function catFile(config: Config, fileName: string): void {
|
||||
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
|
||||
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
|
||||
const content = curlCommand(config, `"${downloadUrl}"`);
|
||||
console.log(content);
|
||||
}
|
||||
|
||||
function uploadFile(config: Config, localFile: string, remoteFile?: string): void {
|
||||
if (!fs.existsSync(localFile)) {
|
||||
console.error(`❌ File not found: ${localFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const remoteName = remoteFile || path.basename(localFile);
|
||||
const uploadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/upload-link/?p=/"`);
|
||||
const uploadUrl = uploadUrlResponse.replace(/"/g, '');
|
||||
|
||||
try {
|
||||
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" \
|
||||
-F "file=@${localFile}" \
|
||||
-F "filename=${remoteName}" \
|
||||
-F "parent_dir=/" \
|
||||
"${uploadUrl}" >/dev/null`, { stdio: 'ignore' });
|
||||
console.log(`✅ Uploaded ${localFile} → ${remoteName}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Upload failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile(config: Config, fileName: string): void {
|
||||
curlCommandSilent(config, `-X DELETE "${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
|
||||
console.log(`🗑️ Removed ${fileName}`);
|
||||
}
|
||||
|
||||
function moveFile(config: Config, oldName: string, newName: string): void {
|
||||
try {
|
||||
execSync(`curl -s -X POST -H "Authorization: Token ${config.TOKEN}" \
|
||||
-d "operation=rename" \
|
||||
-d "newname=${newName}" \
|
||||
"${config.URL}/${config.REPO}/file/?p=/${oldName}" >/dev/null`, { stdio: 'ignore' });
|
||||
console.log(`✏️ Renamed ${oldName} → ${newName}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Rename failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile(config: Config, remoteFile: string, localFile?: string): void {
|
||||
const localName = localFile || remoteFile;
|
||||
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${remoteFile}"`);
|
||||
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
|
||||
|
||||
try {
|
||||
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" "${downloadUrl}" -o "${localName}"`,
|
||||
{ stdio: 'ignore' });
|
||||
console.log(`⬇️ Downloaded ${remoteFile} → ${localName}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Download failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function showHelp(): void {
|
||||
console.log(`note - simple CLI for Seafile notes
|
||||
Usage:
|
||||
node note.ts ls List files
|
||||
node note.ts cat <file> Show file content
|
||||
node note.ts cp <local> [remote] Upload file
|
||||
node note.ts rm <remote> Remove file
|
||||
node note.ts mv <old> <new> Rename/move file
|
||||
node note.ts get <remote> [local] Download file
|
||||
node note.ts config Edit config (~/.note.conf)
|
||||
|
||||
Config (~/.note.conf):
|
||||
TOKEN=your_seafile_token
|
||||
REPO=repos/<repo-id>
|
||||
URL=your_seafile_url/api2`);
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
function not3(): void {
|
||||
const args = process.argv.slice(2);
|
||||
const cmd = args[0] || 'help';
|
||||
|
||||
// Handle config command without dependency check
|
||||
if (cmd === 'config') {
|
||||
editConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check dependencies for other commands
|
||||
checkDependencies();
|
||||
|
||||
// Load config for other commands
|
||||
const config = loadConfig();
|
||||
|
||||
switch (cmd) {
|
||||
case 'ls':
|
||||
listFiles(config);
|
||||
break;
|
||||
|
||||
case 'cat':
|
||||
if (!args[1]) {
|
||||
console.error('Usage: node note.ts cat <file>');
|
||||
process.exit(1);
|
||||
}
|
||||
catFile(config, args[1]);
|
||||
break;
|
||||
|
||||
case 'cp':
|
||||
if (!args[1]) {
|
||||
console.error('Usage: node note.ts cp <local_file> [remote_file]');
|
||||
process.exit(1);
|
||||
}
|
||||
uploadFile(config, args[1], args[2]);
|
||||
break;
|
||||
|
||||
case 'rm':
|
||||
if (!args[1]) {
|
||||
console.error('Usage: node note.ts rm <remote_file>');
|
||||
process.exit(1);
|
||||
}
|
||||
removeFile(config, args[1]);
|
||||
break;
|
||||
|
||||
case 'mv':
|
||||
if (!args[1] || !args[2]) {
|
||||
console.error('Usage: node note.ts mv <old_name> <new_name>');
|
||||
process.exit(1);
|
||||
}
|
||||
moveFile(config, args[1], args[2]);
|
||||
break;
|
||||
|
||||
case 'get':
|
||||
if (!args[1]) {
|
||||
console.error('Usage: node note.ts get <remote_file> [local_file]');
|
||||
process.exit(1);
|
||||
}
|
||||
downloadFile(config, args[1], args[2]);
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
default:
|
||||
showHelp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default not3;
|
||||
3
bun.lock
3
bun.lock
@@ -9,6 +9,7 @@
|
||||
"@babel/types": "^7.28.2",
|
||||
"@types/babel__traverse": "^7.28.0",
|
||||
"@types/minimist": "^1.2.5",
|
||||
"dedent": "^1.7.0",
|
||||
"dotenv": "^17.2.1",
|
||||
"minimist": "^1.2.8",
|
||||
},
|
||||
@@ -50,6 +51,8 @@
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.1", "", {}, "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
75
compose.yml
Normal file
75
compose.yml
Normal file
@@ -0,0 +1,75 @@
|
||||
|
||||
services:
|
||||
hipmi-docker-proxy:
|
||||
image: tecnativa/docker-socket-proxy
|
||||
container_name: hipmi-docker-proxy
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
CONTAINERS: 1
|
||||
POST: 1
|
||||
PING: 1
|
||||
networks:
|
||||
- hipmi
|
||||
hipmi-dev:
|
||||
image: bip/dev:latest
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
target: dev
|
||||
container_name: hipmi-dev
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/app:/app
|
||||
- ./data/ssh/authorized_keys:/home/bip/.ssh/authorized_keys:ro
|
||||
networks:
|
||||
- hipmi
|
||||
depends_on:
|
||||
hipmi-postgres:
|
||||
condition: service_healthy
|
||||
hipmi-prod:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
target: prod
|
||||
image: bip/prod:latest
|
||||
container_name: hipmi-prod
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/app:/app
|
||||
networks:
|
||||
- hipmi
|
||||
depends_on:
|
||||
hipmi-postgres:
|
||||
condition: service_healthy
|
||||
hipmi-postgres:
|
||||
image: postgres:16
|
||||
container_name: hipmi-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=bip
|
||||
- POSTGRES_PASSWORD=Production_123
|
||||
- POSTGRES_DB=hipmi
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
networks:
|
||||
- hipmi
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U bip -d hipmi']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
hipmi-frpc:
|
||||
image: snowdreamtech/frpc:latest
|
||||
container_name: hipmi-frpc
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data/frpc/frpc.toml:/etc/frp/frpc.toml:ro
|
||||
networks:
|
||||
- hipmi
|
||||
networks:
|
||||
hipmi:
|
||||
driver: bridge
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"version": "1.0.4",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"g3n": "./bin/g3n.ts",
|
||||
"not3": "./bin/not3.ts"
|
||||
"g3n": "./bin/g3n.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
@@ -15,6 +14,7 @@
|
||||
"@babel/types": "^7.28.2",
|
||||
"@types/babel__traverse": "^7.28.0",
|
||||
"@types/minimist": "^1.2.5",
|
||||
"dedent": "^1.7.0",
|
||||
"dotenv": "^17.2.1",
|
||||
"minimist": "^1.2.8"
|
||||
}
|
||||
|
||||
9
x.ts
9
x.ts
@@ -61,12 +61,6 @@ function loadConfig(): Config {
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: show loaded config (without showing actual token for security)
|
||||
// console.log(`🔍 Config loaded:`);
|
||||
// console.log(` TOKEN: ${config.TOKEN ? '[SET]' : '[NOT SET]'}`);
|
||||
// console.log(` REPO: ${config.REPO || '[NOT SET]'}`);
|
||||
// console.log(` URL: ${config.URL || '[NOT SET]'}`);
|
||||
|
||||
if (!config.TOKEN || !config.REPO) {
|
||||
console.error(`❌ Config invalid. Please set TOKEN=... and REPO=... inside ${CONFIG_FILE}`);
|
||||
process.exit(1);
|
||||
@@ -87,9 +81,6 @@ async function fetchWithAuth(config: Config, url: string, options: RequestInit =
|
||||
...options.headers
|
||||
};
|
||||
|
||||
// Debug: log the URL being called
|
||||
// console.log(`🔍 Calling URL: ${url}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user