Compare commits

...

18 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
50 changed files with 2204 additions and 572 deletions

View File

@ -8,4 +8,8 @@ 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=
CSRF_TRUSTED_ORIGINS=
REQUIRE_LOGIN_FOR_SUBMIT=false
CHECKER_WORKERS=5
CHECKER_DOWN_THRESHOLD=3
MESSAGES_DIR=messages

View File

@ -34,7 +34,6 @@ linters:
- gochecknoinits
- goconst
- gocritic
- gocyclo
- goprintffuncname
- gosec
- govet
@ -57,7 +56,7 @@ linters:
goconst:
min-len: 3
min-occurrences: 3
min-occurrences: 10
gocritic:
enabled-tags: [diagnostic, experimental, opinionated, performance, style]

View File

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

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
- 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,31 +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 /{slug}/next/data`
- Previous site: `GET /{slug}/prev/data`
- Random site: `GET /{slug}/random/data`
- Full data for a site: `GET /{slug}/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:
- Visit site: `GET /{slug}`
- Next site: `GET /{slug}/next`
- Previous site: `GET /{slug}/prev`
- Random site: `GET /{slug}/random`
- Visit site: `GET /{slug}`
- Next site: `GET /{slug}/next`
- Previous site: `GET /{slug}/prev`
- Random site: `GET /{slug}/random`

View File

@ -21,6 +21,7 @@ import (
"webring/internal/dashboard"
"webring/internal/database"
"webring/internal/public"
"webring/internal/telegram"
"webring/internal/uptime"
"webring/internal/user"
@ -101,6 +102,12 @@ func main() {
}
}()
messagesDir := os.Getenv("MESSAGES_DIR")
if messagesDir == "" {
messagesDir = "messages"
}
telegram.InitTemplates(messagesDir)
startBackgroundServices(db)
r := mux.NewRouter()
@ -133,9 +140,8 @@ func registerHandlers(r *mux.Router, db *sql.DB) {
dashboard.RegisterHandlers(r, db)
user.RegisterHandlers(r, db)
public.RegisterSubmissionHandlers(r, db)
api.RegisterHandlers(r, db)
api.RegisterSwaggerHandlers(r)
api.RegisterHandlers(r, db)
public.RegisterHandlers(r, db)
}

View File

@ -1,30 +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>
<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: '/docs/swagger.json',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ],
plugins: [ SwaggerUIBundle.plugins.DownloadUrl ],
layout: "StandaloneLayout"
});
};
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>

10
go.mod
View File

@ -3,15 +3,15 @@ module webring
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
github.com/andybalholm/cascadia v1.3.3 // 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/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=
@ -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/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=
@ -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.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

View File

