mirror of
https://github.com/Alexander-D-Karpov/webring.git
synced 2026-03-16 22:07:41 +03:00
Compare commits
18 Commits
v20251130-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65d4d3c3fc | ||
| 12fdd0ba64 | |||
| f774fa993f | |||
| d4607434d4 | |||
| 7ed6a23b63 | |||
|
|
d324c9490d | ||
| 499c4a807b | |||
| bd346671b8 | |||
| e98dd6e64b | |||
| 6bd5393c0c | |||
| d605bd37b7 | |||
| 3110c00e71 | |||
| 5d14f42352 | |||
| 0acd6686cc | |||
| 2c1885e6de | |||
| aba05aa6b6 | |||
| 73347edc67 | |||
| c1eed46f6d |
|
|
@ -8,4 +8,8 @@ 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_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
|
||||||
|
|
@ -34,7 +34,6 @@ linters:
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- govet
|
- govet
|
||||||
|
|
@ -57,7 +56,7 @@ linters:
|
||||||
|
|
||||||
goconst:
|
goconst:
|
||||||
min-len: 3
|
min-len: 3
|
||||||
min-occurrences: 3
|
min-occurrences: 10
|
||||||
|
|
||||||
gocritic:
|
gocritic:
|
||||||
enabled-tags: [diagnostic, experimental, opinionated, performance, style]
|
enabled-tags: [diagnostic, experimental, opinionated, performance, style]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
45
README.md
45
README.md
|
|
@ -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,31 +29,51 @@ make migrate-up
|
||||||
```
|
```
|
||||||
|
|
||||||
## Local Run
|
## Local Run
|
||||||
|
|
||||||
```
|
```
|
||||||
go run cmd/server/main.go
|
go run cmd/server/main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
or download prebuild version
|
or download prebuild version
|
||||||
|
|
||||||
```
|
```
|
||||||
wget https://github.com/Alexander-D-Karpov/webring/releases/latest/download/webring
|
wget https://github.com/Alexander-D-Karpov/webring/releases/latest/download/webring
|
||||||
chmod +x webring
|
chmod +x webring
|
||||||
./webring
|
./webring
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Customizing Notification Messages
|
||||||
|
|
||||||
|
Telegram notification templates live in the `messages/` directory (configurable via `MESSAGES_DIR` env var).
|
||||||
|
Each file is plain text with Go template syntax and MarkdownV2 formatting.
|
||||||
|
|
||||||
|
To customize a message, edit the corresponding `.txt` file.
|
||||||
|
|
||||||
|
Available templates:
|
||||||
|
|
||||||
|
| File | Event |
|
||||||
|
|-----------------------------|---------------------------------------------|
|
||||||
|
| `new_request_create.txt` | Admin notification: new site submitted |
|
||||||
|
| `new_request_update.txt` | Admin notification: site update requested |
|
||||||
|
| `approved_create.txt` | User notification: site submission approved |
|
||||||
|
| `approved_update.txt` | User notification: site update approved |
|
||||||
|
| `declined_create.txt` | User notification: site submission declined |
|
||||||
|
| `declined_update.txt` | User notification: site update declined |
|
||||||
|
| `admin_approved_create.txt` | Other admins: site creation approved |
|
||||||
|
| `admin_approved_update.txt` | Other admins: site update approved |
|
||||||
|
| `admin_declined_create.txt` | Other admins: site creation declined |
|
||||||
|
| `admin_declined_update.txt` | Other admins: site update declined |
|
||||||
|
| `site_online.txt` | Owner notification: site back online |
|
||||||
|
| `site_offline.txt` | Owner notification: site went offline |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- Access the dashboard at `http://localhost:8080/dashboard` (use the credentials set in your `.env` file)
|
- Access the dashboard at `http://localhost:8080/dashboard` (use the credentials set in your `.env` file)
|
||||||
- API endpoints:
|
- API endpoints:
|
||||||
- Next site: `GET /{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`
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -133,9 +140,8 @@ func registerHandlers(r *mux.Router, db *sql.DB) {
|
||||||
dashboard.RegisterHandlers(r, db)
|
dashboard.RegisterHandlers(r, db)
|
||||||
user.RegisterHandlers(r, db)
|
user.RegisterHandlers(r, db)
|
||||||
public.RegisterSubmissionHandlers(r, db)
|
public.RegisterSubmissionHandlers(r, db)
|
||||||
api.RegisterHandlers(r, db)
|
|
||||||
api.RegisterSwaggerHandlers(r)
|
api.RegisterSwaggerHandlers(r)
|
||||||
|
api.RegisterHandlers(r, db)
|
||||||
public.RegisterHandlers(r, db)
|
public.RegisterHandlers(r, db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: '/docs/swagger.json',
|
url: '/api/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>
|
||||||
10
go.mod
10
go.mod
|
|
@ -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
42
go.sum
|
|
@ -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
|
|
@ -18,7 +18,7 @@ import (
|
||||||
// @BasePath /
|
// @BasePath /
|
||||||
|
|
||||||
func RegisterSwaggerHandlers(r *mux.Router) {
|
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")
|
docsFS, err := fs.Sub(webring.Files, "docs")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -26,7 +26,7 @@ func RegisterSwaggerHandlers(r *mux.Router) {
|
||||||
return
|
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) {
|
func swaggerJSONHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
|
|
||||||
|
|
@ -59,18 +59,24 @@ 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, &user.TelegramID, &user.TelegramUsername,
|
&user.ID, &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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -42,7 +42,7 @@
|
||||||
<i class="ri-global-line"></i>
|
<i class="ri-global-line"></i>
|
||||||
<span>Public</span>
|
<span>Public</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/docs/" class="nav-item secondary">
|
<a href="/api/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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"`
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -34,11 +34,12 @@
|
||||||
My Dashboard
|
My Dashboard
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<a href="/docs/" class="user-action docs">
|
<a href="/api/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
|
||||||
|
|
@ -85,7 +86,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/docs/" class="action-card">
|
<a href="/api/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>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,14 @@
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<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;">
|
<form action="/submit" method="POST" style="max-width: 400px; margin: 0 auto;">
|
||||||
{{csrfField .Request}}
|
{{csrfField .Request}}
|
||||||
<div style="margin-bottom: 1rem;">
|
<div style="margin-bottom: 1rem;">
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
279
internal/telegram/templates.go
Normal file
279
internal/telegram/templates.go
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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
|
|
@ -42,7 +42,7 @@
|
||||||
<i class="ri-global-line"></i>
|
<i class="ri-global-line"></i>
|
||||||
<span>Public</span>
|
<span>Public</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/docs/" class="nav-item secondary">
|
<a href="/api/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>
|
||||||
|
|
|
||||||
82
internal/user/templates/request_error.html
Normal file
82
internal/user/templates/request_error.html
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Request Error - Webring</title>
|
||||||
|
<link rel="stylesheet" href="/static/dashboard.css">
|
||||||
|
<link rel="preconnect" href="https://rsms.me/">
|
||||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.3.0/fonts/remixicon.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="admin-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-left">
|
||||||
|
<a href="/admin" class="logo-link">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="ri-bubble-chart-fill"></i>
|
||||||
|
<span class="logo-text">Webring</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="page-info">
|
||||||
|
<h1>Request Error</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<nav class="header-nav">
|
||||||
|
<a href="/admin/requests" class="nav-item">
|
||||||
|
<i class="ri-arrow-left-line"></i>
|
||||||
|
<span>Back to Requests</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-icon">
|
||||||
|
<i class="ri-error-warning-line"></i>
|
||||||
|
</div>
|
||||||
|
<div class="error-content">
|
||||||
|
<h2>Unable to Process Request</h2>
|
||||||
|
<p class="error-message">{{.Error}}</p>
|
||||||
|
|
||||||
|
{{if .Request}}
|
||||||
|
<div class="error-details">
|
||||||
|
<h3>Request Details</h3>
|
||||||
|
{{if eq .Request.RequestType "create"}}
|
||||||
|
<p><strong>Type:</strong> Site Creation</p>
|
||||||
|
{{else}}
|
||||||
|
<p><strong>Type:</strong> Site Update</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Request.ChangedFields}}
|
||||||
|
<div class="changed-fields">
|
||||||
|
<strong>Requested Fields:</strong>
|
||||||
|
<ul>
|
||||||
|
{{range $key, $value := .Request.ChangedFields}}
|
||||||
|
<li><strong>{{$key}}:</strong> {{$value}}</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/admin/requests" class="btn btn-primary">
|
||||||
|
<i class="ri-arrow-left-line"></i>
|
||||||
|
Back to Requests
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-note">
|
||||||
|
<p><strong>Note:</strong> The request has not been deleted and can still be reviewed. You may need to contact the user to choose a different slug or make other changes.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -47,6 +47,17 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<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-header">
|
||||||
<div class="content-title">
|
<div class="content-title">
|
||||||
<h2>Your Sites</h2>
|
<h2>Your Sites</h2>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<i class="ri-global-line"></i>
|
<i class="ri-global-line"></i>
|
||||||
<span>Public</span>
|
<span>Public</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/docs/" class="nav-item secondary">
|
<a href="/api/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>
|
||||||
|
|
@ -123,6 +123,12 @@
|
||||||
{{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>
|
||||||
|
|
|
||||||
6
messages/admin_approved_create.txt
Normal file
6
messages/admin_approved_create.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*Request Approved*
|
||||||
|
|
||||||
|
*Admin:* {{.AdminName}}
|
||||||
|
*Action:* Approved site creation
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
13
messages/admin_approved_update.txt
Normal file
13
messages/admin_approved_update.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
*Update Approved*
|
||||||
|
|
||||||
|
*Admin:* {{.AdminName}}
|
||||||
|
*Action:* Approved site update
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
|
{{- if .Changes}}
|
||||||
|
|
||||||
|
*Changes:*
|
||||||
|
{{- range .Changes}}
|
||||||
|
• *{{.Key}}:* {{.Value}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
6
messages/admin_declined_create.txt
Normal file
6
messages/admin_declined_create.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*Request Declined*
|
||||||
|
|
||||||
|
*Admin:* {{.AdminName}}
|
||||||
|
*Action:* Declined site creation
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
6
messages/admin_declined_update.txt
Normal file
6
messages/admin_declined_update.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*Update Declined*
|
||||||
|
|
||||||
|
*Admin:* {{.AdminName}}
|
||||||
|
*Action:* Declined site update
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
7
messages/approved_create.txt
Normal file
7
messages/approved_create.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
*Request Approved*
|
||||||
|
|
||||||
|
Your site submission has been approved!
|
||||||
|
|
||||||
|
*Site:* {{.SiteName}}
|
||||||
|
|
||||||
|
Your site is now part of the webring.
|
||||||
10
messages/approved_update.txt
Normal file
10
messages/approved_update.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
*Update Approved*
|
||||||
|
|
||||||
|
Your site update request has been approved and the changes have been applied.
|
||||||
|
{{- if .Changes}}
|
||||||
|
|
||||||
|
*Applied changes:*
|
||||||
|
{{- range .Changes}}
|
||||||
|
• *{{.Key}}:* {{.Value}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
5
messages/declined_create.txt
Normal file
5
messages/declined_create.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
*Request Declined*
|
||||||
|
|
||||||
|
Your site submission request for *{{.SiteName}}* has been declined by an administrator.
|
||||||
|
|
||||||
|
If you have questions, please contact the webring administrator.
|
||||||
5
messages/declined_update.txt
Normal file
5
messages/declined_update.txt
Normal 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\.
|
||||||
8
messages/new_request_create.txt
Normal file
8
messages/new_request_create.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
*New Site Submission Request*
|
||||||
|
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Slug:* `{{.Slug}}`
|
||||||
|
*Site Name:* {{.SiteName}}
|
||||||
|
*URL:* {{.URL}}
|
||||||
|
|
||||||
|
*Submitted:* {{.Date}}
|
||||||
11
messages/new_request_update.txt
Normal file
11
messages/new_request_update.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
*Site Update Request*
|
||||||
|
|
||||||
|
*User:* {{.UserName}}
|
||||||
|
*Site:* {{.SiteName}} (`{{.SiteSlug}}`)
|
||||||
|
|
||||||
|
*Changes:*
|
||||||
|
{{- range .Changes}}
|
||||||
|
• *{{.Key}}:* {{.Value}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
*Submitted:* {{.Date}}
|
||||||
3
messages/site_offline.txt
Normal file
3
messages/site_offline.txt
Normal 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
3
messages/site_online.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*Site Status: Online*
|
||||||
|
|
||||||
|
Your site *{{.SiteName}}* is now responding and back online.
|
||||||
1
migrations/011_make_telegram_username_unique.down.sql
Normal file
1
migrations/011_make_telegram_username_unique.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP INDEX IF EXISTS users_telegram_username_unique;
|
||||||
53
migrations/011_make_telegram_username_unique.up.sql
Normal file
53
migrations/011_make_telegram_username_unique.up.sql
Normal 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;
|
||||||
4
migrations/012_lowercase_telegram_usernames.down.sql
Normal file
4
migrations/012_lowercase_telegram_usernames.down.sql
Normal 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;
|
||||||
57
migrations/012_lowercase_telegram_usernames.up.sql
Normal file
57
migrations/012_lowercase_telegram_usernames.up.sql
Normal 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;
|
||||||
5
migrations/013_handle_null_usernames.down.sql
Normal file
5
migrations/013_handle_null_usernames.down.sql
Normal 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;
|
||||||
38
migrations/013_handle_null_usernames.up.sql
Normal file
38
migrations/013_handle_null_usernames.up.sql
Normal 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;
|
||||||
1
migrations/014_add_enabled_column.down.sql
Normal file
1
migrations/014_add_enabled_column.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE sites DROP COLUMN enabled;
|
||||||
1
migrations/014_add_enabled_column.up.sql
Normal file
1
migrations/014_add_enabled_column.up.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE sites ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT true;
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user