Compare commits

..

No commits in common. "master" and "v20250808-bf62b8f" have entirely different histories.

59 changed files with 987 additions and 4093 deletions

View File

@ -1,4 +1,4 @@
PORT=8080 PORT=8000
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
@ -6,10 +6,4 @@ CONTACT_LINK=mailto:webring@example.com
TELEGRAM_BOT_TOKEN=your_bot_token TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_BOT_USERNAME=your_bot_username TELEGRAM_BOT_USERNAME=your_bot_username
SESSION_TTL_HOURS=2160 # 90 days SESSION_TTL_HOURS=2160 # 90 days
SESSION_SECURE_COOKIE=true # Set to true if using HTTPS, false for HTTP 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

View File

@ -22,7 +22,7 @@ secret_scanning:
pattern: 'postgres://postgres:postgres@localhost' pattern: 'postgres://postgres:postgres@localhost'
- name: "Test credentials" - name: "Test credentials"
pattern: 'postgres|postgres|test_.*' pattern: 'testuser|testpass|test_.*'
additional_config: additional_config:
high_entropy_threshold: 4.5 high_entropy_threshold: 4.5

View File

@ -8,7 +8,7 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
GO_VERSION: '1.25.0' GO_VERSION: '1.22'
permissions: permissions:
contents: read contents: read
@ -28,7 +28,7 @@ jobs:
cache: true cache: true
- name: Run golangci-lint - name: Run golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v4
with: with:
version: latest version: latest
args: --timeout=5m args: --timeout=5m
@ -42,8 +42,8 @@ jobs:
postgres: postgres:
image: postgres:15 image: postgres:15
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: testpass
POSTGRES_USER: postgres POSTGRES_USER: testuser
POSTGRES_DB: webring_test POSTGRES_DB: webring_test
options: >- options: >-
--health-cmd pg_isready --health-cmd pg_isready
@ -68,29 +68,22 @@ jobs:
- name: Run migrations - name: Run migrations
env: env:
DB_CONNECTION_STRING: postgres://postgres:postgres@localhost:5432/webring_test?sslmode=disable DB_CONNECTION_STRING: postgres://testuser:testpass@localhost:5432/webring_test?sslmode=disable
run: | run: |
migrate -database "$DB_CONNECTION_STRING" -path migrations up migrate -database "$DB_CONNECTION_STRING" -path migrations up
- name: Run tests - name: Run tests
env: env:
DB_CONNECTION_STRING: postgres://postgres:postgres@localhost:5432/webring_test?sslmode=disable DB_CONNECTION_STRING: postgres://testuser:testpass@localhost:5432/webring_test?sslmode=disable
CHECKER_DEBUG: "false" CHECKER_DEBUG: false
LOG_FILE_PATH: test.log LOG_FILE_PATH: test.log
TELEGRAM_BOT_TOKEN: ""
TELEGRAM_BOT_USERNAME: ""
PORT: "8080"
MEDIA_FOLDER: "media"
SESSION_TTL_HOURS: "2160"
SESSION_SECURE_COOKIE: "false"
run: go test -v -race -coverprofile=coverage.out ./... run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v3
with: with:
file: ./coverage.out file: ./coverage.out
fail_ci_if_error: false fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
build: build:
name: Build name: Build
@ -206,7 +199,7 @@ jobs:
echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
with: with:
tag_name: ${{ steps.tag.outputs.tag }} tag_name: ${{ steps.tag.outputs.tag }}
name: Release ${{ steps.tag.outputs.date }} name: Release ${{ steps.tag.outputs.date }}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
# Build stage # Build stage
FROM golang:1.25-alpine AS builder FROM golang:1.22-alpine AS builder
# Install git and ca-certificates # Install git and ca-certificates
RUN apk add --no-cache git ca-certificates tzdata RUN apk add --no-cache git ca-certificates tzdata
@ -29,8 +29,6 @@ WORKDIR /root/
# Copy the binary from builder stage # Copy the binary from builder stage
COPY --from=builder /build/webring . COPY --from=builder /build/webring .
COPY --from=builder /build/docs ./docs
COPY --from=builder /build/messages ./messages
# Create media directory # Create media directory
RUN mkdir -p media RUN mkdir -p media

