Compare commits

...

8 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
30 changed files with 739 additions and 384 deletions

View File

@ -11,3 +11,5 @@ CSRF_AUTH_KEY=your_csrf_auth_key
CSRF_TRUSTED_ORIGINS= CSRF_TRUSTED_ORIGINS=
REQUIRE_LOGIN_FOR_SUBMIT=false REQUIRE_LOGIN_FOR_SUBMIT=false
CHECKER_WORKERS=5 CHECKER_WORKERS=5
CHECKER_DOWN_THRESHOLD=3
MESSAGES_DIR=messages

View File

@ -30,6 +30,7 @@ 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/docs ./docs
COPY --from=builder /build/messages ./messages
# Create media directory # Create media directory
RUN mkdir -p media RUN mkdir -p media

View File

@ -7,6 +7,10 @@ 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
@ -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,20 +29,40 @@ 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

View File

@ -21,6 +21,7 @@ import (
"webring/internal/dashboard" "webring/internal/dashboard"
"webring/internal/database" "webring/internal/database"
"webring/internal/public" "webring/internal/public"
"webring/internal/telegram"
"webring/internal/uptime" "webring/internal/uptime"
"webring/internal/user" "webring/internal/user"
@ -101,6 +102,12 @@ func main() {
} }
}() }()
messagesDir := os.Getenv("MESSAGES_DIR")
if messagesDir == "" {
messagesDir = "messages"
}
telegram.InitTemplates(messagesDir)
startBackgroundServices(db) startBackgroundServices(db)
r := mux.NewRouter() r := mux.NewRouter()

8
go.mod
View File

@ -3,15 +3,15 @@ module webring
go 1.25.0 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/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
github.com/gorilla/securecookie v1.1.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
) )

42
go.sum
View File

@ -1,7 +1,12 @@
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 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0=
@ -14,21 +19,41 @@ 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=
@ -36,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

View File