@ -18,7 +18,7 @@ import (
// @BasePath /
func RegisterSwaggerHandlers(r *mux.Router) {
r.HandleFunc("/docs/swagger.json", swaggerJSONHandler).Methods("GET")
r.HandleFunc("/api/docs/swagger.json", swaggerJSONHandler).Methods("GET")
docsFS, err := fs.Sub(webring.Files, "docs")
if err != nil {
@ -26,7 +26,7 @@ func RegisterSwaggerHandlers(r *mux.Router) {
return
}
r.PathPrefix("/docs/").Handler(http.StripPrefix("/docs/", http.FileServer(http.FS(docsFS))))
r.PathPrefix("/api/docs/").Handler(http.StripPrefix("/api/docs/", http.FileServer(http.FS(docsFS))))
}
func swaggerJSONHandler(w http.ResponseWriter, _ *http.Request) {

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,7 @@
<i class="ri-global-line"></i>
<span>Public</span>
</a>
<a href="/docs/" class="nav-item secondary">
<a href="/api/docs/" class="nav-item secondary">
<i class="ri-book-line"></i>
<span>API</span>
</a>
@ -69,6 +69,7 @@
<th class="col-url">URL</th>
<th class="col-telegram">TELEGRAM</th>
<th class="col-status">STATUS</th>
<th class="col-enabled">ENABLED</th>
<th class="col-ping">PING</th>
<th class="col-actions">ACTIONS</th>
</tr>
@ -97,6 +98,7 @@
<span class="status-badge new">New</span>
</td>
<td></td>
<td></td>
<td>
<div class="actions">
<button type="submit" form="form-new" class="btn btn-primary btn-sm">
@ -169,6 +171,18 @@
<span class="status-badge down">DOWN</span>
{{end}}
</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">
{{if .LastCheck}}
<span class="ping-value">{{.LastCheck}}</span>

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ type Site struct {
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"`

File diff suppressed because it is too large Load Diff

View File

@ -34,11 +34,12 @@
My Dashboard
</a>
{{end}}
<a href="/docs/" class="user-action docs">
<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
@ -85,7 +86,7 @@
</div>
</a>
<a href="/docs/" class="action-card">
<a href="/api/docs/" class="action-card">
<i class="ri-book-line"></i>
<div class="action-content">
<div class="action-title">API Docs</div>

View File

@ -17,6 +17,14 @@
</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;">

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

@ -42,7 +42,7 @@
<i class="ri-global-line"></i>
<span>Public</span>
</a>
<a href="/docs/" class="nav-item secondary">
<a href="/api/docs/" class="nav-item secondary">
<i class="ri-book-line"></i>
<span>API</span>
</a>

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>

View File

@ -47,6 +47,17 @@
</header>
<main>
{{if .Error}}
<div class="error-banner">
<div class="error-banner-icon">
<i class="ri-error-warning-line"></i>
</div>
<div class="error-banner-content">
<span class="error-banner-text">{{.Error}}</span>
</div>
</div>
{{end}}
<div class="content-header">
<div class="content-title">
<h2>Your Sites</h2>

View File

@ -42,7 +42,7 @@
<i class="ri-global-line"></i>
<span>Public</span>
</a>
<a href="/docs/" class="nav-item secondary">
<a href="/api/docs/" class="nav-item secondary">
<i class="ri-book-line"></i>
<span>API</span>
</a>
@ -123,6 +123,12 @@
{{if ne .TelegramID 0}}{{.TelegramID}}{{else}}<em>Not set</em>{{end}}
</span>
</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">
<span class="detail-label">Joined:</span>
<span class="detail-value">{{.CreatedAt.Format "Jan 2, 2006"}}</span>

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 @@
DROP INDEX IF EXISTS users_telegram_username_unique;

View File

@ -0,0 +1,53 @@
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;

View File

@ -0,0 +1,4 @@
DROP INDEX IF EXISTS users_telegram_username_unique_lower;
CREATE UNIQUE INDEX users_telegram_username_unique ON users(telegram_username)
WHERE telegram_username IS NOT NULL;

View File

@ -0,0 +1,57 @@
DO $$
DECLARE
dup_record RECORD;
keep_id INTEGER;
merge_ids INTEGER[];
BEGIN
FOR dup_record IN
SELECT LOWER(telegram_username) as lower_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 LOWER(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.lower_username, keep_id, merge_ids;
END LOOP;
END $$;
UPDATE users SET telegram_username = LOWER(telegram_username) WHERE telegram_username IS NOT NULL;
DROP INDEX IF EXISTS users_telegram_username_unique;
CREATE UNIQUE INDEX users_telegram_username_unique_lower ON users(LOWER(telegram_username))
WHERE telegram_username IS NOT NULL;

View File

@ -0,0 +1,5 @@
DROP INDEX IF EXISTS users_single_anonymous;
DROP INDEX IF EXISTS users_telegram_username_unique_lower;
CREATE UNIQUE INDEX users_telegram_username_unique_lower ON users(LOWER(telegram_username))
WHERE telegram_username IS NOT NULL;

View File

@ -0,0 +1,38 @@
DO $$
DECLARE
keep_id INTEGER;
merge_ids INTEGER[];
anon_users INTEGER[];
BEGIN
SELECT array_agg(id ORDER BY created_at ASC) INTO anon_users
FROM users
WHERE telegram_id IS NULL AND telegram_username IS NULL;
IF array_length(anon_users, 1) > 1 THEN
keep_id := anon_users[1];
merge_ids := anon_users[2:array_length(anon_users, 1)];
UPDATE users SET
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 anonymous users: kept ID %, merged IDs %', keep_id, merge_ids;
END IF;
END $$;
DROP INDEX IF EXISTS users_telegram_username_unique_lower;
DROP INDEX IF EXISTS users_telegram_username_unique;
CREATE UNIQUE INDEX users_telegram_username_unique_lower ON users(LOWER(telegram_username))
WHERE telegram_username IS NOT NULL;
CREATE UNIQUE INDEX users_single_anonymous ON users((1))
WHERE telegram_id IS NULL AND telegram_username IS NULL;

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 = ../.;
subPackages = [ "cmd/server" ];
vendorHash = "sha256-bwCfn3AEWKJmsy8FTkLqtx5VXIjOZ7Nux6wAogeb9JM=";
vendorHash = "sha256-l8JA0MKEEngPb5R4r3Xd0MhB8Ah2x1mwREgPmqF1D+I=";
postInstall = ''
mv $out/bin/server $out/bin/webring-server

File diff suppressed because it is too large Load Diff