View File

@ -85,28 +85,6 @@ db-reset:
@$(MIGRATE) down -all @$(MIGRATE) down -all
@$(MIGRATE) up @$(MIGRATE) up
# Docker commands
docker-build:
@echo "Building Docker image..."
@docker build -t webring:latest .
docker-run:
@echo "Running Docker container..."
@docker run -p 8080:8080 --env-file .env webring:latest
# Go module management
mod-tidy:
@echo "Tidying Go modules..."
@go mod tidy
mod-download:
@echo "Downloading Go modules..."
@go mod download
mod-verify:
@echo "Verifying Go modules..."
@go mod verify
# Help # Help
help: help:
@echo "Available commands:" @echo "Available commands:"
@ -127,9 +105,4 @@ help:
@echo " clean - Clean build artifacts" @echo " clean - Clean build artifacts"
@echo " install-tools - Install development tools" @echo " install-tools - Install development tools"
@echo " db-reset - Reset database (down-all then up)" @echo " db-reset - Reset database (down-all then up)"
@echo " docker-build - Build Docker image"
@echo " docker-run - Run Docker container"
@echo " mod-tidy - Tidy Go modules"
@echo " mod-download - Download Go modules"
@echo " mod-verify - Verify Go modules"
@echo " help - Show this help message" @echo " help - Show this help message"

View File

