mirror of
https://github.com/Alexander-D-Karpov/webring.git
synced 2026-03-16 22:07:41 +03:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65d4d3c3fc | ||
| 12fdd0ba64 | |||
| f774fa993f | |||
| d4607434d4 | |||
| 7ed6a23b63 | |||
|
|
d324c9490d | ||
| 499c4a807b | |||
| bd346671b8 | |||
| e98dd6e64b | |||
| 6bd5393c0c | |||
| d605bd37b7 | |||
| 3110c00e71 | |||
| 5d14f42352 | |||
| 0acd6686cc | |||
| 2c1885e6de | |||
| aba05aa6b6 | |||
| 73347edc67 | |||
| c1eed46f6d | |||
| e1bc69205b | |||
| 90c129d606 | |||
| be8340b8a9 | |||
| ba3474e214 | |||
| 1fcb4e35f7 | |||
| 14c6e6c4d3 | |||
| 34cb69b047 | |||
|
|
bf62b8f427 | ||
| 341e12e611 | |||
| 3133c15253 | |||
| f12e106049 | |||
| 68e160f325 | |||
| d2ff9246f9 | |||
| d370489d9e | |||
| ebb576c2a2 |
44
.air.toml
Normal file
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 ./cmd/server"
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata", "docs"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_root = 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
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
PORT=8000
|
PORT=8080
|
||||||
DB_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/webring?sslmode=disable
|
DB_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/webring?sslmode=disable
|
||||||
DASHBOARD_USER=admin
|
DASHBOARD_USER=admin
|
||||||
DASHBOARD_PASSWORD=admin
|
DASHBOARD_PASSWORD=admin
|
||||||
CONTACT_LINK=mailto:webring@example.com
|
CONTACT_LINK=mailto:webring@example.com
|
||||||
|
TELEGRAM_BOT_TOKEN=your_bot_token
|
||||||
|
TELEGRAM_BOT_USERNAME=your_bot_username
|
||||||
|
SESSION_TTL_HOURS=2160 # 90 days
|
||||||
|
SESSION_SECURE_COOKIE=true # Set to true if using HTTPS, false for HTTP
|
||||||
|
CSRF_AUTH_KEY=your_csrf_auth_key
|
||||||
|
CSRF_TRUSTED_ORIGINS=
|
||||||
|
REQUIRE_LOGIN_FOR_SUBMIT=false
|
||||||
|
CHECKER_WORKERS=5
|
||||||
|
CHECKER_DOWN_THRESHOLD=3
|
||||||
|
MESSAGES_DIR=messages
|
||||||
35
.gitguardian.yml
Normal file
35
.gitguardian.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
secret_scanning:
|
||||||
|
# Paths to exclude from scanning
|
||||||
|
ignored_paths:
|
||||||
|
- '**/.env.template'
|
||||||
|
- '**/.env.example'
|
||||||
|
- '**/testdata/**'
|
||||||
|
- '**/test/**'
|
||||||
|
- '**/*_test.go'
|
||||||
|
- 'docs/**'
|
||||||
|
|
||||||
|
# Specific detectors to ignore
|
||||||
|
ignored_detectors:
|
||||||
|
- generic_high_entropy_secret
|
||||||
|
|
||||||
|
ignored_patterns:
|
||||||
|
- name: "Template environment variables"
|
||||||
|
pattern: 'your_bot_token|your_bot_username|example\.com'
|
||||||
|
|
||||||
|
- name: "Localhost database strings"
|
||||||
|
pattern: 'postgres://postgres:postgres@localhost'
|
||||||
|
|
||||||
|
- name: "Test credentials"
|
||||||
|
pattern: 'postgres|postgres|test_.*'
|
||||||
|
|
||||||
|
additional_config:
|
||||||
|
high_entropy_threshold: 4.5
|
||||||
|
ignored_matches:
|
||||||
|
- match: 'TELEGRAM_BOT_TOKEN=your_bot_token'
|
||||||
|
reason: "Template placeholder"
|
||||||
|
- match: 'TELEGRAM_BOT_USERNAME=your_bot_username'
|
||||||
|
reason: "Template placeholder"
|
||||||
|
- match: 'postgres://postgres:postgres@localhost'
|
||||||
|
reason: "Local development database"
|
||||||
271
.github/workflows/go.yml
vendored
271
.github/workflows/go.yml
vendored
File diff suppressed because it is too large
Load Diff
83
.golangci.yml
Normal file
83
.golangci.yml
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
version: "2"
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
issues-exit-code: 1
|
||||||
|
tests: true
|
||||||
|
|
||||||
|
output:
|
||||||
|
formats:
|
||||||
|
text:
|
||||||
|
path: stdout
|
||||||
|
print-issued-lines: true
|
||||||
|
print-linter-name: true
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
settings:
|
||||||
|
gofmt:
|
||||||
|
simplify: true
|
||||||
|
goimports:
|
||||||
|
local-prefixes:
|
||||||
|
- webring
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- copyloopvar
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- exhaustive
|
||||||
|
- gochecknoinits
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nolintlint
|
||||||
|
- rowserrcheck
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- whitespace
|
||||||
|
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: true
|
||||||
|
check-blank: true
|
||||||
|
|
||||||
|
goconst:
|
||||||
|
min-len: 3
|
||||||
|
min-occurrences: 10
|
||||||
|
|
||||||
|
gocritic:
|
||||||
|
enabled-tags: [diagnostic, experimental, opinionated, performance, style]
|
||||||
|
|
||||||
|
govet:
|
||||||
|
enable: [shadow]
|
||||||
|
disable: [fieldalignment]
|
||||||
|
|
||||||
|
lll:
|
||||||
|
line-length: 120
|
||||||
|
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
|
||||||
|
nakedret:
|
||||||
|
max-func-lines: 30
|
||||||
|
|
||||||
|
prealloc:
|
||||||
|
simple: true
|
||||||
|
range-loops: true
|
||||||
|
for-loops: false
|
||||||
|
|
||||||
|
unparam:
|
||||||
|
check-exported: false
|
||||||
46
Dockerfile
Normal file
46
Dockerfile
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Build stage
|
||||||
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
|
# Install git and ca-certificates
|
||||||
|
RUN apk add --no-cache git ca-certificates tzdata
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy go mod files
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o webring cmd/server/main.go
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install ca-certificates for HTTPS requests
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
# Copy the binary from builder stage
|
||||||
|
COPY --from=builder /build/webring .
|
||||||
|
COPY --from=builder /build/docs ./docs
|
||||||
|
COPY --from=builder /build/messages ./messages
|
||||||
|
|
||||||
|
# Create media directory
|
||||||
|
RUN mkdir -p media
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Set default environment variables
|
||||||
|
ENV PORT=8080
|
||||||
|
ENV MEDIA_FOLDER=media
|
||||||
|
|
||||||
|
# Run the binary
|
||||||
|
CMD ["./webring"]
|
||||||
46
README.md
46
README.md
|
|
@ -5,8 +5,12 @@ This project is a webring relay service built with Go. It manages a list of webs
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Dashboard for managing websites in the webring
|
- Dashboard for managing websites in the webring
|
||||||
- Automatic uptime checking of websites
|
- Automatic uptime checking of websites (with proxy support)
|
||||||
- API endpoints for navigating the webring
|
- API endpoints for navigating the webring
|
||||||
|
- Telegram authentication and user management
|
||||||
|
- Site submission and update request workflow with admin approval
|
||||||
|
- Telegram notifications for status changes, submissions and approvals
|
||||||
|
- Customizable notification messages via template files
|
||||||
- Basic authentication for the dashboard
|
- Basic authentication for the dashboard
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
@ -17,7 +21,6 @@ This project is a webring relay service built with Go. It manages a list of webs
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
edit .env to set correct path to database
|
edit .env to set correct path to database
|
||||||
|
|
||||||
```
|
```
|
||||||
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
|
@ -26,30 +29,51 @@ make migrate-up
|
||||||
```
|
```
|
||||||
|
|
||||||
## Local Run
|
## Local Run
|
||||||
|
|
||||||
```
|
```
|
||||||
go run cmd/server/main.go
|
go run cmd/server/main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
or download prebuild version
|
or download prebuild version
|
||||||
|
|
||||||
```
|
```
|
||||||
wget https://github.com/Alexander-D-Karpov/webring/releases/latest/download/webring
|
wget https://github.com/Alexander-D-Karpov/webring/releases/latest/download/webring
|
||||||
chmod +x webring
|
chmod +x webring
|
||||||
./webring
|
./webring
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Customizing Notification Messages
|
||||||
|
|
||||||
|
Telegram notification templates live in the `messages/` directory (configurable via `MESSAGES_DIR` env var).
|
||||||
|
Each file is plain text with Go template syntax and MarkdownV2 formatting.
|
||||||
|
|
||||||
|
To customize a message, edit the corresponding `.txt` file.
|
||||||
|
|
||||||
|
Available templates:
|
||||||
|
|
||||||
|
| File | Event |
|
||||||
|
|-----------------------------|---------------------------------------------|
|
||||||
|
| `new_request_create.txt` | Admin notification: new site submitted |
|
||||||
|
| `new_request_update.txt` | Admin notification: site update requested |
|
||||||
|
| `approved_create.txt` | User notification: site submission approved |
|
||||||
|
| `approved_update.txt` | User notification: site update approved |
|
||||||
|
| `declined_create.txt` | User notification: site submission declined |
|
||||||
|
| `declined_update.txt` | User notification: site update declined |
|
||||||
|
| `admin_approved_create.txt` | Other admins: site creation approved |
|
||||||
|
| `admin_approved_update.txt` | Other admins: site update approved |
|
||||||
|
| `admin_declined_create.txt` | Other admins: site creation declined |
|
||||||
|
| `admin_declined_update.txt` | Other admins: site update declined |
|
||||||
|
| `site_online.txt` | Owner notification: site back online |
|
||||||
|
| `site_offline.txt` | Owner notification: site went offline |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- Access the dashboard at `http://localhost:8080/dashboard` (use the credentials set in your `.env` file)
|
- Access the dashboard at `http://localhost:8080/dashboard` (use the credentials set in your `.env` file)
|
||||||
- API endpoints:
|
- API endpoints:
|
||||||
- Next site: `GET /{id}/next/`
|
- Next site: `GET /{slug}/next/data`
|
||||||
- Previous site: `GET /{id}/prev/`
|
- Previous site: `GET /{slug}/prev/data`
|
||||||
- Random site: `GET /{id}/random/`
|
- Random site: `GET /{slug}/random/data`
|
||||||
- Full data for a site: `GET /{id}/data`
|
- Full data for a site: `GET /{slug}/data`
|
||||||
- Redirect endpoints:
|
- Redirect endpoints:
|
||||||
- Next site: `GET /{id}/next`
|
- Visit site: `GET /{slug}`
|
||||||
- Previous site: `GET /{id}/prev`
|
- Next site: `GET /{slug}/next`
|
||||||
- Random site: `GET /{id}/random`
|
- Previous site: `GET /{slug}/prev`
|
||||||
|
- Random site: `GET /{slug}/random`
|
||||||
File diff suppressed because it is too large
Load Diff
30
docs/index.html
Normal file
30
docs/index.html
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Webring API Documentation</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
|
||||||
|
<style>
|
||||||
|
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
|
||||||
|
*, *:before, *:after { box-sizing: inherit; }
|
||||||
|
body { margin:0; background: #fafafa; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
url: '/api/docs/swagger.json',
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ],
|
||||||
|
plugins: [ SwaggerUIBundle.plugins.DownloadUrl ],
|
||||||
|
layout: "StandaloneLayout"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -4,5 +4,5 @@ import (
|
||||||
"embed"
|
"embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed static internal/dashboard/templates internal/public/templates
|
//go:embed static internal/dashboard/templates internal/public/templates internal/user/templates docs
|
||||||
var Files embed.FS
|
var Files embed.FS
|
||||||
|
|
|
||||||
61
flake.lock
Normal file
61
flake.lock
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1762844143,
|
||||||
|
"narHash": "sha256-SlybxLZ1/e4T2lb1czEtWVzDCVSTvk9WLwGhmxFmBxI=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "9da7f1cf7f8a6e2a7cb3001b048546c92a8258b4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
44
flake.nix
Normal file
44
flake.nix
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
description = "webring";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
webring = pkgs.callPackage ./nix/package.nix {};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
go
|
||||||
|
postgresql
|
||||||
|
gnumake
|
||||||
|
go-migrate.overrideAttrs(oldAttrs: {
|
||||||
|
tags = ["postgres"];
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
${pkgs.go}/bin/go mod tidy
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
apps.default = { type = "app"; program = "${webring}/bin/webring-server"; };
|
||||||
|
apps.webring = self.apps.${system}.default;
|
||||||
|
|
||||||
|
packages = {
|
||||||
|
inherit webring;
|
||||||
|
default = webring;
|
||||||
|
};
|
||||||
|
}) // {
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
webring = prev.callPackage ./nix/package.nix {};
|
||||||
|
};
|
||||||
|
nixosModules.default = { imports = [./nix/module.nix]; };
|
||||||
|
};
|
||||||
|
}
|
||||||
12
go.mod
12
go.mod
|
|
@ -1,15 +1,17 @@
|
||||||
module webring
|
module webring
|
||||||
|
|
||||||
go 1.22.4
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.9.2
|
github.com/PuerkitoBio/goquery v1.11.0
|
||||||
|
github.com/gorilla/csrf v1.7.3
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.11.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
golang.org/x/net v0.24.0 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
|
golang.org/x/net v0.50.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
48
go.sum
48
go.sum
|
|
@ -1,28 +1,59 @@
|
||||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0=
|
||||||
|
github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||||
|
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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-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.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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.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/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
@ -30,17 +61,34 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.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.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.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.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/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
725
internal/api/handlers_test.go
Normal file
725
internal/api/handlers_test.go
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -4,22 +4,18 @@ import "net/http"
|
||||||
|
|
||||||
func CORSMiddleware(next http.Handler) http.Handler {
|
func CORSMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Allow all origins
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
// Allow common HTTP methods
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
|
||||||
// Allow common HTTP headers
|
allowedHeaders := "Accept, Content-Type, Content-Length, Accept-Encoding, " +
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
"X-CSRF-Token, Authorization"
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", allowedHeaders)
|
||||||
|
|
||||||
// Handle preflight requests
|
|
||||||
if r.Method == "OPTIONS" {
|
if r.Method == "OPTIONS" {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the next handler
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
339
internal/api/swagger.go
Normal file
339
internal/api/swagger.go
Normal file
File diff suppressed because it is too large
Load Diff
135
internal/auth/session.go
Normal file
135
internal/auth/session.go
Normal file
File diff suppressed because it is too large
Load Diff
75
internal/auth/telegram.go
Normal file
75
internal/auth/telegram.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TelegramUser struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
PhotoURL string `json:"photo_url,omitempty"`
|
||||||
|
AuthDate int64 `json:"auth_date"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyTelegramAuth(values url.Values, botToken string) (*TelegramUser, error) {
|
||||||
|
hash := values.Get("hash")
|
||||||
|
if hash == "" {
|
||||||
|
return nil, fmt.Errorf("missing hash parameter")
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataStrings []string
|
||||||
|
for key, value := range values {
|
||||||
|
if key != "hash" && len(value) > 0 {
|
||||||
|
dataStrings = append(dataStrings, fmt.Sprintf("%s=%s", key, value[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(dataStrings)
|
||||||
|
dataString := strings.Join(dataStrings, "\n")
|
||||||
|
|
||||||
|
// Create secret key
|
||||||
|
secretKey := sha256.Sum256([]byte(botToken))
|
||||||
|
|
||||||
|
// Create HMAC
|
||||||
|
h := hmac.New(sha256.New, secretKey[:])
|
||||||
|
h.Write([]byte(dataString))
|
||||||
|
expectedHash := hex.EncodeToString(h.Sum(nil))
|
||||||
|
|
||||||
|
if hash != expectedHash {
|
||||||
|
return nil, fmt.Errorf("invalid hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse user data
|
||||||
|
id, err := strconv.ParseInt(values.Get("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid id")
|
||||||
|
}
|
||||||
|
|
||||||
|
authDate, err := strconv.ParseInt(values.Get("auth_date"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid auth_date")
|
||||||
|
}
|
||||||
|
if time.Since(time.Unix(authDate, 0)) > 24*time.Hour {
|
||||||
|
return nil, fmt.Errorf("stale login payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TelegramUser{
|
||||||
|
ID: id,
|
||||||
|
FirstName: values.Get("first_name"),
|
||||||
|
LastName: values.Get("last_name"),
|
||||||
|
Username: values.Get("username"),
|
||||||
|
PhotoURL: values.Get("photo_url"),
|
||||||
|
AuthDate: authDate,
|
||||||
|
Hash: hash,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -2,8 +2,9 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
_ "github.com/lib/pq"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq" // PostgreSQL driver
|
||||||
)
|
)
|
||||||
|
|
||||||
func Connect() (*sql.DB, error) {
|
func Connect() (*sql.DB, error) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,16 +1,21 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
type Site struct {
|
type Site struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Slug string `json:"slug"`
|
||||||
URL string `json:"url"`
|
Name string `json:"name"`
|
||||||
IsUp bool `json:"is_up"`
|
URL string `json:"url"`
|
||||||
LastCheck float64 `json:"last_check"`
|
IsUp bool `json:"is_up"`
|
||||||
Favicon *string `json:"favicon"`
|
Enabled bool `json:"enabled"`
|
||||||
|
LastCheck float64 `json:"last_check"`
|
||||||
|
Favicon *string `json:"favicon"`
|
||||||
|
UserID *int `json:"user_id"`
|
||||||
|
User *User `json:"user,omitempty"`
|
||||||
|
TelegramUsername *string `json:"telegram_username,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublicSite struct {
|
type PublicSite struct {
|
||||||
ID int `json:"id"`
|
Slug string `json:"slug"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Favicon *string `json:"favicon"`
|
Favicon *string `json:"favicon"`
|
||||||
|
|
|
||||||
31
internal/models/user.go
Normal file
31
internal/models/user.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
TelegramID int64 `json:"telegram_id"`
|
||||||
|
TelegramUsername *string `json:"telegram_username"`
|
||||||
|
FirstName *string `json:"first_name"`
|
||||||
|
LastName *string `json:"last_name"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateRequest struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
SiteID *int `json:"site_id"`
|
||||||
|
RequestType string `json:"request_type"`
|
||||||
|
ChangedFields map[string]interface{} `json:"changed_fields"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
User *User `json:"user,omitempty"`
|
||||||
|
Site *Site `json:"site,omitempty"`
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,6 +15,39 @@
|
||||||
<i class="ri-bubble-chart-fill"></i>
|
<i class="ri-bubble-chart-fill"></i>
|
||||||
Webring Listing
|
Webring Listing
|
||||||
</h1>
|
</h1>
|
||||||
|
{{if .User}}
|
||||||
|
<div class="user-bar">
|
||||||
|
<span>Welcome, {{if .User.FirstName}}{{.User.FirstName}}{{else}}{{.User.TelegramUsername}}{{end}}!</span>
|
||||||
|
<div class="user-actions">
|
||||||
|
{{if .User.IsAdmin}}
|
||||||
|
<a href="/admin" class="user-action admin">
|
||||||
|
<i class="ri-settings-line"></i>
|
||||||
|
Manage Sites
|
||||||
|
</a>
|
||||||
|
<a href="/admin/requests" class="user-action admin">
|
||||||
|
<i class="ri-shield-user-line"></i>
|
||||||
|
Admin Dashboard
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
<a href="/user" class="user-action">
|
||||||
|
<i class="ri-user-line"></i>
|
||||||
|
My Dashboard
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
<a href="/api/docs/" class="user-action docs">
|
||||||
|
<i class="ri-book-line"></i>
|
||||||
|
API Docs
|
||||||
|
</a>
|
||||||
|
<form action="/logout" method="POST" style="display: inline;">
|
||||||
|
{{csrfField .Request}}
|
||||||
|
<button type="submit" class="user-action logout">
|
||||||
|
<i class="ri-logout-circle-line"></i>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<ul class="site-list">
|
<ul class="site-list">
|
||||||
|
|
@ -31,13 +64,38 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .ContactLink}}
|
|
||||||
<li class="join-link">
|
|
||||||
<i class="ri-user-add-line"></i>
|
|
||||||
<a href="{{.ContactLink}}" target="_blank">...and maybe you?</a>
|
|
||||||
</li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{{if not .User}}
|
||||||
|
<section class="action-section">
|
||||||
|
<h2>Get Involved</h2>
|
||||||
|
<div class="action-cards">
|
||||||
|
<a href="/submit" class="action-card action-card-primary">
|
||||||
|
<i class="ri-add-circle-line"></i>
|
||||||
|
<div class="action-content">
|
||||||
|
<div class="action-title">Submit Your Site</div>
|
||||||
|
<div class="action-description">Join the webring with your website</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/login" class="action-card">
|
||||||
|
<i class="ri-login-circle-line"></i>
|
||||||
|
<div class="action-content">
|
||||||
|
<div class="action-title">Login</div>
|
||||||
|
<div class="action-description">Manage your websites in the ring</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/api/docs/" class="action-card">
|
||||||
|
<i class="ri-book-line"></i>
|
||||||
|
<div class="action-content">
|
||||||
|
<div class="action-title">API Docs</div>
|
||||||
|
<div class="action-description">Developer documentation</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{end}}
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<a href="https://github.com/Alexander-D-Karpov/webring">
|
<a href="https://github.com/Alexander-D-Karpov/webring">
|
||||||
|
|
|
||||||
68
internal/public/templates/submit_site.html
Normal file
68
internal/public/templates/submit_site.html
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Submit Site - Webring</title>
|
||||||
|
<link rel="stylesheet" href="/static/public.css">
|
||||||
|
<link rel="preconnect" href="https://rsms.me/">
|
||||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.3.0/fonts/remixicon.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>
|
||||||
|
<i class="ri-bubble-chart-fill"></i>
|
||||||
|
Submit Your Site
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{{if .Error}}
|
||||||
|
<div style="max-width: 400px; margin: 0 auto 1.5rem auto; padding: 1rem; background: rgba(185, 28, 28, 0.1); border: 1px solid rgba(185, 28, 28, 0.3); border-left: 4px solid #b91c1c; border-radius: 6px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||||
|
<i class="ri-error-warning-line" style="color: #fca5a5; font-size: 1.25rem;"></i>
|
||||||
|
<span style="color: #fecaca; font-size: 0.875rem;">{{.Error}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<form action="/submit" method="POST" style="max-width: 400px; margin: 0 auto;">
|
||||||
|
{{csrfField .Request}}
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<label for="slug">Slug (unique identifier):</label>
|
||||||
|
<input type="text" id="slug" name="slug" pattern="[a-z0-9-]{3,50}" required
|
||||||
|
placeholder="my-awesome-site" style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
<small style="color: var(--color-gray-400);">Only lowercase letters, numbers, and hyphens. 3-50 characters.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<label for="name">Site Name:</label>
|
||||||
|
<input type="text" id="name" name="name" required
|
||||||
|
placeholder="My Awesome Site" style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<label for="url">Site URL:</label>
|
||||||
|
<input type="url" id="url" name="url" required
|
||||||
|
placeholder="https://example.com" style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<label for="telegram_username">Your Telegram Username (optional):</label>
|
||||||
|
<input type="text" id="telegram_username" name="telegram_username"
|
||||||
|
placeholder="username" style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
<small style="color: var(--color-gray-400);">
|
||||||
|
Enter your Telegram username (without @) to Manage your websites later. (Optional)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" style="width: 100%; padding: 0.75rem; background: var(--color-primary-900); color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
Submit Site for Review
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style="text-align: center; margin-top: 2rem; color: var(--color-gray-400);">
|
||||||
|
<a href="/">← Back to site listing</a>
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
internal/public/templates/submit_success.html
Normal file
29
internal/public/templates/submit_success.html
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Submission Successful - Webring</title>
|
||||||
|
<link rel="stylesheet" href="/static/public.css">
|
||||||
|
<link rel="preconnect" href="https://rsms.me/">
|
||||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.3.0/fonts/remixicon.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>
|
||||||
|
<i class="ri-check-circle-fill"></i>
|
||||||
|
Site Submitted Successfully!
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<p>Your site has been submitted for review. An admin will review it shortly.</p>
|
||||||
|
<p style="margin-top: 2rem;">
|
||||||
|
<a href="/">← Back to site listing</a> |
|
||||||
|
<a href="/user">Go to your dashboard</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
318
internal/telegram/notifications.go
Normal file
318
internal/telegram/notifications.go
Normal file
File diff suppressed because it is too large
Load Diff
279
internal/telegram/templates.go
Normal file
279
internal/telegram/templates.go
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
593
internal/user/admin.go
Normal file
593
internal/user/admin.go
Normal file
File diff suppressed because it is too large
Load Diff
22
internal/user/context.go
Normal file
22
internal/user/context.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"webring/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const userContextKey contextKey = "user"
|
||||||
|
|
||||||
|
func SetUserContext(ctx context.Context, user *models.User) context.Context {
|
||||||
|
return context.WithValue(ctx, userContextKey, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserFromContext(ctx context.Context) *models.User {
|
||||||
|
if user, ok := ctx.Value(userContextKey).(*models.User); ok {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
430
internal/user/dashboard.go
Normal file
430
internal/user/dashboard.go
Normal file
File diff suppressed because it is too large
Load Diff
385
internal/user/handlers.go
Normal file
385
internal/user/handlers.go
Normal file
File diff suppressed because it is too large
Load Diff
165
internal/user/templates/admin_dashboard.html
Normal file
165
internal/user/templates/admin_dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
36
internal/user/templates/login.html
Normal file
36
internal/user/templates/login.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - Webring</title>
|
||||||
|
<link rel="stylesheet" href="/static/public.css">
|
||||||
|
<link rel="preconnect" href="https://rsms.me/">
|
||||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.3.0/fonts/remixicon.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>
|
||||||
|
<i class="ri-bubble-chart-fill"></i>
|
||||||
|
Login to Webring
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div style="text-align: center; margin: 2rem 0;">
|
||||||
|
<p>Login with your Telegram account to Manage your websites in the webring.</p>
|
||||||
|
|
||||||
|
<script async src="https://telegram.org/js/telegram-widget.js?22"
|
||||||
|
data-telegram-login="{{.BotUsername}}"
|
||||||
|
data-size="large"
|
||||||
|
data-auth-url="/auth/telegram"
|
||||||
|
data-request-access="write">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p style="margin-top: 2rem; color: var(--color-gray-400);">
|
||||||
|
<a href="/">← Back to site listing</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
82
internal/user/templates/request_error.html
Normal file
82
internal/user/templates/request_error.html
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Request Error - Webring</title>
|
||||||
|
<link rel="stylesheet" href="/static/dashboard.css">
|
||||||
|
<link rel="preconnect" href="https://rsms.me/">
|
||||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.3.0/fonts/remixicon.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="admin-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-left">
|
||||||
|
<a href="/admin" class="logo-link">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="ri-bubble-chart-fill"></i>
|
||||||
|
<span class="logo-text">Webring</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="page-info">
|
||||||
|
<h1>Request Error</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<nav class="header-nav">
|
||||||
|
<a href="/admin/requests" class="nav-item">
|
||||||
|
<i class="ri-arrow-left-line"></i>
|
||||||
|
<span>Back to Requests</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-icon">
|
||||||
|
<i class="ri-error-warning-line"></i>
|
||||||
|
</div>
|
||||||
|
<div class="error-content">
|
||||||
|
<h2>Unable to Process Request</h2>
|
||||||
|
<p class="error-message">{{.Error}}</p>
|
||||||
|
|
||||||
|
{{if .Request}}
|
||||||
|
<div class="error-details">
|
||||||
|
<h3>Request Details</h3>
|
||||||
|
{{if eq .Request.RequestType "create"}}
|
||||||
|
<p><strong>Type:</strong> Site Creation</p>
|
||||||
|
{{else}}
|
||||||
|
<p><strong>Type:</strong> Site Update</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Request.ChangedFields}}
|
||||||
|
<div class="changed-fields">
|
||||||
|
<strong>Requested Fields:</strong>
|
||||||
|
<ul>
|
||||||
|
{{range $key, $value := .Request.ChangedFields}}
|
||||||
|
<li><strong>{{$key}}:</strong> {{$value}}</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/admin/requests" class="btn btn-primary">
|
||||||
|
<i class="ri-arrow-left-line"></i>
|
||||||
|
Back to Requests
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-note">
|
||||||
|
<p><strong>Note:</strong> The request has not been deleted and can still be reviewed. You may need to contact the user to choose a different slug or make other changes.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
215
internal/user/templates/user_dashboard.html
Normal file
215
internal/user/templates/user_dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
178
internal/user/templates/users_management.html
Normal file
178
internal/user/templates/users_management.html
Normal file
File diff suppressed because it is too large
Load Diff
6
messages/admin_approved_create.txt
Normal file
6
messages/admin_approved_create.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*Request Approved*
|
||||||
|
|
||||||
|
*Admin:* {{.AdminName}}
|
||||||
|
*Action:* Approved site creation
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
13
messages/admin_approved_update.txt
Normal file
13
messages/admin_approved_update.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
*Update Approved*
|
||||||
|
|
||||||
|
*Admin:* {{.AdminName}}
|
||||||
|
*Action:* Approved site update
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
|
{{- if .Changes}}
|
||||||
|
|
||||||
|
*Changes:*
|
||||||
|
{{- range .Changes}}
|
||||||
|
• *{{.Key}}:* {{.Value}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
6
messages/admin_declined_create.txt
Normal file
6
messages/admin_declined_create.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*Request Declined*
|
||||||
|
|
||||||
|
*Admin:* {{.AdminName}}
|
||||||
|
*Action:* Declined site creation
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
6
messages/admin_declined_update.txt
Normal file
6
messages/admin_declined_update.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*Update Declined*
|
||||||
|
|
||||||
|
*Admin:* {{.AdminName}}
|
||||||
|
*Action:* Declined site update
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
7
messages/approved_create.txt
Normal file
7
messages/approved_create.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
*Request Approved*
|
||||||
|
|
||||||
|
Your site submission has been approved!
|
||||||
|
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
|
|
||||||
|
Your site is now part of the webring.
|
||||||
10
messages/approved_update.txt
Normal file
10
messages/approved_update.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
*Update Approved*
|
||||||
|
|
||||||
|
Your site update request has been approved and the changes have been applied.
|
||||||
|
{{- if .Changes}}
|
||||||
|
|
||||||
|
*Applied changes:*
|
||||||
|
{{- range .Changes}}
|
||||||
|
• *{{.Key}}:* {{.Value}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
5
messages/declined_create.txt
Normal file
5
messages/declined_create.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
*Request Declined*
|
||||||
|
|
||||||
|
Your site submission request for *{{.SiteName}}* has been declined by an administrator.
|
||||||
|
|
||||||
|
If you have questions, please contact the webring administrator.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user