Copying my other project structure to this project
44
.air.toml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 0
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata", "frontend/node_modules", "data", "bin"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html", "css", "js"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
21
.editorconfig
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
; https://editorconfig.org/
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[{Makefile,go.mod,go.sum,*.go}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
|
||||||
|
[{*.yml,*.yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
17
.env.example
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
SECRET_KEY="" # must be 32 char length, use `openssl rand -base64 32` to generate random secret
|
||||||
|
LOG_LEVEL=INFO # can be DEBUG, INFO, WARNING, ERROR
|
||||||
|
|
||||||
|
# Fiber Config
|
||||||
|
APP_DEBUG=false # if this set to true , LOG_LEVEL will be set to DEBUG
|
||||||
|
APP_PREFORK=true
|
||||||
|
APP_HOST="0.0.0.0"
|
||||||
|
APP_PORT=18090
|
||||||
|
APP_PROXY_HEADER="X-Real-Ip" # CF-Connecting-IP
|
||||||
|
APP_ALLOW_ORIGIN="http://localhost:5173,http://192.168.1.99:5173,https://ditatompel.com"
|
||||||
|
|
||||||
|
# DB settings:
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=wa_ditatombot
|
16
Makefile
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.PHONY: ui build linux64
|
||||||
|
|
||||||
|
BINARY_NAME = xmr-nodes
|
||||||
|
|
||||||
|
build: ui linux64
|
||||||
|
|
||||||
|
ui:
|
||||||
|
go generate ./...
|
||||||
|
|
||||||
|
linux64:
|
||||||
|
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o bin/${BINARY_NAME}-static-linux-amd64
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
rm -rfv ./bin
|
||||||
|
rm -rf ./frontend/build
|
80
cmd/admin.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/database"
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/repo"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
var adminCmd = &cobra.Command{
|
||||||
|
Use: "admin",
|
||||||
|
Short: "Create Admin",
|
||||||
|
Long: `Create an admin account for WebUI access.`,
|
||||||
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Println("Usage: xmr-nodes admin create")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if args[0] == "create" {
|
||||||
|
if err := database.ConnectDB(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := createAdmin(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Admin account created")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(adminCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAdmin() error {
|
||||||
|
admin := repo.NewAdminRepo(database.GetDB())
|
||||||
|
a := repo.Admin{
|
||||||
|
Username: stringPrompt("Username:"),
|
||||||
|
Password: passPrompt("Password:"),
|
||||||
|
}
|
||||||
|
_, err := admin.CreateAdmin(&a)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringPrompt(label string) string {
|
||||||
|
var s string
|
||||||
|
r := bufio.NewReader(os.Stdin)
|
||||||
|
for {
|
||||||
|
fmt.Fprint(os.Stderr, label+" ")
|
||||||
|
s, _ = r.ReadString('\n')
|
||||||
|
if s != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func passPrompt(label string) string {
|
||||||
|
var s string
|
||||||
|
for {
|
||||||
|
fmt.Fprint(os.Stderr, label+" ")
|
||||||
|
b, _ := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
s = string(b)
|
||||||
|
if s != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
return s
|
||||||
|
}
|
31
cmd/root.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/config"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AppVer = "0.0.1"
|
||||||
|
|
||||||
|
var LogLevel string
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "xmr-nodes",
|
||||||
|
Short: "XMR Nodes",
|
||||||
|
Version: AppVer,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
err := rootCmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
config.LoadAll(".env")
|
||||||
|
LogLevel = config.AppCfg().LogLevel
|
||||||
|
}
|
104
cmd/serve.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/ditatompel/xmr-nodes/frontend"
|
||||||
|
"github.com/ditatompel/xmr-nodes/handler"
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/config"
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/database"
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/repo"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/encryptcookie"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serveCmd = &cobra.Command{
|
||||||
|
Use: "serve",
|
||||||
|
Short: "Serve the WebUI",
|
||||||
|
Long: `This command will run HTTP server for APIs and WebUI.`,
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
serve()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(serveCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serve() {
|
||||||
|
appCfg := config.AppCfg()
|
||||||
|
// connect to DB
|
||||||
|
if err := database.ConnectDB(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define Fiber config & app.
|
||||||
|
app := fiber.New(fiberConfig())
|
||||||
|
|
||||||
|
// recover
|
||||||
|
app.Use(recover.New(recover.Config{EnableStackTrace: appCfg.Debug}))
|
||||||
|
|
||||||
|
// logger middleware
|
||||||
|
if appCfg.Debug {
|
||||||
|
app.Use(logger.New(logger.Config{
|
||||||
|
Format: "[${time}] ${status} - ${latency} ${method} ${path} ${queryParams} ${ip} ${ua}\n",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Use(cors.New(cors.Config{
|
||||||
|
AllowOrigins: appCfg.AllowOrigin,
|
||||||
|
AllowHeaders: "Origin, Content-Type, Accept",
|
||||||
|
AllowCredentials: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// cookie
|
||||||
|
app.Use(encryptcookie.New(encryptcookie.Config{Key: appCfg.SecretKey}))
|
||||||
|
|
||||||
|
handler.AppRoute(app)
|
||||||
|
handler.V1Api(app)
|
||||||
|
app.Use("/", filesystem.New(filesystem.Config{
|
||||||
|
Root: frontend.SvelteKitHandler(),
|
||||||
|
// NotFoundFile: "index.html",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// signal channel to capture system calls
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
|
||||||
|
|
||||||
|
// start a cleanup cron-job
|
||||||
|
if !fiber.IsChild() {
|
||||||
|
cronRepo := repo.NewCron(database.GetDB())
|
||||||
|
go cronRepo.RunCronProcess()
|
||||||
|
}
|
||||||
|
|
||||||
|
// start shutdown goroutine
|
||||||
|
go func() {
|
||||||
|
// capture sigterm and other system call here
|
||||||
|
<-sigCh
|
||||||
|
fmt.Println("Shutting down HTTP server...")
|
||||||
|
_ = app.Shutdown()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// start http server
|
||||||
|
serverAddr := fmt.Sprintf("%s:%d", appCfg.Host, appCfg.Port)
|
||||||
|
if err := app.Listen(serverAddr); err != nil {
|
||||||
|
fmt.Printf("Server is not running! error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fiberConfig() fiber.Config {
|
||||||
|
return fiber.Config{
|
||||||
|
Prefork: config.AppCfg().Prefork,
|
||||||
|
ProxyHeader: config.AppCfg().ProxyHeader,
|
||||||
|
AppName: "ditatompel's XMR Nodes HTTP server " + AppVer,
|
||||||
|
}
|
||||||
|
}
|
13
frontend/.eslintignore
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
15
frontend/.eslintrc.cjs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/** @type { import("eslint").Linter.Config } */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['eslint:recommended', 'plugin:svelte/recommended', 'prettier'],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
extraFileExtensions: ['.svelte']
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
}
|
||||||
|
};
|
10
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
1
frontend/.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
4
frontend/.prettierignore
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
8
frontend/.prettierrc
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
35
frontend/README.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# UI
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
after running `npm run build` from development device, copy `./build`, `package.json` and `package-lock.json` to server. On the server, run `npm ci --omit dev` then restart the systemd service.
|
||||||
|
|
||||||
|
Playbook example (run from root project):
|
||||||
|
```shell
|
||||||
|
ansible-playbook -i ./utils/ansible/inventory.ini -l production ./utils/ansible/deploy.yml -K
|
||||||
|
```
|
21
frontend/embed.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package frontend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate npm i
|
||||||
|
//go:generate npm run build
|
||||||
|
//go:embed all:build/*
|
||||||
|
var f embed.FS
|
||||||
|
|
||||||
|
func SvelteKitHandler() http.FileSystem {
|
||||||
|
build, err := fs.Sub(f, "build")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return http.FS(build)
|
||||||
|
}
|
18
frontend/jsconfig.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
4070
frontend/package-lock.json
generated
Normal file
40
frontend/package.json
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "xmr-nodes-frontend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "VITE_API_URL=http://127.0.0.1:18901 vite dev --host 127.0.0.1",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.6.3",
|
||||||
|
"@skeletonlabs/skeleton": "^2.9.0",
|
||||||
|
"@skeletonlabs/tw-plugin": "^0.3.1",
|
||||||
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.1",
|
||||||
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
|
"@types/eslint": "^8.56.0",
|
||||||
|
"@vincjo/datatables": "^1.14.5",
|
||||||
|
"autoprefixer": "^10.4.17",
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-svelte": "^2.35.1",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"prettier": "^3.1.1",
|
||||||
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
|
"svelte": "^4.2.7",
|
||||||
|
"svelte-check": "^3.6.0",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^5.0.3"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
6
frontend/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
13
frontend/src/app.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
@tailwind variants;
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card {
|
||||||
|
@apply bg-surface-50-900-token rounded-lg border-2 border-dashed border-gray-200 p-4 dark:border-gray-700;
|
||||||
|
}
|
16
frontend/src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
VITE_API_URL: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
13
frontend/src/app.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="false" data-theme="skeleton">
|
||||||
|
<div style="display: contents" class="h-full">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
50
frontend/src/lib/components/InfiniteScroll.svelte
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<script>
|
||||||
|
import { onDestroy, createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let threshold = 0;
|
||||||
|
export let horizontal = false;
|
||||||
|
export let hasMore = true;
|
||||||
|
/** @type {any} */
|
||||||
|
let elementScroll;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
let isLoadMore = false;
|
||||||
|
/** @type {any} */
|
||||||
|
let component;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (component || elementScroll) {
|
||||||
|
const element = elementScroll ? elementScroll : component.parentNode;
|
||||||
|
|
||||||
|
element.addEventListener('scroll', onScroll);
|
||||||
|
element.addEventListener('resize', onScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {any} e */
|
||||||
|
const onScroll = (e) => {
|
||||||
|
const offset = horizontal
|
||||||
|
? e.target.scrollWidth - e.target.clientWidth - e.target.scrollLeft
|
||||||
|
: e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop;
|
||||||
|
|
||||||
|
if (offset <= threshold) {
|
||||||
|
if (!isLoadMore && hasMore) {
|
||||||
|
dispatch('loadMore');
|
||||||
|
}
|
||||||
|
isLoadMore = true;
|
||||||
|
} else {
|
||||||
|
isLoadMore = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (component || elementScroll) {
|
||||||
|
const element = elementScroll ? elementScroll : component.parentNode;
|
||||||
|
|
||||||
|
element.removeEventListener('scroll', null);
|
||||||
|
element.removeEventListener('resize', null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={component} class="w-0" />
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||||
|
|
||||||
|
type T = $$Generic<Row>;
|
||||||
|
|
||||||
|
export let handler: DataHandler<T>;
|
||||||
|
|
||||||
|
const pageNumber = handler.getPageNumber();
|
||||||
|
const pageCount = handler.getPageCount();
|
||||||
|
const pages = handler.getPages({ ellipsis: true });
|
||||||
|
|
||||||
|
const setPage = (value: 'previous' | 'next' | number) => {
|
||||||
|
handler.setPage(value);
|
||||||
|
handler.invalidate();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class={$$props.class ?? ''}>
|
||||||
|
{#if $pages === undefined}
|
||||||
|
<button type="button" class="sm-btn" on:click={() => setPage('previous')}> ❮ </button>
|
||||||
|
<button class="mx-4">page <b>{$pageNumber}</b></button>
|
||||||
|
<button type="button" class="sm-btn" on:click={() => setPage('next')}>❯</button>
|
||||||
|
{:else}
|
||||||
|
<div class="lg:hidden">
|
||||||
|
<button type="button" class="sm-btn" on:click={() => setPage('previous')}> ❮ </button>
|
||||||
|
<button class="mx-4">page <b>{$pageNumber}</b></button>
|
||||||
|
<button
|
||||||
|
class="sm-btn"
|
||||||
|
class:disabled={$pageNumber === $pageCount}
|
||||||
|
on:click={() => setPage('next')}
|
||||||
|
>
|
||||||
|
❯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group variant-ghost-surface hidden lg:block">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="hover:variant-soft-secondary"
|
||||||
|
class:disabled={$pageNumber === 1}
|
||||||
|
on:click={() => setPage('previous')}>❮</button
|
||||||
|
>
|
||||||
|
{#each $pages as page}<button
|
||||||
|
type="button"
|
||||||
|
class="hover:variant-filled-secondary"
|
||||||
|
class:!variant-filled-primary={$pageNumber === page}
|
||||||
|
class:ellipse={page === null}
|
||||||
|
on:click={() => setPage(page)}>{page ?? '...'}</button
|
||||||
|
>{/each}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="hover:variant-soft-secondary"
|
||||||
|
class:disabled={$pageNumber === $pageCount}
|
||||||
|
on:click={() => setPage('next')}
|
||||||
|
>
|
||||||
|
❯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.sm-btn {
|
||||||
|
@apply btn btn-sm variant-ghost-surface hover:variant-soft-secondary;
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
@apply cursor-not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||||
|
|
||||||
|
type T = $$Generic<Row>;
|
||||||
|
|
||||||
|
export let handler: DataHandler<T>;
|
||||||
|
const rowCount = handler.getRowCount();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $rowCount === undefined}
|
||||||
|
<div />
|
||||||
|
{:else}
|
||||||
|
<div class={$$props.class ?? 'mr-6 leading-8 lg:leading-10'}>
|
||||||
|
{#if $rowCount.total > 0}
|
||||||
|
<b>{$rowCount.start}</b>
|
||||||
|
- <b>{$rowCount.end}</b>
|
||||||
|
/ <b>{$rowCount.total}</b>
|
||||||
|
{:else}
|
||||||
|
No entries found
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||||
|
|
||||||
|
type T = $$Generic<Row>;
|
||||||
|
|
||||||
|
export let handler: DataHandler<T>;
|
||||||
|
export let options = [5, 10, 20, 50, 100];
|
||||||
|
export let labelId = 'rowsPerPage';
|
||||||
|
|
||||||
|
const rowsPerPage = handler.getRowsPerPage();
|
||||||
|
|
||||||
|
const setRowsPerPage = () => {
|
||||||
|
handler.setPage(1);
|
||||||
|
handler.invalidate();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex place-items-center">
|
||||||
|
<label for={labelId}>Show</label>
|
||||||
|
<select
|
||||||
|
class="select ml-2"
|
||||||
|
id={labelId}
|
||||||
|
name="rowsPerPage"
|
||||||
|
bind:value={$rowsPerPage}
|
||||||
|
on:change={setRowsPerPage}
|
||||||
|
>
|
||||||
|
{#each options as option}
|
||||||
|
<option value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DataHandler } from '@vincjo/datatables/remote';
|
||||||
|
export let handler: DataHandler;
|
||||||
|
let value: string;
|
||||||
|
let timeout: any;
|
||||||
|
|
||||||
|
const search = () => {
|
||||||
|
handler.search(value);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
handler.invalidate();
|
||||||
|
}, 400);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="input-variant-secondary input w-36 sm:w-64"
|
||||||
|
type="search"
|
||||||
|
name="tableGlobalSearch"
|
||||||
|
placeholder="Search..."
|
||||||
|
bind:value
|
||||||
|
on:input={search}
|
||||||
|
/>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||||
|
|
||||||
|
type T = $$Generic<Row>;
|
||||||
|
|
||||||
|
export let handler: DataHandler<T>;
|
||||||
|
export let filterBy: keyof T;
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
export let placeholder: string = 'Filter';
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
export let colspan: number = 1;
|
||||||
|
|
||||||
|
let value: string = '';
|
||||||
|
let timeout: any;
|
||||||
|
|
||||||
|
const filter = () => {
|
||||||
|
handler.filter(value, filterBy);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
handler.invalidate();
|
||||||
|
}, 400);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<th {colspan}>
|
||||||
|
<input
|
||||||
|
class="input variant-form-material h-8 w-full text-sm"
|
||||||
|
type="text"
|
||||||
|
{placeholder}
|
||||||
|
bind:value
|
||||||
|
on:input={filter}
|
||||||
|
/>
|
||||||
|
</th>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||||
|
type T = $$Generic<Row>;
|
||||||
|
|
||||||
|
export let handler: DataHandler<T>;
|
||||||
|
export let orderBy: keyof T;
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
handler.sort(orderBy);
|
||||||
|
handler.invalidate();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<th on:click={update} class="cursor-pointer select-none p-2 px-5">
|
||||||
|
<div class="flex h-full items-center justify-start gap-x-2">
|
||||||
|
<slot /> ↕️
|
||||||
|
</div>
|
||||||
|
</th>
|
6
frontend/src/lib/components/datatables/server/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export { default as DtSrPagination } from './DtSrPagination.svelte';
|
||||||
|
export { default as DtSrRowCount } from './DtSrRowCount.svelte';
|
||||||
|
export { default as DtSrRowsPerPage } from './DtSrRowsPerPage.svelte';
|
||||||
|
export { default as DtSrSearch } from './DtSrSearch.svelte';
|
||||||
|
export { default as DtSrThFilter } from './DtSrThFilter.svelte';
|
||||||
|
export { default as DtSrThSort } from './DtSrThSort.svelte';
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { navs } from './navs';
|
||||||
|
import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||||
|
|
||||||
|
const drawerStore = getDrawerStore();
|
||||||
|
|
||||||
|
function drawerClose() {
|
||||||
|
drawerStore.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: classesActive = (/** @type {string} */ href) =>
|
||||||
|
$page.url.pathname.startsWith(href) ? 'bg-primary-500' : '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="list-nav p-4">
|
||||||
|
<ul>
|
||||||
|
{#each navs as nav}
|
||||||
|
<li>
|
||||||
|
<a href={nav.path} class={classesActive(nav.path)} on:click={drawerClose}>{nav.name}</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
85
frontend/src/lib/components/navigation/AdminNav.svelte
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<script>
|
||||||
|
import { invalidateAll, goto } from '$app/navigation';
|
||||||
|
import { LightSwitch, getDrawerStore } from '@skeletonlabs/skeleton';
|
||||||
|
import { apiUri } from '$lib/utils/common';
|
||||||
|
|
||||||
|
const drawerStore = getDrawerStore();
|
||||||
|
function drawerOpen() {
|
||||||
|
drawerStore.open({});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef formResult
|
||||||
|
* @type {object}
|
||||||
|
* @property {string} status
|
||||||
|
* @property {string} message
|
||||||
|
* @property {null | object} data
|
||||||
|
*/
|
||||||
|
/** @type {formResult} */
|
||||||
|
let formResult;
|
||||||
|
|
||||||
|
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
|
||||||
|
async function handleLogout(event) {
|
||||||
|
const data = new FormData(event.currentTarget);
|
||||||
|
|
||||||
|
const response = await fetch(event.currentTarget.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(Object.fromEntries(data))
|
||||||
|
});
|
||||||
|
|
||||||
|
formResult = await response.json();
|
||||||
|
|
||||||
|
if (formResult.status === 'ok') {
|
||||||
|
// rerun all `load` functions, following the successful update
|
||||||
|
await invalidateAll();
|
||||||
|
goto('/login/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="bg-surface-100-800-token fixed top-0 z-30 w-full shadow-2xl">
|
||||||
|
<div class="px-3 py-2 lg:px-5 lg:pl-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center justify-start rtl:justify-end">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm inline-flex items-center md:hidden"
|
||||||
|
aria-label="Mobile Drawer Button"
|
||||||
|
on:click={drawerOpen}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<svg viewBox="0 0 100 80" class="fill-token h-4 w-4">
|
||||||
|
<rect width="100" height="20" />
|
||||||
|
<rect y="30" width="100" height="20" />
|
||||||
|
<rect y="60" width="100" height="20" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="/app/dashboard/" class="ms-2 flex md:me-24" aria-label="title">
|
||||||
|
<span class="hidden self-center whitespace-nowrap text-2xl font-semibold lg:block"
|
||||||
|
>XMR Nodes</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="ms-3 flex items-center space-x-4">
|
||||||
|
<LightSwitch />
|
||||||
|
<form
|
||||||
|
action={apiUri('/auth/logout')}
|
||||||
|
method="POST"
|
||||||
|
on:submit|preventDefault={handleLogout}
|
||||||
|
>
|
||||||
|
<input type="hidden" name="logout" value="logout" />
|
||||||
|
<button type="submit" class="btn btn-sm variant-filled-error" role="menuitem">
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
34
frontend/src/lib/components/navigation/AdminSidebar.svelte
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { navs } from './navs';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
id="logo-sidebar"
|
||||||
|
class="bg-surface-100-800-token fixed left-0 top-0 z-20 h-screen w-64 -translate-x-full pt-20 shadow-2xl transition-transform sm:translate-x-0"
|
||||||
|
aria-label="Sidebar"
|
||||||
|
>
|
||||||
|
<div class="h-full overflow-y-auto px-3 pb-4">
|
||||||
|
<ul class="space-y-2 font-medium list-none" data-sveltekit-preload-data="false">
|
||||||
|
{#each navs as nav}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={nav.path}
|
||||||
|
class={$page.url.pathname.startsWith(nav.path) ? 'active' : 'nav-link'}
|
||||||
|
>
|
||||||
|
<span class="ms-3">{nav.name}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.active {
|
||||||
|
@apply flex items-center rounded-lg bg-primary-500 p-2;
|
||||||
|
}
|
||||||
|
.nav-link {
|
||||||
|
@apply flex items-center rounded-lg p-2 hover:bg-secondary-500 hover:text-white;
|
||||||
|
}
|
||||||
|
</style>
|
3
frontend/src/lib/components/navigation/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as AdminNav } from './AdminNav.svelte';
|
||||||
|
export { default as AdminSidebar } from './AdminSidebar.svelte';
|
||||||
|
export { default as AdminMobileDrawer } from './AdminMobileDrawer.svelte';
|
5
frontend/src/lib/components/navigation/navs.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const navs = [
|
||||||
|
{ name: 'Dashboard', path: '/app/dashboard/' },
|
||||||
|
{ name: 'Prober', path: '/app/prober/' },
|
||||||
|
{ name: 'Crons', path: '/app/crons/' }
|
||||||
|
];
|
1
frontend/src/lib/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
4
frontend/src/lib/utils/common.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/** @param {string} path */
|
||||||
|
export const apiUri = (path) => {
|
||||||
|
return `${import.meta.env.VITE_API_URL || ''}${path}`;
|
||||||
|
};
|
21
frontend/src/routes/(loggedin)/+layout.svelte
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<script>
|
||||||
|
import { Toast, Drawer, Modal } from '@skeletonlabs/skeleton';
|
||||||
|
import { AdminNav, AdminSidebar, AdminMobileDrawer } from '$lib/components/navigation';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Toast />
|
||||||
|
<Modal />
|
||||||
|
|
||||||
|
<Drawer>
|
||||||
|
<h2 class="p-4">Navigation</h2>
|
||||||
|
<hr />
|
||||||
|
<AdminMobileDrawer />
|
||||||
|
<hr />
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<AdminNav />
|
||||||
|
<AdminSidebar />
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-gray-100/80 p-4 pt-14 dark:bg-gray-900/80 sm:ml-64">
|
||||||
|
<slot />
|
||||||
|
</div>
|
174
frontend/src/routes/(loggedin)/app/crons/+page.svelte
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
<script>
|
||||||
|
import { DataHandler } from '@vincjo/datatables/remote';
|
||||||
|
import { format, formatDistance } from 'date-fns';
|
||||||
|
import { loadData } from './api-handler';
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { DtSrThSort, DtSrThFilter, DtSrRowCount } from '$lib/components/datatables/server';
|
||||||
|
|
||||||
|
const handler = new DataHandler([], { rowsPerPage: 1000, totalRows: 0 });
|
||||||
|
let rows = handler.getRows();
|
||||||
|
|
||||||
|
const reloadData = () => {
|
||||||
|
handler.invalidate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {string | number} */
|
||||||
|
let filterState = -1;
|
||||||
|
/** @type {string | number} */
|
||||||
|
let filterEnabled = -1;
|
||||||
|
|
||||||
|
/** @type {number | undefined} */
|
||||||
|
let intervalId;
|
||||||
|
let intervalValue = 0;
|
||||||
|
|
||||||
|
const intervalOptions = [
|
||||||
|
{ value: 0, label: 'No' },
|
||||||
|
{ value: 5, label: '5s' },
|
||||||
|
{ value: 10, label: '10s' },
|
||||||
|
{ value: 30, label: '30s' },
|
||||||
|
{ value: 60, label: '1m' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const startInterval = () => {
|
||||||
|
const seconds = intervalValue;
|
||||||
|
if (isNaN(seconds) || seconds < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intervalOptions.some((option) => option.value === seconds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > 0) {
|
||||||
|
reloadData();
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
reloadData();
|
||||||
|
}, seconds * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: startInterval(); // Automatically start the interval on change
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(intervalId); // Clear the interval when the component is destroyed
|
||||||
|
});
|
||||||
|
onMount(() => {
|
||||||
|
handler.onChange((state) => loadData(state));
|
||||||
|
handler.invalidate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h1 class="h2 font-extrabold dark:text-white">Crons</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="invisible flex place-items-center md:visible">
|
||||||
|
<label for="autoRefreshInterval">Auto Refresh:</label>
|
||||||
|
<select
|
||||||
|
class="select ml-2"
|
||||||
|
id="autoRefreshInterval"
|
||||||
|
bind:value={intervalValue}
|
||||||
|
on:change={startInterval}
|
||||||
|
>
|
||||||
|
{#each intervalOptions as { value, label }}
|
||||||
|
<option {value}>{label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex place-items-center">
|
||||||
|
<button
|
||||||
|
id="reloadDt"
|
||||||
|
name="reloadDt"
|
||||||
|
class="variant-filled-primary btn"
|
||||||
|
on:click={reloadData}>Reload</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-2 overflow-x-auto">
|
||||||
|
<table class="table table-hover table-compact w-full table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<DtSrThSort {handler} orderBy="id">ID</DtSrThSort>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Slug</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<DtSrThSort {handler} orderBy="run_every">Run Every</DtSrThSort>
|
||||||
|
<DtSrThSort {handler} orderBy="last_run">Last Run</DtSrThSort>
|
||||||
|
<DtSrThSort {handler} orderBy="next_run">Next Run</DtSrThSort>
|
||||||
|
<DtSrThSort {handler} orderBy="run_time">Run Time</DtSrThSort>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Enabled</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<DtSrThFilter {handler} filterBy="title" placeholder="Title" colspan={3} />
|
||||||
|
<DtSrThFilter {handler} filterBy="description" placeholder="Description" colspan={5} />
|
||||||
|
<th>
|
||||||
|
<select
|
||||||
|
id="fState"
|
||||||
|
name="fState"
|
||||||
|
class="select variant-form-material"
|
||||||
|
bind:value={filterState}
|
||||||
|
on:change={() => {
|
||||||
|
handler.filter(filterState, 'cron_state');
|
||||||
|
reloadData();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={-1}>Any</option>
|
||||||
|
<option value={1}>Running</option>
|
||||||
|
<option value={0}>Idle</option>
|
||||||
|
</select>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<select
|
||||||
|
id="fEnabled"
|
||||||
|
name="fEnabled"
|
||||||
|
class="select variant-form-material"
|
||||||
|
bind:value={filterEnabled}
|
||||||
|
on:change={() => {
|
||||||
|
handler.filter(filterEnabled, 'is_enabled');
|
||||||
|
reloadData();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={-1}>Any</option>
|
||||||
|
<option value={1}>Yes</option>
|
||||||
|
<option value={0}>No</option>
|
||||||
|
</select>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each $rows as row (row.id)}
|
||||||
|
<tr>
|
||||||
|
<td>{row.id}</td>
|
||||||
|
<td>{row.title}</td>
|
||||||
|
<td>{row.slug}</td>
|
||||||
|
<td>{row.description}</td>
|
||||||
|
<td>{row.run_every}s</td>
|
||||||
|
<td>
|
||||||
|
{format(row.last_run * 1000, 'PP HH:mm')}<br />
|
||||||
|
{formatDistance(row.last_run * 1000, new Date(), { addSuffix: true })}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{format(row.next_run * 1000, 'PP HH:mm')}<br />
|
||||||
|
{formatDistance(row.next_run * 1000, new Date(), { addSuffix: true })}
|
||||||
|
</td>
|
||||||
|
<td>{row.run_time}</td>
|
||||||
|
<td>{row.cron_state ? 'RUNNING' : 'IDLE'}</td>
|
||||||
|
<td>{row.is_enabled ? 'ENABLED' : 'DISABLED'}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between mb-2">
|
||||||
|
<DtSrRowCount {handler} />
|
||||||
|
</div>
|
||||||
|
</div>
|
21
frontend/src/routes/(loggedin)/app/crons/api-handler.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { apiUri } from '$lib/utils/common';
|
||||||
|
|
||||||
|
/** @param {import('@vincjo/datatables/remote/state')} state */
|
||||||
|
export const loadData = async (state) => {
|
||||||
|
const response = await fetch(apiUri(`/api/v1/crons?${getParams(state)}`));
|
||||||
|
const json = await response.json();
|
||||||
|
state.setTotalRows(json.data.length ?? 0);
|
||||||
|
return json.data ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
|
||||||
|
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
||||||
|
|
||||||
|
if (sort) {
|
||||||
|
params += `&orderBy=${sort.orderBy}&orderDir=${sort.direction}`;
|
||||||
|
}
|
||||||
|
if (filters) {
|
||||||
|
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="mb-4">
|
||||||
|
<h1 class="h2 font-extrabold dark:text-white">Dashboard</h1>
|
||||||
|
</div>
|
120
frontend/src/routes/(loggedin)/app/prober/+page.svelte
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<script>
|
||||||
|
import { DataHandler } from '@vincjo/datatables/remote';
|
||||||
|
import { format, formatDistance } from 'date-fns';
|
||||||
|
import { loadData } from './api-handler';
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { DtSrThSort, DtSrThFilter, DtSrRowCount } from '$lib/components/datatables/server';
|
||||||
|
|
||||||
|
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
|
||||||
|
let rows = handler.getRows();
|
||||||
|
|
||||||
|
const reloadData = () => {
|
||||||
|
handler.invalidate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {number | undefined} */
|
||||||
|
let intervalId;
|
||||||
|
let intervalValue = 0;
|
||||||
|
|
||||||
|
const intervalOptions = [
|
||||||
|
{ value: 0, label: 'No' },
|
||||||
|
{ value: 5, label: '5s' },
|
||||||
|
{ value: 10, label: '10s' },
|
||||||
|
{ value: 30, label: '30s' },
|
||||||
|
{ value: 60, label: '1m' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const startInterval = () => {
|
||||||
|
const seconds = intervalValue;
|
||||||
|
if (isNaN(seconds) || seconds < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intervalOptions.some((option) => option.value === seconds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > 0) {
|
||||||
|
reloadData();
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
reloadData();
|
||||||
|
}, seconds * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: startInterval(); // Automatically start the interval on change
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(intervalId); // Clear the interval when the component is destroyed
|
||||||
|
});
|
||||||
|
onMount(() => {
|
||||||
|
handler.onChange((state) => loadData(state));
|
||||||
|
handler.invalidate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h1 class="h2 font-extrabold dark:text-white">Prober</h1>
|
||||||
|
<a class="variant-filled-success btn btn-sm mb-4" href="/app/prober/add">Add Prober</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="invisible flex place-items-center md:visible">
|
||||||
|
<label for="autoRefreshInterval">Auto Refresh:</label>
|
||||||
|
<select
|
||||||
|
class="select ml-2"
|
||||||
|
id="autoRefreshInterval"
|
||||||
|
bind:value={intervalValue}
|
||||||
|
on:change={startInterval}
|
||||||
|
>
|
||||||
|
{#each intervalOptions as { value, label }}
|
||||||
|
<option {value}>{label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex place-items-center">
|
||||||
|
<button id="reloadDt" name="reloadDt" class="variant-filled-primary btn" on:click={reloadData}
|
||||||
|
>Reload</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-2 overflow-x-auto">
|
||||||
|
<table class="table table-hover table-compact w-full table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<DtSrThSort {handler} orderBy="id">ID</DtSrThSort>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>API Key</th>
|
||||||
|
<DtSrThSort {handler} orderBy="last_submit_ts">Last Submit</DtSrThSort>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<DtSrThFilter {handler} filterBy="name" placeholder="Name" colspan={2} />
|
||||||
|
<DtSrThFilter {handler} filterBy="api_key" placeholder="API Key" colspan={2} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each $rows as row (row.id)}
|
||||||
|
<tr>
|
||||||
|
<td>{row.id}</td>
|
||||||
|
<td>{row.name}</td>
|
||||||
|
<td>{row.api_key}</td>
|
||||||
|
<td>
|
||||||
|
{format(row.last_submit_ts * 1000, 'PP HH:mm')}<br />
|
||||||
|
{formatDistance(row.last_submit_ts * 1000, new Date(), { addSuffix: true })}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between mb-2">
|
||||||
|
<DtSrRowCount {handler} />
|
||||||
|
</div>
|
||||||
|
</div>
|
94
frontend/src/routes/(loggedin)/app/prober/add/+page.svelte
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<script>
|
||||||
|
import { invalidateAll, goto } from '$app/navigation';
|
||||||
|
import { apiUri } from '$lib/utils/common';
|
||||||
|
import { ProgressBar } from '@skeletonlabs/skeleton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef formResult
|
||||||
|
* @type {object}
|
||||||
|
* @property {string} status
|
||||||
|
* @property {string} message
|
||||||
|
* @property {null | object} data
|
||||||
|
*/
|
||||||
|
/** @type {formResult} */
|
||||||
|
export let formResult;
|
||||||
|
|
||||||
|
let isProcessing = false;
|
||||||
|
|
||||||
|
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
|
||||||
|
async function handleSubmit(event) {
|
||||||
|
isProcessing = true;
|
||||||
|
const data = new FormData(event.currentTarget);
|
||||||
|
|
||||||
|
const response = await fetch(event.currentTarget.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(Object.fromEntries(data))
|
||||||
|
});
|
||||||
|
|
||||||
|
formResult = await response.json();
|
||||||
|
isProcessing = false;
|
||||||
|
|
||||||
|
if (formResult.status === 'ok') {
|
||||||
|
// rerun all `load` functions, following the successful update
|
||||||
|
await invalidateAll();
|
||||||
|
goto('/app/prober/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h1 class="h2 font-extrabold dark:text-white">Add Prober</h1>
|
||||||
|
</div>
|
||||||
|
{#if !isProcessing}
|
||||||
|
{#if formResult?.status === 'error'}
|
||||||
|
<div class="p-4 mb-4 text-sm rounded-lg bg-gray-700 text-red-400" role="alert">
|
||||||
|
<span class="font-medium">Error:</span>
|
||||||
|
{formResult.message}!
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if formResult?.status === 'ok'}
|
||||||
|
<div class="p-4 mb-4 text-sm rounded-lg bg-gray-700 text-green-400" role="alert">
|
||||||
|
<span class="font-medium">Success:</span>
|
||||||
|
{formResult.message}!
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<ProgressBar meter="bg-secondary-500" track="bg-secondary-500/30" value={undefined} />
|
||||||
|
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-blue-400" role="alert">
|
||||||
|
<span class="font-medium">Processing...</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<form
|
||||||
|
class="space-y-4 md:space-y-6"
|
||||||
|
action={apiUri('/api/v1/prober')}
|
||||||
|
method="POST"
|
||||||
|
on:submit|preventDefault={handleSubmit}
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label for="name" class="label">
|
||||||
|
<span>Name</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
placeholder="Prober name"
|
||||||
|
autocomplete="off"
|
||||||
|
class="input variant-form-material"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full rounded-lg bg-primary-600 px-5 py-2.5 text-center text-sm font-medium hover:bg-primary-700"
|
||||||
|
>Submit</button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</div>
|
21
frontend/src/routes/(loggedin)/app/prober/api-handler.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { apiUri } from '$lib/utils/common';
|
||||||
|
|
||||||
|
/** @param {import('@vincjo/datatables/remote/state')} state */
|
||||||
|
export const loadData = async (state) => {
|
||||||
|
const response = await fetch(apiUri(`/api/v1/prober?${getParams(state)}`));
|
||||||
|
const json = await response.json();
|
||||||
|
state.setTotalRows(json.data.length ?? 0);
|
||||||
|
return json.data.items ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
|
||||||
|
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
||||||
|
|
||||||
|
if (sort) {
|
||||||
|
params += `&orderBy=${sort.orderBy}&orderDir=${sort.direction}`;
|
||||||
|
}
|
||||||
|
if (filters) {
|
||||||
|
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
};
|
2
frontend/src/routes/+layout.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const prerender = true
|
||||||
|
export const trailingSlash = 'always';
|
34
frontend/src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<script>
|
||||||
|
// import { base } from '$app/paths';
|
||||||
|
import '../app.css';
|
||||||
|
import { beforeNavigate, afterNavigate } from '$app/navigation';
|
||||||
|
import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom'
|
||||||
|
import {
|
||||||
|
ProgressBar,
|
||||||
|
initializeStores,
|
||||||
|
storePopup // PopUps
|
||||||
|
} from '@skeletonlabs/skeleton';
|
||||||
|
|
||||||
|
initializeStores();
|
||||||
|
|
||||||
|
// popups
|
||||||
|
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
|
||||||
|
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
|
// progress bar show
|
||||||
|
beforeNavigate(() => (isLoading = true));
|
||||||
|
|
||||||
|
afterNavigate((/* params */) => {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{#if isLoading}
|
||||||
|
<ProgressBar
|
||||||
|
class="fixed top-0 z-50"
|
||||||
|
height="h-1"
|
||||||
|
track="bg-opacity-100"
|
||||||
|
meter="bg-gradient-to-br from-purple-600 via-pink-600 to-blue-600"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<slot />
|
7
frontend/src/routes/+page.svelte
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<h1 class="h1">( . ) ( . )</h1>
|
||||||
|
<p>WAT?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
118
frontend/src/routes/login/+page.svelte
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<script>
|
||||||
|
import { invalidateAll, goto } from '$app/navigation';
|
||||||
|
import { apiUri } from '$lib/utils/common';
|
||||||
|
import { ProgressBar, LightSwitch } from '@skeletonlabs/skeleton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef formResult
|
||||||
|
* @type {object}
|
||||||
|
* @property {string} status
|
||||||
|
* @property {string} message
|
||||||
|
* @property {null | object} data
|
||||||
|
*/
|
||||||
|
/** @type {formResult} */
|
||||||
|
export let formResult;
|
||||||
|
|
||||||
|
let isProcessing = false;
|
||||||
|
|
||||||
|
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
|
||||||
|
async function handleSubmit(event) {
|
||||||
|
isProcessing = true;
|
||||||
|
const data = new FormData(event.currentTarget);
|
||||||
|
|
||||||
|
const response = await fetch(event.currentTarget.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(Object.fromEntries(data))
|
||||||
|
});
|
||||||
|
|
||||||
|
formResult = await response.json();
|
||||||
|
isProcessing = false;
|
||||||
|
|
||||||
|
if (formResult.status === 'ok') {
|
||||||
|
// rerun all `load` functions, following the successful update
|
||||||
|
await invalidateAll();
|
||||||
|
goto('/app/dashboard/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||||
|
<a href="/" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white"
|
||||||
|
>XMR Nodes</a
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full rounded-lg shadow border md:mt-0 sm:max-w-md xl:p-0 bg-white border-gray-700 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||||
|
<h1
|
||||||
|
class="text-xl font-bold leading-tight tracking-tight tmd:text-2xl text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Sign in to your account
|
||||||
|
</h1>
|
||||||
|
<form
|
||||||
|
class="space-y-4 md:space-y-6"
|
||||||
|
action={apiUri('/auth/login')}
|
||||||
|
method="POST"
|
||||||
|
on:submit|preventDefault={handleSubmit}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="username"
|
||||||
|
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
class="input"
|
||||||
|
placeholder="username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="password"
|
||||||
|
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
class="input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn variant-filled-primary w-full">Sign in</button>
|
||||||
|
|
||||||
|
<LightSwitch />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !isProcessing}
|
||||||
|
{#if formResult?.status === 'error'}
|
||||||
|
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-red-400" role="alert">
|
||||||
|
<span class="font-medium">Error:</span>
|
||||||
|
{formResult.message}!
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if formResult?.status === 'ok'}
|
||||||
|
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-green-400" role="alert">
|
||||||
|
<span class="font-medium">Success:</span>
|
||||||
|
{formResult.message}!
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<ProgressBar meter="bg-secondary-500" track="bg-secondary-500/30" value={undefined} />
|
||||||
|
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-blue-400" role="alert">
|
||||||
|
<span class="font-medium">Processing...</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
BIN
frontend/static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
frontend/static/favicon.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
frontend/static/img/icon/android-icon-144x144.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
frontend/static/img/icon/android-icon-192x192.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
frontend/static/img/icon/android-icon-36x36.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/static/img/icon/android-icon-48x48.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
frontend/static/img/icon/android-icon-512x512.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
frontend/static/img/icon/android-icon-72x72.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
frontend/static/img/icon/android-icon-96x96.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
frontend/static/img/icon/apple-icon-180x180.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
frontend/static/img/icon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
frontend/static/img/icon/maskable-512x512.png
Normal file
After Width: | Height: | Size: 19 KiB |
63
frontend/static/manifest.json
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
{
|
||||||
|
"name": "xmr.ditatompel.com",
|
||||||
|
"short_name": "xmr-nodes",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#272b31",
|
||||||
|
"theme_color": "#272b31",
|
||||||
|
"description": "WA Bot UI",
|
||||||
|
"icons": [{
|
||||||
|
"src": "/img/icon/android-icon-36x36.png",
|
||||||
|
"sizes": "36x36",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any",
|
||||||
|
"density": "0.75"
|
||||||
|
}, {
|
||||||
|
"src": "/img/icon/android-icon-48x48.png",
|
||||||
|
"sizes": "48x48",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any",
|
||||||
|
"density": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/icon/android-icon-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any",
|
||||||
|
"density": "1.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/icon/android-icon-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any",
|
||||||
|
"density": "2.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/icon/android-icon-144x144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any",
|
||||||
|
"density": "3.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/icon/android-icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any",
|
||||||
|
"density": "4.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/icon/android-icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any",
|
||||||
|
"density": "4.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/icon/maskable-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}]
|
||||||
|
}
|
2
frontend/static/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
16
frontend/svelte.config.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
kit: {
|
||||||
|
// paths: {
|
||||||
|
// base: '/'
|
||||||
|
// },
|
||||||
|
// trailingSlash: 'always',
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
23
frontend/tailwind.config.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
import { skeleton } from '@skeletonlabs/tw-plugin';
|
||||||
|
import forms from '@tailwindcss/forms';
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
darkMode: 'class',
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{html,js,svelte,ts}',
|
||||||
|
join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
forms,
|
||||||
|
skeleton({
|
||||||
|
themes: {
|
||||||
|
preset: ['skeleton']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
BIN
frontend/tmp/main
Normal file
9
frontend/vite.config.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// @ts-check
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
sveltekit(),
|
||||||
|
]
|
||||||
|
});
|
31
go.mod
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
module github.com/ditatompel/xmr-nodes
|
||||||
|
|
||||||
|
go 1.22.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexedwards/argon2id v1.0.0
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.4
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/spf13/cobra v1.8.0
|
||||||
|
golang.org/x/term v0.19.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/crypto v0.21.0 // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
|
)
|
93
go.sum
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||||
|
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||||
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||||
|
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
|
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
|
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||||
|
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
18
handler/middlewares.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CookieProtected(c *fiber.Ctx) error {
|
||||||
|
cookie := c.Cookies("xmr-nodes-ui")
|
||||||
|
if cookie == "" {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Unauthorized",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
132
handler/response.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/database"
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/repo"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Login(c *fiber.Ctx) error {
|
||||||
|
payload := repo.Admin{}
|
||||||
|
if err := c.BodyParser(&payload); err != nil {
|
||||||
|
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
|
||||||
|
"status": "error",
|
||||||
|
"message": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := repo.NewAdminRepo(database.GetDB())
|
||||||
|
res, err := repo.Login(payload.Username, payload.Password)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"status": "error",
|
||||||
|
"message": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
token := fmt.Sprintf("auth_%d_%d", res.Id, time.Now().Unix())
|
||||||
|
c.Cookie(&fiber.Cookie{
|
||||||
|
Name: "xmr-nodes-ui",
|
||||||
|
Value: token,
|
||||||
|
Expires: time.Now().Add(time.Hour * 24),
|
||||||
|
HTTPOnly: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Logged in",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Logout(c *fiber.Ctx) error {
|
||||||
|
c.Cookie(&fiber.Cookie{
|
||||||
|
Name: "xmr-nodes-ui",
|
||||||
|
Value: "",
|
||||||
|
Expires: time.Now(),
|
||||||
|
HTTPOnly: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Logged out",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Prober(c *fiber.Ctx) error {
|
||||||
|
proberRepo := repo.NewProberRepo(database.GetDB())
|
||||||
|
|
||||||
|
if c.Method() == "POST" {
|
||||||
|
payload := repo.Prober{}
|
||||||
|
if err := c.BodyParser(&payload); err != nil {
|
||||||
|
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
|
||||||
|
"status": "error",
|
||||||
|
"message": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if payload.Name == "" {
|
||||||
|
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Please fill prober name",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err := proberRepo.AddProber(payload.Name)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"status": "error",
|
||||||
|
"message": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := repo.ProbersQueryParams{
|
||||||
|
RowsPerPage: c.QueryInt("limit", 10),
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Name: c.Query("name"),
|
||||||
|
ApiKey: c.Query("api_key"),
|
||||||
|
}
|
||||||
|
|
||||||
|
prober, err := proberRepo.Probers(query)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"status": "error",
|
||||||
|
"message": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Success",
|
||||||
|
"data": prober,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Crons(c *fiber.Ctx) error {
|
||||||
|
cronRepo := repo.NewCron(database.GetDB())
|
||||||
|
|
||||||
|
crons, err := cronRepo.Crons()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"status": "error",
|
||||||
|
"message": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Crons",
|
||||||
|
"data": crons,
|
||||||
|
})
|
||||||
|
}
|
18
handler/routes.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AppRoute(app *fiber.App) {
|
||||||
|
app.Post("/auth/login", Login)
|
||||||
|
app.Post("/auth/logout", Logout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func V1Api(app *fiber.App) {
|
||||||
|
v1 := app.Group("/api/v1")
|
||||||
|
|
||||||
|
v1.Get("/prober", Prober)
|
||||||
|
v1.Post("/prober", Prober)
|
||||||
|
v1.Get("/crons", Crons)
|
||||||
|
}
|
41
internal/config/app.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
Debug bool
|
||||||
|
Prefork bool
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
ProxyHeader string
|
||||||
|
AllowOrigin string
|
||||||
|
SecretKey string
|
||||||
|
LogLevel string
|
||||||
|
}
|
||||||
|
|
||||||
|
var app = &App{}
|
||||||
|
|
||||||
|
func AppCfg() *App {
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadApp loads App configuration
|
||||||
|
func LoadApp() {
|
||||||
|
app.Host = os.Getenv("APP_HOST")
|
||||||
|
app.Port, _ = strconv.Atoi(os.Getenv("APP_PORT"))
|
||||||
|
app.Debug, _ = strconv.ParseBool(os.Getenv("APP_DEBUG"))
|
||||||
|
app.Prefork, _ = strconv.ParseBool(os.Getenv("APP_PREFORK"))
|
||||||
|
app.ProxyHeader = os.Getenv("APP_PROXY_HEADER")
|
||||||
|
app.AllowOrigin = os.Getenv("APP_ALLOW_ORIGIN")
|
||||||
|
app.SecretKey = os.Getenv("SECRET_KEY")
|
||||||
|
app.LogLevel = os.Getenv("LOG_LEVEL")
|
||||||
|
if app.LogLevel == "" {
|
||||||
|
app.LogLevel = "INFO"
|
||||||
|
}
|
||||||
|
if app.Debug {
|
||||||
|
app.LogLevel = "DEBUG"
|
||||||
|
}
|
||||||
|
}
|
18
internal/config/config.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadAllConfigs set various configs
|
||||||
|
func LoadAll(envFile string) {
|
||||||
|
err := godotenv.Load(envFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can't load .env file. error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadApp()
|
||||||
|
LoadDBCfg()
|
||||||
|
}
|
31
internal/config/db.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB holds the DB configuration
|
||||||
|
type DB struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Name string
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
var db = &DB{}
|
||||||
|
|
||||||
|
// DBCfg returns the default DB configuration
|
||||||
|
func DBCfg() *DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadDBCfg loads DB configuration
|
||||||
|
func LoadDBCfg() {
|
||||||
|
db.Host = os.Getenv("DB_HOST")
|
||||||
|
db.Port, _ = strconv.Atoi(os.Getenv("DB_PORT"))
|
||||||
|
db.User = os.Getenv("DB_USER")
|
||||||
|
db.Password = os.Getenv("DB_PASSWORD")
|
||||||
|
db.Name = os.Getenv("DB_NAME")
|
||||||
|
}
|
50
internal/database/mysql.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/config"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB holds the database
|
||||||
|
type DB struct{ *sqlx.DB }
|
||||||
|
|
||||||
|
// database instance
|
||||||
|
var defaultDB = &DB{}
|
||||||
|
|
||||||
|
// connect sets the db client of database using configuration
|
||||||
|
func (db *DB) connect(cfg *config.DB) (err error) {
|
||||||
|
dbURI := fmt.Sprintf("%s:%s@(%s:%d)/%s",
|
||||||
|
cfg.User,
|
||||||
|
cfg.Password,
|
||||||
|
cfg.Host,
|
||||||
|
cfg.Port,
|
||||||
|
cfg.Name,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.DB, err = sqlx.Connect("mysql", dbURI)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to ping database.
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
defer db.Close() // close database connection
|
||||||
|
return fmt.Errorf("can't sent ping to database, %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB returns db instance
|
||||||
|
func GetDB() *DB {
|
||||||
|
return defaultDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectDB sets the db client of database using default configuration
|
||||||
|
func ConnectDB() error {
|
||||||
|
return defaultDB.connect(config.DBCfg())
|
||||||
|
}
|
169
internal/repo/admin.go
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/database"
|
||||||
|
|
||||||
|
"github.com/alexedwards/argon2id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Admin struct {
|
||||||
|
Id int `db:"id"`
|
||||||
|
Username string `db:"username"`
|
||||||
|
Password string `db:"password"`
|
||||||
|
LastactiveTs int64 `db:"lastactive_ts"`
|
||||||
|
CreatedTs int64 `db:"created_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminRepo struct {
|
||||||
|
db *database.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminRepository interface {
|
||||||
|
CreateAdmin(*Admin) (*Admin, error)
|
||||||
|
Login(username string, password string) (*Admin, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminRepo(db *database.DB) AdminRepository {
|
||||||
|
return &AdminRepo{db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *AdminRepo) CreateAdmin(admin *Admin) (*Admin, error) {
|
||||||
|
if !validUsername(admin.Username) {
|
||||||
|
return nil, errors.New("username is not valid, must be at least 4 characters long and contain only lowercase letters and numbers")
|
||||||
|
}
|
||||||
|
if !strongPassword(admin.Password) {
|
||||||
|
return nil, errors.New("password is not strong enough, must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number and one special character")
|
||||||
|
}
|
||||||
|
hash, err := setPassword(admin.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
admin.Password = hash
|
||||||
|
|
||||||
|
admin.CreatedTs = time.Now().Unix()
|
||||||
|
|
||||||
|
if repo.isUsernameExists(admin.Username) {
|
||||||
|
return nil, errors.New("username already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO tbl_admin (username, password, created_ts) VALUES (?, ?, ?)`
|
||||||
|
_, err = repo.db.Exec(query, admin.Username, admin.Password, admin.CreatedTs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return admin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *AdminRepo) Login(username, password string) (*Admin, error) {
|
||||||
|
query := `SELECT id, username, password FROM tbl_admin WHERE username = ? LIMIT 1`
|
||||||
|
row, err := repo.db.Query(query, username)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer row.Close()
|
||||||
|
|
||||||
|
admin := Admin{}
|
||||||
|
if row.Next() {
|
||||||
|
err = row.Scan(&admin.Id, &admin.Username, &admin.Password)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("Invalid username or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
match, err := checkPassword(admin.Password, password)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
return nil, errors.New("Invalid username or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
update := `UPDATE tbl_admin SET lastactive_ts = ? WHERE id = ?`
|
||||||
|
_, err = repo.db.Exec(update, time.Now().Unix(), admin.Id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &admin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *AdminRepo) isUsernameExists(username string) bool {
|
||||||
|
query := `SELECT id FROM tbl_admin WHERE username = ? LIMIT 1`
|
||||||
|
row, err := repo.db.Query(query, username)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer row.Close()
|
||||||
|
return row.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPassword(password string) (string, error) {
|
||||||
|
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPassword(hash, password string) (bool, error) {
|
||||||
|
match, err := argon2id.ComparePasswordAndHash(password, hash)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func strongPassword(password string) bool {
|
||||||
|
if len(password) < 8 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.ContainsAny(password, "0123456789") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.ContainsAny(password, "abcdefghijklmnopqrstuvwxyz") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.ContainsAny(password, "!@#$%^&*()_+|~-=`{}[]:;<>?,./") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// No special character and unicode for username
|
||||||
|
func validUsername(username string) bool {
|
||||||
|
if len(username) < 5 || len(username) > 20 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// reject witespace, tabs, newlines, and other special characters
|
||||||
|
if strings.ContainsAny(username, " \t\n") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// reject unicode
|
||||||
|
if strings.ContainsAny(username, "^\x00-\x7F") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// reject special characters
|
||||||
|
if strings.ContainsAny(username, "!@#$%^&*()_+|~-=`{}[]:;<>?,./ ") { // note last blank space
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.ContainsAny(username, "abcdefghijklmnopqrstuvwxyz0123456789") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
112
internal/repo/cron.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CronRepository interface {
|
||||||
|
RunCronProcess()
|
||||||
|
Crons() ([]CronTask, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronRepo struct {
|
||||||
|
db *database.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronTask struct {
|
||||||
|
Id int `json:"id" db:"id"`
|
||||||
|
Title string `json:"title" db:"title"`
|
||||||
|
Slug string `json:"slug" db:"slug"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
RunEvery int `json:"run_every" db:"run_every"`
|
||||||
|
LastRun int64 `json:"last_run" db:"last_run"`
|
||||||
|
NextRun int64 `json:"next_run" db:"next_run"`
|
||||||
|
RunTime float64 `json:"run_time" db:"run_time"`
|
||||||
|
CronState int `json:"cron_state" db:"cron_state"`
|
||||||
|
IsEnabled int `json:"is_enabled" db:"is_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var rerunTimeout = 300
|
||||||
|
|
||||||
|
func NewCron(db *database.DB) CronRepository {
|
||||||
|
return &CronRepo{db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CronRepo) RunCronProcess() {
|
||||||
|
for {
|
||||||
|
time.Sleep(60 * time.Second)
|
||||||
|
fmt.Println("Running cron...")
|
||||||
|
list, err := repo.queueList()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error parsing to struct:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, task := range list {
|
||||||
|
startTime := time.Now()
|
||||||
|
currentTs := startTime.Unix()
|
||||||
|
delayedTask := currentTs - task.NextRun
|
||||||
|
if task.CronState == 1 && delayedTask <= int64(rerunTimeout) {
|
||||||
|
fmt.Println("SKIP STATE 1:", task.Slug)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.preRunTask(task.Id, currentTs)
|
||||||
|
|
||||||
|
repo.execCron(task.Slug)
|
||||||
|
|
||||||
|
runTime := math.Ceil(time.Since(startTime).Seconds()*1000) / 1000
|
||||||
|
fmt.Println("Runtime:", runTime)
|
||||||
|
nextRun := currentTs + int64(task.RunEvery)
|
||||||
|
|
||||||
|
repo.postRunTask(task.Id, nextRun, runTime)
|
||||||
|
}
|
||||||
|
fmt.Println("Cron done!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CronRepo) Crons() ([]CronTask, error) {
|
||||||
|
tasks := []CronTask{}
|
||||||
|
query := `SELECT * FROM tbl_cron`
|
||||||
|
err := repo.db.Select(&tasks, query)
|
||||||
|
return tasks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CronRepo) queueList() ([]CronTask, error) {
|
||||||
|
tasks := []CronTask{}
|
||||||
|
query := `SELECT id, run_every, last_run, slug, next_run, cron_state FROM tbl_cron
|
||||||
|
WHERE is_enabled = ? AND next_run <= ?`
|
||||||
|
err := repo.db.Select(&tasks, query, 1, time.Now().Unix())
|
||||||
|
|
||||||
|
return tasks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CronRepo) preRunTask(id int, lastRunTs int64) {
|
||||||
|
query := `UPDATE tbl_cron SET cron_state = ?, last_run = ? WHERE id = ?`
|
||||||
|
row, err := repo.db.Query(query, 1, lastRunTs, id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("ERROR PRERUN:", err)
|
||||||
|
}
|
||||||
|
defer row.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CronRepo) postRunTask(id int, nextRun int64, runtime float64) {
|
||||||
|
query := `UPDATE tbl_cron SET cron_state = ?, next_run = ?, run_time = ? WHERE id = ?`
|
||||||
|
row, err := repo.db.Query(query, 0, nextRun, runtime, id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("ERROR PRERUN:", err)
|
||||||
|
}
|
||||||
|
defer row.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CronRepo) execCron(slug string) {
|
||||||
|
switch slug {
|
||||||
|
case "something":
|
||||||
|
fmt.Println("Running task", slug)
|
||||||
|
// do task
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
104
internal/repo/prober.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ditatompel/xmr-nodes/internal/database"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProberRepository interface {
|
||||||
|
AddProber(name string) error
|
||||||
|
Probers(q ProbersQueryParams) (Probers, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProberRepo struct {
|
||||||
|
db *database.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type Prober struct {
|
||||||
|
Id int64 `json:"id" db:"id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
ApiKey uuid.UUID `json:"api_key" db:"api_key"`
|
||||||
|
LastSubmitTs int64 `json:"last_submit_ts" db:"last_submit_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProbersQueryParams struct {
|
||||||
|
Name string
|
||||||
|
ApiKey string
|
||||||
|
RowsPerPage int
|
||||||
|
Page int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Probers struct {
|
||||||
|
TotalRows int `json:"total_rows"`
|
||||||
|
RowsPerPage int `json:"rows_per_page"`
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
NextPage int `json:"next_page"`
|
||||||
|
Items []*Prober `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProberRepo(db *database.DB) ProberRepository {
|
||||||
|
return &ProberRepo{db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *ProberRepo) AddProber(name string) error {
|
||||||
|
query := `INSERT INTO tbl_prober (name, api_key, last_submit_ts) VALUES (?, ?, ?)`
|
||||||
|
_, err := repo.db.Exec(query, name, uuid.New(), 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *ProberRepo) Probers(q ProbersQueryParams) (Probers, error) {
|
||||||
|
queryParams := []interface{}{}
|
||||||
|
whereQueries := []string{}
|
||||||
|
where := ""
|
||||||
|
|
||||||
|
if q.Name != "" {
|
||||||
|
whereQueries = append(whereQueries, "name LIKE ?")
|
||||||
|
queryParams = append(queryParams, "%"+q.Name+"%")
|
||||||
|
}
|
||||||
|
if q.ApiKey != "" {
|
||||||
|
whereQueries = append(whereQueries, "api_key LIKE ?")
|
||||||
|
queryParams = append(queryParams, "%"+q.ApiKey+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(whereQueries) > 0 {
|
||||||
|
where = "WHERE " + strings.Join(whereQueries, " AND ")
|
||||||
|
}
|
||||||
|
|
||||||
|
probers := Probers{}
|
||||||
|
queryTotalRows := fmt.Sprintf("SELECT COUNT(id) AS total_rows FROM tbl_prober %s", where)
|
||||||
|
|
||||||
|
err := repo.db.QueryRow(queryTotalRows, queryParams...).Scan(&probers.TotalRows)
|
||||||
|
if err != nil {
|
||||||
|
return probers, err
|
||||||
|
}
|
||||||
|
queryParams = append(queryParams, q.RowsPerPage, (q.Page-1)*q.RowsPerPage)
|
||||||
|
|
||||||
|
query := fmt.Sprintf("SELECT id, name, api_key, last_submit_ts FROM tbl_prober %s ORDER BY id DESC LIMIT ? OFFSET ?", where)
|
||||||
|
|
||||||
|
row, err := repo.db.Query(query, queryParams...)
|
||||||
|
if err != nil {
|
||||||
|
return probers, err
|
||||||
|
}
|
||||||
|
defer row.Close()
|
||||||
|
|
||||||
|
probers.RowsPerPage = q.RowsPerPage
|
||||||
|
probers.CurrentPage = q.Page
|
||||||
|
probers.NextPage = q.Page + 1
|
||||||
|
|
||||||
|
for row.Next() {
|
||||||
|
prober := Prober{}
|
||||||
|
err = row.Scan(&prober.Id, &prober.Name, &prober.ApiKey, &prober.LastSubmitTs)
|
||||||
|
if err != nil {
|
||||||
|
return probers, err
|
||||||
|
}
|
||||||
|
probers.Items = append(probers.Items, &prober)
|
||||||
|
}
|
||||||
|
return probers, nil
|
||||||
|
}
|
7
main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/ditatompel/xmr-nodes/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
14
tools/mysql-dump.sh
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Dump local dev database structure and required data from specific tables.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# shellcheck disable=SC2046 # Ignore SC2046: Quote this to prevent word splitting.
|
||||||
|
|
||||||
|
SD=$(dirname "$(readlink -f -- "$0")")
|
||||||
|
cd "$SD" || exit 1 && cd ".." || exit 1
|
||||||
|
|
||||||
|
## Structure only dump
|
||||||
|
mariadb-dump --no-data --skip-comments xmr_nodes | \
|
||||||
|
sed 's/ AUTO_INCREMENT=[0-9]*//g' > \
|
||||||
|
"./tools/resources/database/structure.sql"
|
||||||
|
|
||||||
|
# vim: set ts=4 sw=4 tw=0 et ft=sh:
|
63
tools/resources/database/structure.sql
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||||
|
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||||
|
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||||
|
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||||
|
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||||
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
DROP TABLE IF EXISTS `tbl_admin`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8 */;
|
||||||
|
CREATE TABLE `tbl_admin` (
|
||||||
|
`id` bigint(30) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(200) NOT NULL,
|
||||||
|
`password` varchar(200) NOT NULL,
|
||||||
|
`lastactive_ts` int(11) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`created_ts` int(11) unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `username` (`username`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
DROP TABLE IF EXISTS `tbl_cron`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8 */;
|
||||||
|
CREATE TABLE `tbl_cron` (
|
||||||
|
`id` int(8) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` varchar(200) NOT NULL DEFAULT '',
|
||||||
|
`slug` varchar(200) NOT NULL DEFAULT '',
|
||||||
|
`description` varchar(200) DEFAULT NULL,
|
||||||
|
`run_every` int(8) unsigned NOT NULL DEFAULT 60 COMMENT 'in seconds',
|
||||||
|
`last_run` bigint(20) unsigned DEFAULT NULL,
|
||||||
|
`next_run` bigint(20) unsigned DEFAULT NULL,
|
||||||
|
`run_time` float(7,3) unsigned NOT NULL DEFAULT 0.000,
|
||||||
|
`cron_state` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`is_enabled` int(1) unsigned NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
DROP TABLE IF EXISTS `tbl_prober`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8 */;
|
||||||
|
CREATE TABLE `tbl_prober` (
|
||||||
|
`id` int(9) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(255) NOT NULL,
|
||||||
|
`api_key` varchar(36) NOT NULL,
|
||||||
|
`last_submit_ts` int(11) unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `api_key` (`api_key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||||
|
|
||||||
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
|
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
|