@ -7,10 +7,6 @@ This project is a webring relay service built with Go. It manages a list of webs
- Dashboard for managing websites in the webring - Dashboard for managing websites in the webring
- Automatic uptime checking of websites (with proxy support) - 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
@ -21,6 +17,7 @@ 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
@ -29,51 +26,31 @@ 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 /{slug}/next/data` - Next site: `GET /{slug}/next/data`
- Previous site: `GET /{slug}/prev/data` - Previous site: `GET /{slug}/prev/data`
- Random site: `GET /{slug}/random/data` - Random site: `GET /{slug}/random/data`
- Full data for a site: `GET /{slug}/data` - Full data for a site: `GET /{slug}/data`
- Redirect endpoints: - Redirect endpoints:
- Visit site: `GET /{slug}` - Visit site: `GET /{slug}`
- Next site: `GET /{slug}/next` - Next site: `GET /{slug}/next`
- Previous site: `GET /{slug}/prev` - Previous site: `GET /{slug}/prev`
- Random site: `GET /{slug}/random` - Random site: `GET /{slug}/random`

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Webring API Documentation</title> <title>Webring API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" /> <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
<style> <style>
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; } html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
*, *:before, *:after { box-sizing: inherit; } *, *:before, *:after { box-sizing: inherit; }
body { margin:0; background: #fafafa; } body { margin:0; background: #fafafa; }
</style> </style>
</head> </head>
<body> <body>
<div id="swagger-ui"></div> <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-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script> <script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script>
<script> <script>
window.onload = function() { window.onload = function() {
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({
url: '/api/docs/swagger.json', url: '/docs/swagger.json',
dom_id: '#swagger-ui', dom_id: '#swagger-ui',
deepLinking: true, deepLinking: true,
presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ],
plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ],
layout: "StandaloneLayout" layout: "StandaloneLayout"
}); });
}; };
</script> </script>
</body> </body>
</html> </html>

View File

@ -4,5 +4,5 @@ import (
"embed" "embed"
) )
//go:embed static internal/dashboard/templates internal/public/templates internal/user/templates docs //go:embed static internal/dashboard/templates internal/public/templates internal/user/templates
var Files embed.FS var Files embed.FS

View File

@ -1,61 +0,0 @@
{
"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
}

View File

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

48
go.sum
View File

@ -1,59 +1,28 @@
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=
@ -61,34 +30,17 @@ 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

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,35 @@
package api package api
import ( import (
"io/fs"
"log" "log"
"net/http" "net/http"
"os"
"webring"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
const dirPerm = 0o755
// @title Webring API // @title Webring API
// @version 1.0 // @version 1.0
// @description API for the webring // @description API for the webring
// @host localhost:8080 // @host localhost:8000
// @contact.url mailto://sanspie@akarpov.ru // @contact.url mailto://sanspie@akarpov.ru
// @BasePath / // @BasePath /
func RegisterSwaggerHandlers(r *mux.Router) { func RegisterSwaggerHandlers(r *mux.Router) {
r.HandleFunc("/api/docs/swagger.json", swaggerJSONHandler).Methods("GET") ensureDocsDirectory()
docsFS, err := fs.Sub(webring.Files, "docs") // Register specific JSON endpoint BEFORE the PathPrefix handler
if err != nil { r.HandleFunc("/docs/swagger.json", swaggerJSONHandler).Methods("GET")
log.Printf("Warning: Could not access docs directory: %v", err) r.PathPrefix("/docs/").Handler(http.StripPrefix("/docs/", http.FileServer(http.Dir("./docs/"))))
return }
func ensureDocsDirectory() {
docsDir := "docs"
if err := os.MkdirAll(docsDir, dirPerm); err != nil {
log.Printf("Warning: Could not create docs directory: %v", err)
} }
r.PathPrefix("/api/docs/").Handler(http.StripPrefix("/api/docs/", http.FileServer(http.FS(docsFS))))
} }
func swaggerJSONHandler(w http.ResponseWriter, _ *http.Request) { func swaggerJSONHandler(w http.ResponseWriter, _ *http.Request) {

View File

@ -59,24 +59,18 @@ func CreateSession(db *sql.DB, userID int) (*models.Session, error) {
func GetSessionUser(db *sql.DB, sessionID string) (*models.User, error) { func GetSessionUser(db *sql.DB, sessionID string) (*models.User, error) {
var user models.User var user models.User
var telegramID sql.NullInt64
err := db.QueryRow(` err := db.QueryRow(`
SELECT u.id, u.telegram_id, u.telegram_username, u.first_name, u.last_name, u.is_admin, u.created_at SELECT u.id, u.telegram_id, u.telegram_username, u.first_name, u.last_name, u.is_admin, u.created_at
FROM users u FROM users u
JOIN sessions s ON u.id = s.user_id JOIN sessions s ON u.id = s.user_id
WHERE s.id = $1 AND s.expires_at > NOW() WHERE s.id = $1 AND s.expires_at > NOW()
`, sessionID).Scan( `, sessionID).Scan(
&user.ID, &telegramID, &user.TelegramUsername, &user.ID, &user.TelegramID, &user.TelegramUsername,
&user.FirstName, &user.LastName, &user.IsAdmin, &user.CreatedAt) &user.FirstName, &user.LastName, &user.IsAdmin, &user.CreatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if telegramID.Valid {
user.TelegramID = telegramID.Int64
}
return &user, nil return &user, nil
} }

View File

@ -9,7 +9,6 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
) )
type TelegramUser struct { type TelegramUser struct {
@ -59,9 +58,6 @@ func VerifyTelegramAuth(values url.Values, botToken string) (*TelegramUser, erro
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid auth_date") 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{ return &TelegramUser{
ID: id, ID: id,

File diff suppressed because it is too large Load Diff

View File

@ -42,17 +42,10 @@
<i class="ri-global-line"></i> <i class="ri-global-line"></i>
<span>Public</span> <span>Public</span>
</a> </a>
<a href="/api/docs/" class="nav-item secondary"> <a href="/docs/" class="nav-item secondary">
<i class="ri-book-line"></i> <i class="ri-book-line"></i>
<span>API</span> <span>API</span>
</a> </a>
<form action="/logout" method="POST" style="display: inline;">
{{csrfField .Request}}
<button type="submit" class="nav-item logout-btn">
<i class="ri-logout-circle-line"></i>
<span>Logout</span>
</button>
</form>
</nav> </nav>
</div> </div>
</div> </div>
@ -69,7 +62,6 @@
<th class="col-url">URL</th> <th class="col-url">URL</th>
<th class="col-telegram">TELEGRAM</th> <th class="col-telegram">TELEGRAM</th>
<th class="col-status">STATUS</th> <th class="col-status">STATUS</th>
<th class="col-enabled">ENABLED</th>
<th class="col-ping">PING</th> <th class="col-ping">PING</th>
<th class="col-actions">ACTIONS</th> <th class="col-actions">ACTIONS</th>
</tr> </tr>
@ -98,32 +90,27 @@
<span class="status-badge new">New</span> <span class="status-badge new">New</span>
</td> </td>
<td></td> <td></td>
<td></td>
<td> <td>
<div class="actions"> <div class="actions">
<button type="submit" form="form-new" class="btn btn-primary btn-sm"> <button type="submit" form="form-new" class="btn btn-primary btn-sm">
<i class="ri-check-line"></i> <i class="ri-check-line"></i>
</button> </button>
</div> </div>
<form action="/admin/add" method="POST" style="display: none" id="form-new"> <form action="/admin/add" method="POST" style="display: none" id="form-new"></form>
{{csrfField .Request}}
</form>
</td> </td>
</tr> </tr>
{{range .Sites}} {{range .}}
<tr class="row-site"> <tr class="row-site">
<td> <td>
<div class="order-controls"> <div class="order-controls">
<span class="order-number">{{.ID}}</span> <span class="order-number">{{.ID}}</span>
<div class="order-actions"> <div class="order-actions">
<form action="/admin/reorder/{{.ID}}/up" method="POST"> <form action="/admin/reorder/{{.ID}}/up" method="POST">
{{csrfField $.Request}}
<button type="submit" class="btn-order"> <button type="submit" class="btn-order">
<i class="ri-arrow-up-s-line"></i> <i class="ri-arrow-up-s-line"></i>
</button> </button>
</form> </form>
<form action="/admin/reorder/{{.ID}}/down" method="POST"> <form action="/admin/reorder/{{.ID}}/down" method="POST">
{{csrfField $.Request}}
<button type="submit" class="btn-order"> <button type="submit" class="btn-order">
<i class="ri-arrow-down-s-line"></i> <i class="ri-arrow-down-s-line"></i>
</button> </button>
@ -171,18 +158,6 @@
<span class="status-badge down">DOWN</span> <span class="status-badge down">DOWN</span>
{{end}} {{end}}
</td> </td>
<td>
<form action="/admin/toggle-enabled/{{.ID}}" method="POST" style="display: inline;">
{{csrfField $.Request}}
<button type="submit" class="btn btn-sm {{if .Enabled}}btn-success{{else}}btn-warning{{end}}" title="{{if .Enabled}}Click to disable{{else}}Click to enable{{end}}">
{{if .Enabled}}
<i class="ri-check-line"></i>
{{else}}
<i class="ri-close-line"></i>
{{end}}
</button>
</form>
</td>
<td class="ping-cell"> <td class="ping-cell">
{{if .LastCheck}} {{if .LastCheck}}
<span class="ping-value">{{.LastCheck}}</span> <span class="ping-value">{{.LastCheck}}</span>
@ -195,11 +170,8 @@
<button type="submit" form="form-{{.ID}}" class="btn btn-primary btn-sm"> <button type="submit" form="form-{{.ID}}" class="btn btn-primary btn-sm">
<i class="ri-save-3-line"></i> <i class="ri-save-3-line"></i>
</button> </button>
<form action="/admin/update/{{.ID}}" method="POST" id="form-{{.ID}}"> <form action="/admin/update/{{.ID}}" method="POST" id="form-{{.ID}}"></form>
{{csrfField $.Request}}
</form>
<form action="/admin/remove/{{.ID}}" method="POST" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this site?')"> <form action="/admin/remove/{{.ID}}" method="POST" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this site?')">
{{csrfField $.Request}}
<button type="submit" class="btn btn-danger btn-sm"> <button type="submit" class="btn btn-danger btn-sm">
<i class="ri-delete-bin-line"></i> <i class="ri-delete-bin-line"></i>
</button> </button>

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,6 @@ type Site struct {
Name string `json:"name"` Name string `json:"name"`
URL string `json:"url"` URL string `json:"url"`
IsUp bool `json:"is_up"` IsUp bool `json:"is_up"`
Enabled bool `json:"enabled"`
LastCheck float64 `json:"last_check"` LastCheck float64 `json:"last_check"`
Favicon *string `json:"favicon"` Favicon *string `json:"favicon"`
UserID *int `json:"user_id"` UserID *int `json:"user_id"`
@ -15,6 +14,7 @@ type Site struct {
} }
type PublicSite struct { type PublicSite struct {
ID int `json:"id"`
Slug string `json:"slug"` Slug string `json:"slug"`
Name string `json:"name"` Name string `json:"name"`
URL string `json:"url"` URL string `json:"url"`

File diff suppressed because it is too large Load Diff

View File

@ -34,12 +34,11 @@
My Dashboard My Dashboard
</a> </a>
{{end}} {{end}}
<a href="/api/docs/" class="user-action docs"> <a href="/docs/" class="user-action docs">
<i class="ri-book-line"></i> <i class="ri-book-line"></i>
API Docs API Docs
</a> </a>
<form action="/logout" method="POST" style="display: inline;"> <form action="/logout" method="POST" style="display: inline;">
{{csrfField .Request}}
<button type="submit" class="user-action logout"> <button type="submit" class="user-action logout">
<i class="ri-logout-circle-line"></i> <i class="ri-logout-circle-line"></i>
Logout Logout
@ -86,7 +85,7 @@
</div> </div>
</a> </a>
<a href="/api/docs/" class="action-card"> <a href="/docs/" class="action-card">
<i class="ri-book-line"></i> <i class="ri-book-line"></i>
<div class="action-content"> <div class="action-content">
<div class="action-title">API Docs</div> <div class="action-title">API Docs</div>

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,82 +0,0 @@
<!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

View File

@ -42,13 +42,12 @@
<i class="ri-global-line"></i> <i class="ri-global-line"></i>
<span>Public</span> <span>Public</span>
</a> </a>
<a href="/api/docs/" class="nav-item secondary"> <a href="/docs/" class="nav-item secondary">
<i class="ri-book-line"></i> <i class="ri-book-line"></i>
<span>API</span> <span>API</span>
</a> </a>
{{if ne .CurrentUser.ID -1}} {{if ne .CurrentUser.ID -1}}
<form action="/logout" method="POST" style="display: inline;"> <form action="/logout" method="POST" style="display: inline;">
{{csrfField .Request}}
<button type="submit" class="nav-item logout-btn"> <button type="submit" class="nav-item logout-btn">
<i class="ri-logout-circle-line"></i> <i class="ri-logout-circle-line"></i>
<span>Logout</span> <span>Logout</span>
@ -123,12 +122,6 @@
{{if ne .TelegramID 0}}{{.TelegramID}}{{else}}<em>Not set</em>{{end}} {{if ne .TelegramID 0}}{{.TelegramID}}{{else}}<em>Not set</em>{{end}}
</span> </span>
</div> </div>
<div class="detail-item">
<span class="detail-label">Username:</span>
<span class="detail-value">
{{if .TelegramUsername}}@{{.TelegramUsername}}{{else}}<em>Not set</em>{{end}}
</span>
</div>
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">Joined:</span> <span class="detail-label">Joined:</span>
<span class="detail-value">{{.CreatedAt.Format "Jan 2, 2006"}}</span> <span class="detail-value">{{.CreatedAt.Format "Jan 2, 2006"}}</span>
@ -146,9 +139,7 @@
Make Admin Make Admin
{{end}} {{end}}
</button> </button>
<form action="/admin/users/{{.ID}}/toggle-admin" method="POST" id="form-{{.ID}}"> <form action="/admin/users/{{.ID}}/toggle-admin" method="POST" id="form-{{.ID}}"></form>
{{csrfField $.Request}}
</form>
{{else}} {{else}}
<div class="self-indicator"> <div class="self-indicator">
<i class="ri-user-star-line"></i> <i class="ri-user-star-line"></i>

View File

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

View File

@ -1,13 +0,0 @@
*Update Approved*
*Admin:* {{.AdminName}}
*Action:* Approved site update
*User:* {{.UserName}}
*Site:* {{.SiteName}}
{{- if .Changes}}
*Changes:*
{{- range .Changes}}
• *{{.Key}}:* {{.Value}}
{{- end}}
{{- end}}

View File

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

View File

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

View File

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

View File

@ -1,10 +0,0 @@
*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

@ -1,5 +0,0 @@
*Request Declined*
Your site submission request for *{{.SiteName}}* has been declined by an administrator.
If you have questions, please contact the webring administrator.

View File

@ -1,5 +0,0 @@
*Update Request Declined*
Your update request for *{{.SiteName}}* has been declined by an administrator\.
If you have questions, please contact the webring administrator\.

View File

@ -1,8 +0,0 @@
*New Site Submission Request*
*User:* {{.UserName}}
*Slug:* `{{.Slug}}`
*Site Name:* {{.SiteName}}
*URL:* {{.URL}}
*Submitted:* {{.Date}}

View File

@ -1,11 +0,0 @@
*Site Update Request*
*User:* {{.UserName}}
*Site:* {{.SiteName}} (`{{.SiteSlug}}`)
*Changes:*
{{- range .Changes}}
• *{{.Key}}:* {{.Value}}
{{- end}}
*Submitted:* {{.Date}}

View File

@ -1,3 +0,0 @@
*Site Status: Offline*
Your site *{{.SiteName}}* is currently not responding after {{.DownThreshold}} consecutive checks. Please check your server.

View File

@ -1,3 +0,0 @@
*Site Status: Online*
Your site *{{.SiteName}}* is now responding and back online.

View File

@ -1 +0,0 @@
DROP INDEX IF EXISTS users_telegram_username_unique;

View File

@ -1,53 +0,0 @@
DO $$
DECLARE
dup_record RECORD;
keep_id INTEGER;
merge_ids INTEGER[];
BEGIN
FOR dup_record IN
SELECT telegram_username, array_agg(id ORDER BY
CASE
WHEN telegram_id IS NOT NULL THEN 0
ELSE 1
END,
created_at ASC
) as user_ids
FROM users
WHERE telegram_username IS NOT NULL
GROUP BY telegram_username
HAVING COUNT(*) > 1
LOOP
keep_id := dup_record.user_ids[1];
merge_ids := dup_record.user_ids[2:array_length(dup_record.user_ids, 1)];
UPDATE users SET
telegram_id = COALESCE(
users.telegram_id,
(SELECT telegram_id FROM users WHERE id = ANY(merge_ids) AND telegram_id IS NOT NULL LIMIT 1)
),
first_name = COALESCE(
users.first_name,
(SELECT first_name FROM users WHERE id = ANY(merge_ids) AND first_name IS NOT NULL LIMIT 1)
),
last_name = COALESCE(
users.last_name,
(SELECT last_name FROM users WHERE id = ANY(merge_ids) AND last_name IS NOT NULL LIMIT 1)
),
is_admin = users.is_admin OR EXISTS(
SELECT 1 FROM users WHERE id = ANY(merge_ids) AND is_admin = true
)
WHERE id = keep_id;
UPDATE sites SET user_id = keep_id WHERE user_id = ANY(merge_ids);
UPDATE update_requests SET user_id = keep_id WHERE user_id = ANY(merge_ids);
UPDATE sessions SET user_id = keep_id WHERE user_id = ANY(merge_ids);
DELETE FROM users WHERE id = ANY(merge_ids);
RAISE NOTICE 'Merged users with username %: kept ID %, merged IDs %',
dup_record.telegram_username, keep_id, merge_ids;
END LOOP;
END $$;
CREATE UNIQUE INDEX users_telegram_username_unique ON users(telegram_username)
WHERE telegram_username IS NOT NULL;

Some files were not shown because too many files have changed in this diff Show More