@ -77,6 +77,7 @@ func RegisterHandlers(r *mux.Router, db *sql.DB) {
adminRouter.HandleFunc("/update/{id}", updateSiteHandler(db)).Methods("POST") adminRouter.HandleFunc("/update/{id}", updateSiteHandler(db)).Methods("POST")
adminRouter.HandleFunc("/reorder/{id}/{direction}", reorderSiteHandler(db)).Methods("POST") adminRouter.HandleFunc("/reorder/{id}/{direction}", reorderSiteHandler(db)).Methods("POST")
adminRouter.HandleFunc("/move/{id}/{position}", moveSiteHandler(db)).Methods("POST") adminRouter.HandleFunc("/move/{id}/{position}", moveSiteHandler(db)).Methods("POST")
adminRouter.HandleFunc("/toggle-enabled/{id}", toggleEnabledHandler(db)).Methods("POST")
} }
func renderTemplate(w http.ResponseWriter, name string, data interface{}) error { func renderTemplate(w http.ResponseWriter, name string, data interface{}) error {
@ -172,8 +173,9 @@ func addSiteHandler(db *sql.DB) http.HandlerFunc {
} }
} }
_, err = db.Exec("INSERT INTO sites (id, slug, name, url, display_order, user_id) VALUES ($1, $2, $3, $4, $5, $6)", _, err = db.Exec("INSERT INTO sites (id, slug, name, url, display_order, user_id, enabled) "+
id, slug, name, url, maxDisplayOrder+1, userID) "VALUES ($1, $2, $3, $4, $5, $6, $7)",
id, slug, name, url, maxDisplayOrder+1, userID, true)
if err != nil { if err != nil {
var pqErr *pq.Error var pqErr *pq.Error
if errors.As(err, &pqErr) && pqErr.Code.Name() == uniqueViolation { if errors.As(err, &pqErr) && pqErr.Code.Name() == uniqueViolation {
@ -306,6 +308,21 @@ func updateSiteHandler(db *sql.DB) http.HandlerFunc {
} }
} }
func toggleEnabledHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
_, err := db.Exec("UPDATE sites SET enabled = NOT enabled WHERE id = $1", id)
if err != nil {
log.Printf("Error toggling site enabled status: %v", err)
http.Error(w, "Error toggling site status", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/admin", http.StatusSeeOther)
}
}
func reorderSiteHandler(db *sql.DB) http.HandlerFunc { func reorderSiteHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"] idStr := mux.Vars(r)["id"]
@ -528,7 +545,7 @@ func moveSiteHandler(db *sql.DB) http.HandlerFunc {
func getAllSites(db *sql.DB) ([]models.Site, error) { func getAllSites(db *sql.DB) ([]models.Site, error) {
rows, err := db.QueryContext( rows, err := db.QueryContext(
context.Background(), ` context.Background(), `
SELECT s.id, s.slug, s.name, s.url, s.is_up, s.last_check, s.favicon, s.user_id, u.telegram_username SELECT s.id, s.slug, s.name, s.url, s.is_up, s.enabled, s.last_check, s.favicon, s.user_id, u.telegram_username
FROM sites s FROM sites s
LEFT JOIN users u ON s.user_id = u.id LEFT JOIN users u ON s.user_id = u.id
ORDER BY s.display_order ORDER BY s.display_order
@ -547,7 +564,7 @@ func getAllSites(db *sql.DB) ([]models.Site, error) {
var site models.Site var site models.Site
var telegramUsername sql.NullString var telegramUsername sql.NullString
scanErr := rows.Scan(&site.ID, &site.Slug, &site.Name, &site.URL, &site.IsUp, scanErr := rows.Scan(&site.ID, &site.Slug, &site.Name, &site.URL, &site.IsUp,
&site.LastCheck, &site.Favicon, &site.UserID, &telegramUsername) &site.Enabled, &site.LastCheck, &site.Favicon, &site.UserID, &telegramUsername)
if scanErr != nil { if scanErr != nil {
return nil, scanErr return nil, scanErr
} }

View File

@ -69,6 +69,7 @@
<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>
@ -97,6 +98,7 @@
<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">
@ -169,6 +171,18 @@
<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>

View File

@ -6,6 +6,7 @@ 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"`

View File

@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"html"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
@ -46,8 +45,7 @@ func InitTemplates(t *template.Template) {
} }
func sanitizeInput(input string) string { func sanitizeInput(input string) string {
trimmed := strings.TrimSpace(input) return strings.TrimSpace(input)
return html.EscapeString(trimmed)
} }
func sanitizeURL(input string) string { func sanitizeURL(input string) string {
@ -55,7 +53,7 @@ func sanitizeURL(input string) string {
if !strings.HasPrefix(trimmed, "http://") && !strings.HasPrefix(trimmed, "https://") { if !strings.HasPrefix(trimmed, "http://") && !strings.HasPrefix(trimmed, "https://") {
trimmed = "https://" + trimmed trimmed = "https://" + trimmed
} }
return html.EscapeString(trimmed) return trimmed
} }
func RegisterHandlers(r *mux.Router, db *sql.DB) { func RegisterHandlers(r *mux.Router, db *sql.DB) {
@ -383,7 +381,8 @@ func getOrCreateAnonymousAdminUser(db *sql.DB) (int, error) {
} }
func getRespondingSites(db *sql.DB) ([]models.PublicSite, error) { func getRespondingSites(db *sql.DB) ([]models.PublicSite, error) {
rows, err := db.Query("SELECT slug, name, url, favicon FROM sites WHERE is_up = true ORDER BY display_order") rows, err := db.Query("SELECT slug, name, url, favicon FROM sites " +
"WHERE is_up = true AND enabled = true ORDER BY display_order")
if err != nil { if err != nil {
return nil, err return nil, err
} }

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

@ -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.

View File

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

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

View File

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

View File

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

3
messages/site_online.txt Normal file
View File

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

View File

@ -0,0 +1 @@
ALTER TABLE sites DROP COLUMN enabled;

View File

@ -0,0 +1 @@
ALTER TABLE sites ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT true;

View File

@ -5,7 +5,7 @@
src = ../.; src = ../.;
subPackages = [ "cmd/server" ]; subPackages = [ "cmd/server" ];
vendorHash = "sha256-bwCfn3AEWKJmsy8FTkLqtx5VXIjOZ7Nux6wAogeb9JM="; vendorHash = "sha256-l8JA0MKEEngPb5R4r3Xd0MhB8Ah2x1mwREgPmqF1D+I=";
postInstall = '' postInstall = ''
mv $out/bin/server $out/bin/webring-server mv $out/bin/server $out/bin/webring-server

View File

@ -1528,3 +1528,8 @@ input[type=url] {
font-size: 0.8125rem; font-size: 0.8125rem;
} }
} }
.col-enabled {
width: 5rem;
text-align: center;
}