refactor: bot submit
This commit is contained in:
parent
611619f180
commit
74149e5d6d
10 changed files with 85 additions and 34 deletions
16
.env.example
Normal file
16
.env.example
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# database
|
||||
DATABASE_POSTGRES_URL=postgresql://elisiei@localhost:5432/postgres
|
||||
|
||||
# discord
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
|
||||
# auth
|
||||
AUTH_PASETO_KEY=
|
||||
|
||||
# goose
|
||||
GOOSE_DRIVER=postgres
|
||||
GOOSE_DBSTRING=postgresql://elisiei@localhost:5432/postgres
|
||||
GOOSE_MIGRATION_DIR=./internal/db/sql/migrations
|
||||
|
||||
PRODUCTION=false
|
||||
44
README.md
44
README.md
|
|
@ -1,8 +1,44 @@
|
|||
# dbots
|
||||
|
||||
simple discord botlist
|
||||
discord botlist.
|
||||
|
||||
## todo
|
||||
## setup
|
||||
|
||||
* [ ] ratelimits
|
||||
* [ ] complete auth (with paseto)
|
||||
### 1. clone and install deps
|
||||
|
||||
### 2. create a `.env` file
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
then fill it in (see [environment variables](#environment-variables) below).
|
||||
|
||||
### 3. run migrations
|
||||
|
||||
```sh
|
||||
goose -dir internal/db/sql/migrations postgres "$DATABASE_POSTGRES_URL" up
|
||||
```
|
||||
|
||||
### 4. start the server
|
||||
|
||||
```sh
|
||||
go run .
|
||||
```
|
||||
|
||||
## environment variables
|
||||
|
||||
| variable | required | default | description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `DATABASE_POSTGRES_URL` | yes | — | postgres connection string, e.g. `postgres://user:pass@localhost:5432/dbots` |
|
||||
| `DATABASE_REDIS_URL` | no | — | redis connection string (not used yet) |
|
||||
| `DISCORD_CLIENT_ID` | yes | — | your discord application's client id |
|
||||
| `DISCORD_CLIENT_SECRET` | yes | — | your discord application's client secret |
|
||||
| `DISCORD_REDIRECT_URI` | no | `http://localhost:8080/auth/callback` | must match what's set in your discord app's oauth2 redirect urls |
|
||||
| `AUTH_PASETO_KEY` | yes | — | 32-byte key as 64 hex chars. generate with `openssl rand -hex 32` |
|
||||
| `SERVER_PORT` | no | `8080` | port to listen on |
|
||||
| `SERVER_ADDRESS` | no | `127.0.0.1` | address to bind to |
|
||||
|
||||
# license
|
||||
|
||||
cc0 1.0
|
||||
|
|
|
|||
BIN
dbots
BIN
dbots
Binary file not shown.
|
|
@ -6,10 +6,11 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
Database DatabaseConfig `env:"DATABASE_"`
|
||||
Server ServerConfig `env:"SERVER_"`
|
||||
Discord DiscordConfig `env:"DISCORD_"`
|
||||
Auth AuthConfig `env:"AUTH_"`
|
||||
Database DatabaseConfig `env:"DATABASE_"`
|
||||
Server ServerConfig `env:"SERVER_"`
|
||||
Discord DiscordConfig `env:"DISCORD_"`
|
||||
Auth AuthConfig `env:"AUTH_"`
|
||||
Production bool `env:"PRODUCTION,default=true"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ var (
|
|||
ErrSearchFailed = errors.New("No bots found fitting this filter")
|
||||
ErrMainOwnerAsCoOwner = errors.New("You cannot set yourself as a co-owner")
|
||||
ErrBotNotExists = errors.New("Bot does not exist inside Discord")
|
||||
ErrBotPrivate = errors.New("You cannot submit private bots")
|
||||
|
||||
// validation
|
||||
ErrInvalidID = errors.New("Invalid Discord id")
|
||||
|
|
|
|||
|
|
@ -12,12 +12,11 @@ import (
|
|||
|
||||
type contextKey string
|
||||
|
||||
// UserContextKey is exported so service routers can read the user from context.
|
||||
var UserContextKey contextKey = "user"
|
||||
|
||||
// AuthMiddleware reads the PASETO session cookie, verifies it, checks it
|
||||
// hasn't been revoked in the DB, then sets the *db.User on the context.
|
||||
// Does NOT block unauthenticated requests — use AuthGuardMiddleware for that.
|
||||
// hasn't been revoked in the db, then sets the *db.User on the context.
|
||||
// does not block unauthenticated requests
|
||||
func AuthMiddleware(q *db.Queries, pasetoKeyHex string) func(http.Handler) http.Handler {
|
||||
key, err := token.KeyFromHex(pasetoKeyHex)
|
||||
if err != nil {
|
||||
|
|
@ -34,13 +33,11 @@ func AuthMiddleware(q *db.Queries, pasetoKeyHex string) func(http.Handler) http.
|
|||
|
||||
claims, err := token.VerifyToken(key, c.Value)
|
||||
if err != nil {
|
||||
// Expired or tampered — clear the cookie and continue as anonymous.
|
||||
http.SetCookie(w, &http.Cookie{Name: "session", MaxAge: -1, Path: "/"})
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check the session hasn't been revoked server-side.
|
||||
if _, err := q.GetSession(r.Context(), claims.JTI); err != nil {
|
||||
http.SetCookie(w, &http.Cookie{Name: "session", MaxAge: -1, Path: "/"})
|
||||
next.ServeHTTP(w, r)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ func NewServer(queries *db.Queries, config config.Config) *Server {
|
|||
router.Use(middleware.Recoverer)
|
||||
router.Use(middleware.RequestID)
|
||||
router.Use(middleware.RealIP)
|
||||
router.Use(customMiddlewares.AuthMiddleware(queries, config.Auth.PasetoKey)) // todo: use this middleware only when necessary
|
||||
// i am using this globally cus it uses mocked data lol
|
||||
router.Use(customMiddlewares.AuthMiddleware(queries, config.Auth.PasetoKey))
|
||||
|
||||
return &Server{
|
||||
router: router,
|
||||
|
|
@ -49,7 +48,7 @@ func (s *Server) Register() {
|
|||
s.config.Discord.RedirectURI,
|
||||
)
|
||||
|
||||
authRouter := auth.NewRouter(s.queries, discordClient, s.config.Auth.PasetoKey)
|
||||
authRouter := auth.NewRouter(s.queries, discordClient, s.config)
|
||||
botRouter := bot.NewRouter(s.queries, discordClient)
|
||||
adminRouter := admin.NewRouter(s.queries)
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ func (s *Service) Callback(ctx context.Context, code string) (*db.User, *discord
|
|||
|
||||
user, err := s.q.GetUser(ctx, dUser.ID)
|
||||
if err != nil {
|
||||
// First login — create the user.
|
||||
user, err = s.q.CreateUser(ctx, db.CreateUserParams{
|
||||
ID: dUser.ID,
|
||||
Username: dUser.Username,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/config"
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/discord"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
"codeberg.org/nextgo/dbots/internal/middleware"
|
||||
"codeberg.org/nextgo/dbots/internal/token"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
|
@ -16,18 +18,18 @@ import (
|
|||
const cookieName = "session"
|
||||
|
||||
type Router struct {
|
||||
auth *Service
|
||||
router chi.Router
|
||||
pasetoKey string // hex-encoded AUTH_PASETO_KEY
|
||||
queries *db.Queries
|
||||
auth *Service
|
||||
router chi.Router
|
||||
config config.Config
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewRouter(q *db.Queries, client *discord.Client, pasetoKey string) *Router {
|
||||
func NewRouter(q *db.Queries, client *discord.Client, config config.Config) *Router {
|
||||
return &Router{
|
||||
auth: NewService(q, client),
|
||||
router: chi.NewRouter(),
|
||||
pasetoKey: pasetoKey,
|
||||
queries: q,
|
||||
auth: NewService(q, client),
|
||||
router: chi.NewRouter(),
|
||||
config: config,
|
||||
queries: q,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,8 +89,7 @@ func (r *Router) callback(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Generate a session ID (jti) and persist it to the DB for server-side revocation.
|
||||
jti, err := GenerateState() // reuses the same crypto/rand helper
|
||||
jti, err := GenerateState()
|
||||
if err != nil {
|
||||
render.Render(w, req, errorutil.ErrInternal(err))
|
||||
return
|
||||
|
|
@ -104,7 +105,7 @@ func (r *Router) callback(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
key, err := token.KeyFromHex(r.pasetoKey)
|
||||
key, err := token.KeyFromHex(r.config.Auth.PasetoKey)
|
||||
if err != nil {
|
||||
render.Render(w, req, errorutil.ErrInternal(err))
|
||||
return
|
||||
|
|
@ -123,25 +124,22 @@ func (r *Router) callback(w http.ResponseWriter, req *http.Request) {
|
|||
SameSite: http.SameSiteStrictMode,
|
||||
MaxAge: int(token.TokenDuration.Seconds()),
|
||||
Path: "/",
|
||||
// Secure: true, // enable in production (HTTPS)
|
||||
Secure: r.config.Production,
|
||||
})
|
||||
|
||||
http.Redirect(w, req, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
// POST /auth/logout — revoke the session server-side and clear the cookie.
|
||||
func (r *Router) logout(w http.ResponseWriter, req *http.Request) {
|
||||
c, err := req.Cookie(cookieName)
|
||||
if err != nil {
|
||||
// Already logged out.
|
||||
render.NoContent(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
key, err := token.KeyFromHex(r.pasetoKey)
|
||||
key, err := token.KeyFromHex(r.config.Auth.PasetoKey)
|
||||
if err == nil {
|
||||
if claims, err := token.VerifyToken(key, c.Value); err == nil {
|
||||
// Best-effort: ignore DB errors, the cookie will be cleared anyway.
|
||||
_ = r.queries.RevokeSession(req.Context(), claims.JTI)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ func (s *Service) Submit(ctx context.Context, data CreateBotRequest) (*db.Bot, e
|
|||
return nil, errorutil.ErrBotNotExists // todo: some old bots have different client id (not the same as the user id)
|
||||
}
|
||||
|
||||
if !application.BotPublic {
|
||||
return nil, errorutil.ErrBotPrivate
|
||||
}
|
||||
|
||||
var count int32
|
||||
b, err := s.q.CreateBot(ctx, db.CreateBotParams{
|
||||
ID: data.ID,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue