Compare commits

..

33 Commits

Author SHA1 Message Date
Damir Modyarov
65d4d3c3fc
build(nix): Update package vendorHash 2026-02-16 00:33:15 +03:00
12fdd0ba64 implement Telegram message templates for notifications 2026-02-10 20:56:01 +03:00
f774fa993f feat: Add enabled status to sites and enabled toggle 2026-01-12 00:17:13 +03:00
d4607434d4 Merge remote-tracking branch 'origin/master' 2026-01-08 03:45:17 +03:00
7ed6a23b63 remove HTML escaping from input sanitization 2026-01-08 03:45:07 +03:00
Damir Modyarov
d324c9490d
fix: Change server error code from 500 to 400 (#6)
* fix: Change server error code from 500 to 400

* fix: Change checker HTTP method to GET
2026-01-03 20:54:53 +03:00
499c4a807b update down threshold configuration and enhance site status handling 2025-12-23 23:54:18 +03:00
bd346671b8 add down threshold configuration and enhance site status handling 2025-12-23 23:49:55 +03:00
e98dd6e64b refactor checker to use workers 2025-12-16 16:39:54 +03:00
6bd5393c0c refactor checker to use workers 2025-12-16 16:32:54 +03:00
d605bd37b7 moved docs to api/docs 2025-12-08 18:51:34 +03:00
3110c00e71 add user agent header to HTTP requests in checker 2025-12-05 00:24:11 +03:00
5d14f42352 normalize telegram usernames to lowercase and enhance favicon handling 2025-12-04 18:30:55 +03:00
0acd6686cc enhance error handling and user feedback for site requests 2025-12-02 00:14:00 +03:00
2c1885e6de feat: update user handling to use sql.NullInt64 for telegram_id 2025-12-01 01:06:32 +03:00
aba05aa6b6 enhance user handling with improved telegram username processing and error logging 2025-12-01 00:51:10 +03:00
73347edc67 enhance user handling with improved telegram username processing and error logging 2025-12-01 00:31:10 +03:00
c1eed46f6d feat: implement unique constraint for telegram_username and update user handling 2025-11-30 20:48:49 +03:00
e1bc69205b Merge remote-tracking branch 'origin/master' 2025-11-30 18:21:33 +03:00
90c129d606 refactor: update swagger handlers to use embedded file system for api docs 2025-11-30 18:21:20 +03:00
be8340b8a9
build: Implement nix flake (#5) 2025-11-27 19:18:51 +03:00
ba3474e214 add logout to navbar 2025-11-10 16:39:03 +03:00
1fcb4e35f7 refactor: update site structure, add CSRF, tests, telegram notifications 2025-11-10 14:43:25 +03:00
14c6e6c4d3 update ci 2025-11-10 02:01:23 +03:00
34cb69b047 update navbar, minor lint update & refactoring 2025-11-10 01:57:22 +03:00
Alexander Karpov
bf62b8f427
added telegram oauth, submittions, dispaly order, admin
* major changes

Introduce full user system and approval workflow
——————————————————————————————————————————

Login & sessions

    Telegram one‑click login (/login → /auth/telegram) with HMAC verification

    New users and sessions tables; telegram_id now optional, TTL‑based cleanup job

    Secure session_id cookie (configurable TTL and Secure/SameSite flags)

User dashboard (/user)

    Lists the member’s sites and their uptime status

    Forms to submit new site or update requests; validation and slug/url sanitisation

    View pending requests with change diff

Request storage

    update_requests table captures create/update ops as JSONB “changed_fields”

Admin review

    /admin/requests interface to approve / reject queued requests

    Approval auto‑creates sites (with ordered display_order) or patches existing ones, then refreshes favicon

Super‑admin panel

    /admin/setup lists all users, toggle is_admin and forcibly logs them out

Notifications

    On every new request, all admins with a Telegram ID receive a Markdown summary via bot API

Public UI tweaks

    Header shows login/logout, role‑aware links and call‑to‑action cards

    /submit page creates a queued request

Config & env

    Added TELEGRAM_BOT_TOKEN, TELEGRAM_BOT_USERNAME, SESSION_TTL_HOURS, SESSION_SECURE_COOKIE

    .env.template updated accordingly

Migrations 004–010

    Users, sessions, foreign key on sites, display_order, update_requests, telegram_id nullability

BREAKING CHANGE

    Environment must supply Telegram bot credentials

    Database must be migrated; existing “dashboard” auth remains but admin routes are now session‑protected where applicable
2025-08-08 16:36:09 +03:00
341e12e611
refactor: Use URL struct in favicon downloader (#3) 2025-06-22 19:24:48 +03:00
3133c15253
fix: Allow short numeric slugs for compatability 2025-06-22 17:27:05 +03:00
f12e106049
feat: Implement slugs and id reordering (#2)
* fix: Make data API route paths consistent

* feat: Implement redirect for current website

* feat: Implement slugs and id reordering

* feat: Implement slug validation

* fix: Add constant zero ID to public site data for compatibility

* fix: Make slug length limited when validating

* docs: Update routes in README.md

* feat: Implement gapless reordering

* fix: Add IDs back to public data

* feat: Implement ID/slug collision checks
2025-06-19 14:34:18 +03:00
68e160f325 Refactor site queries to support downed site info retrieve 2025-01-24 14:24:46 +03:00
d2ff9246f9 minor checker fixes 2024-12-21 16:41:54 +03:00
d370489d9e fixed build 2024-11-12 19:10:28 +03:00
ebb576c2a2 added checker proxy support 2024-11-12 19:07:10 +03:00
83 changed files with 9403 additions and 632 deletions

44
.air.toml Normal file
View 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

View File

@ -1,5 +1,15 @@
PORT=8000
PORT=8080
DB_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/webring?sslmode=disable
DASHBOARD_USER=admin
DASHBOARD_PASSWORD=admin
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
View 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"

File diff suppressed because it is too large Load Diff

83
.golangci.yml Normal file
View 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
View 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"]

117
Makefile

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,12 @@ This project is a webring relay service built with Go. It manages a list of webs
## Features
- 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
- 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
## Prerequisites
@ -17,7 +21,6 @@ This project is a webring relay service built with Go. It manages a list of webs
## Installation
edit .env to set correct path to database
```
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
go mod tidy
@ -26,30 +29,51 @@ make migrate-up
```
## Local Run
```
go run cmd/server/main.go
```
or download prebuild version
```
wget https://github.com/Alexander-D-Karpov/webring/releases/latest/download/webring
chmod +x 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
- Access the dashboard at `http://localhost:8080/dashboard` (use the credentials set in your `.env` file)
- API endpoints:
- Next site: `GET /{id}/next/`
- Previous site: `GET /{id}/prev/`
- Random site: `GET /{id}/random/`
- Full data for a site: `GET /{id}/data`
- Next site: `GET /{slug}/next/data`
- Previous site: `GET /{slug}/prev/data`
- Random site: `GET /{slug}/random/data`
- Full data for a site: `GET /{slug}/data`
- Redirect endpoints:
- Next site: `GET /{id}/next`
- Previous site: `GET /{id}/prev`
- Random site: `GET /{id}/random`
- Visit site: `GET /{slug}`
- Next site: `GET /{slug}/next`
- 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
View 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>

View File

@ -4,5 +4,5 @@ import (
"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

61
flake.lock Normal file
View 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
View 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
View File

@ -1,15 +1,17 @@
module webring
go 1.22.4
go 1.25.0
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/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/lib/pq v1.11.2
)
require (
github.com/andybalholm/cascadia v1.3.2 // indirect
golang.org/x/net v0.24.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
golang.org/x/net v0.50.0 // indirect
)

48
go.sum
View File

@ -1,28 +1,59 @@
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.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/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/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/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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.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=
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.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.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-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.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/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-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.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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.5.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-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.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.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/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-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/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=

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,22 +4,18 @@ import "net/http"
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow all origins
w.Header().Set("Access-Control-Allow-Origin", "*")
// Allow common HTTP methods
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
// Allow common HTTP headers
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
allowedHeaders := "Accept, Content-Type, Content-Length, Accept-Encoding, " +
"X-CSRF-Token, Authorization"
w.Header().Set("Access-Control-Allow-Headers", allowedHeaders)
// Handle preflight requests
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// Call the next handler
next.ServeHTTP(w, r)
})
}

339
internal/api/swagger.go Normal file

File diff suppressed because it is too large Load Diff

135
internal/auth/session.go Normal file

File diff suppressed because it is too large Load Diff

75
internal/auth/telegram.go Normal file
View 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

View File

@ -2,8 +2,9 @@ package database
import (
"database/sql"
_ "github.com/lib/pq"
"os"
_ "github.com/lib/pq" // PostgreSQL driver
)
func Connect() (*sql.DB, error) {

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,21 @@
package models
type Site struct {
ID int `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
IsUp bool `json:"is_up"`
LastCheck float64 `json:"last_check"`
Favicon *string `json:"favicon"`
ID int `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
URL string `json:"url"`
IsUp bool `json:"is_up"`
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 {
ID int `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
URL string `json:"url"`
Favicon *string `json:"favicon"`

31
internal/models/user.go Normal file
View 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

View File

@ -15,6 +15,39 @@
<i class="ri-bubble-chart-fill"></i>
Webring Listing
</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>
<main>
<ul class="site-list">
@ -31,13 +64,38 @@
</a>
</li>
{{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>
{{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>
<footer>
<a href="https://github.com/Alexander-D-Karpov/webring">

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

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

22
internal/user/context.go Normal file
View 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

File diff suppressed because it is too large Load Diff

385
internal/user/handlers.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
*Request Approved*
*Admin:* {{.AdminName}}
*Action:* Approved site creation
*User:* {{.UserName}}
*Site:* {{.SiteName}}

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

View File

@ -0,0 +1,6 @@
*Request Declined*
*Admin:* {{.AdminName}}
*Action:* Declined site creation
*User:* {{.UserName}}
*Site:* {{.SiteName}}

View File

@ -0,0 +1,6 @@
*Update Declined*
*Admin:* {{.AdminName}}
*Action:* Declined site update
*User:* {{.UserName}}
*Site:* {{.SiteName}}

View File

@ -0,0 +1,7 @@
*Request Approved*
Your site submission has been approved!
*Site:* {{.SiteName}}
Your site is now part of the webring.

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

View 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