feat: add SMTP email support and fix profile/permissions bugs

This commit is contained in:
root
2026-06-30 19:29:08 +00:00
parent 4ded582dc4
commit f4437c5e1d
26 changed files with 643 additions and 129 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ func SetSessionCookie(w http.ResponseWriter, cookieName, token string, duration
Path: "/",
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteStrictMode,
SameSite: http.SameSiteLaxMode,
MaxAge: int(duration.Seconds()),
})
}
+222
View File
@@ -0,0 +1,222 @@
package auth
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions"
)
func generateToken() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
func (h *Handler) RequestVerification(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
user, err := h.getProfile(r.Context(), userID)
if err != nil {
http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
return
}
token := generateToken()
_, err = h.db.ExecContext(r.Context(), `
INSERT INTO user_tokens (user_id, token, type, expires_at)
VALUES ($1, $2, 'verify_email', NOW() + INTERVAL '24 hours')
`, userID, token)
if err != nil {
http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError)
return
}
appURL := "http://" + h.cfg.Host
if h.cfg.Port != "80" && h.cfg.Port != "443" {
appURL += ":" + h.cfg.Port
}
if err := h.mailer.SendVerificationEmail(user.Email, user.Username, token, appURL); err != nil {
http.Error(w, `{"error":"failed to send email"}`, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
var req struct {
Token string `json:"token"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
var userID string
err := h.db.QueryRowContext(r.Context(), `
DELETE FROM user_tokens
WHERE token = $1 AND type = 'verify_email' AND expires_at > NOW()
RETURNING user_id
`, req.Token).Scan(&userID)
if err != nil {
http.Error(w, `{"error":"invalid or expired token"}`, http.StatusBadRequest)
return
}
_, err = h.db.ExecContext(r.Context(), `
UPDATE users SET email_verified = TRUE WHERE id = $1
`, userID)
if err != nil {
http.Error(w, `{"error":"failed to update user"}`, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
var userID, username string
err := h.db.QueryRowContext(r.Context(), `
SELECT id, username FROM users WHERE email = $1
`, req.Email).Scan(&userID, &username)
if err != nil {
// Silent fail to prevent email enumeration
w.WriteHeader(http.StatusNoContent)
return
}
token := generateToken()
_, err = h.db.ExecContext(r.Context(), `
INSERT INTO user_tokens (user_id, token, type, expires_at)
VALUES ($1, $2, 'reset_password', NOW() + INTERVAL '1 hour')
`, userID, token)
if err != nil {
http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError)
return
}
appURL := "http://" + h.cfg.Host
if h.cfg.Port != "80" && h.cfg.Port != "443" {
appURL += ":" + h.cfg.Port
}
h.mailer.SendPasswordResetEmail(req.Email, username, token, appURL)
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
var req struct {
Token string `json:"token"`
NewPassword string `json:"new_password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
if len(req.NewPassword) < 8 {
http.Error(w, `{"error":"password too short"}`, http.StatusBadRequest)
return
}
var userID string
err := h.db.QueryRowContext(r.Context(), `
DELETE FROM user_tokens
WHERE token = $1 AND type = 'reset_password' AND expires_at > NOW()
RETURNING user_id
`, req.Token).Scan(&userID)
if err != nil {
http.Error(w, `{"error":"invalid or expired token"}`, http.StatusBadRequest)
return
}
hash, err := HashPassword(req.NewPassword)
if err != nil {
http.Error(w, `{"error":"failed to hash password"}`, http.StatusInternalServerError)
return
}
_, err = h.db.ExecContext(r.Context(), `
UPDATE users SET password_hash = $2 WHERE id = $1
`, userID, hash)
if err != nil {
http.Error(w, `{"error":"failed to update user"}`, http.StatusInternalServerError)
return
}
// Delete all sessions to force re-login
h.db.ExecContext(r.Context(), `DELETE FROM sessions WHERE user_id = $1`, userID)
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) AdminAnnounce(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
var req struct {
Subject string `json:"subject"`
Body string `json:"body"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
// Check if user is an admin across the whole system.
// For now, let's assume if they have ADMINISTRATOR on ANY server, they can announce.
// Or maybe there is a global admin flag? Let's check permissions.
// Actually, let's just make sure they have MANAGE_SERVER on a specific server or something.
// DumpsterChat doesn't have system-level admins out of the box, only Server-level.
// Let's require them to be the owner of at least one server, or have ADMINISTRATOR on a server?
// The prompt just said "admin sent announcements". Let's allow it for users who are in any server with ADMINISTRATOR.
var isAdmin bool
err := h.db.QueryRowContext(r.Context(), `
SELECT EXISTS (
SELECT 1 FROM member_roles mr
JOIN roles r ON mr.role_id = r.id
WHERE mr.user_id = $1 AND (r.permissions & $2) = $2
)
`, userID, permissions.ADMINISTRATOR).Scan(&isAdmin)
if err != nil || !isAdmin {
http.Error(w, `{"error":"must be an administrator"}`, http.StatusForbidden)
return
}
rows, err := h.db.QueryContext(r.Context(), `SELECT email FROM users`)
if err != nil {
http.Error(w, `{"error":"failed to fetch users"}`, http.StatusInternalServerError)
return
}
defer rows.Close()
for rows.Next() {
var email string
if err := rows.Scan(&email); err == nil && email != "" {
// This is synchronous and could block, ideally use a queue, but this is fine for now
h.mailer.SendAnnouncement(email, req.Subject, req.Body)
}
}
w.WriteHeader(http.StatusNoContent)
}
+103 -26
View File
@@ -11,6 +11,7 @@ import (
"strings"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/email"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
"github.com/go-chi/chi/v5"
@@ -21,14 +22,16 @@ type Handler struct {
cfg *config.Config
sessions *SessionStore
hub *gateway.Hub
mailer *email.Mailer
}
func NewHandler(db *sql.DB, cfg *config.Config, hub *gateway.Hub) *Handler {
func NewHandler(db *sql.DB, cfg *config.Config, hub *gateway.Hub, mailer *email.Mailer) *Handler {
return &Handler{
db: db,
cfg: cfg,
sessions: NewSessionStore(db, cfg),
hub: hub,
mailer: mailer,
}
}
@@ -36,9 +39,14 @@ func (h *Handler) RegisterPublicRoutes(r chi.Router) {
r.Post("/register", h.Register)
r.Post("/login/password", h.Login)
r.Post("/logout", h.Logout)
r.Post("/verify-email", h.VerifyEmail)
r.Post("/request-password-reset", h.RequestPasswordReset)
r.Post("/reset-password", h.ResetPassword)
}
func (h *Handler) RegisterProtectedRoutes(r chi.Router) {
r.Post("/auth/request-verification", h.RequestVerification)
r.Post("/auth/admin/announce", h.AdminAnnounce)
r.Get("/auth/me", h.Me)
r.Patch("/auth/me", h.UpdateProfile)
r.Put("/auth/me/password", h.ChangePassword)
@@ -210,16 +218,16 @@ type loginRequest struct {
}
type updateProfileRequest struct {
AvatarURL string `json:"avatar_url"`
DisplayName string `json:"display_name"`
Bio string `json:"bio"`
AccentColor string `json:"accent_color"`
StatusText string `json:"status_text"`
Status string `json:"status"`
BannerURL string `json:"banner_url"`
Tagline string `json:"tagline"`
Pronouns string `json:"pronouns"`
SocialLinks []struct {
AvatarURL *string `json:"avatar_url"`
DisplayName *string `json:"display_name"`
Bio *string `json:"bio"`
AccentColor *string `json:"accent_color"`
StatusText *string `json:"status_text"`
Status *string `json:"status"`
BannerURL *string `json:"banner_url"`
Tagline *string `json:"tagline"`
Pronouns *string `json:"pronouns"`
SocialLinks *[]struct {
Platform string `json:"platform"`
URL string `json:"url"`
} `json:"social_links"`
@@ -410,8 +418,16 @@ func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
// @Failure 401 {object} map[string]string
// @Router /auth/me [get]
func (h *Handler) Me(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
cookie, _ := r.Cookie(h.cfg.Session.CookieName)
if cookie != nil {
fmt.Printf("Me Request: Cookie present! value=%s\n", cookie.Value)
} else {
fmt.Printf("Me Request: NO COOKIE!\n")
}
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
fmt.Printf("Me Request: NO USER ID IN CONTEXT\n")
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
@@ -450,52 +466,113 @@ func (h *Handler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
return
}
if len(req.Bio) > 250 {
// Fetch existing user to merge fields
var existing struct {
DisplayName string
Bio string
AccentColor string
StatusText string
Status string
BannerURL string
Tagline string
Pronouns string
AvatarURL string
SocialLinks []struct {
Platform string `json:"platform"`
URL string `json:"url"`
}
}
var socialLinksBytes []byte
var bannerURL, tagline, pronouns, avatar sql.NullString
err := h.db.QueryRowContext(r.Context(), `
SELECT display_name, bio, accent_color, status_text, status, banner_url, tagline, pronouns, avatar, social_links
FROM users WHERE id = $1
`, userID).Scan(&existing.DisplayName, &existing.Bio, &existing.AccentColor, &existing.StatusText, &existing.Status, &bannerURL, &tagline, &pronouns, &avatar, &socialLinksBytes)
if err != nil {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
existing.BannerURL = bannerURL.String
existing.Tagline = tagline.String
existing.Pronouns = pronouns.String
existing.AvatarURL = avatar.String
if len(socialLinksBytes) > 0 {
json.Unmarshal(socialLinksBytes, &existing.SocialLinks)
}
// Helper to merge strings
mergeStr := func(ptr *string, old string) string {
if ptr != nil {
return *ptr
}
return old
}
newBio := mergeStr(req.Bio, existing.Bio)
newStatusText := mergeStr(req.StatusText, existing.StatusText)
newDisplayName := mergeStr(req.DisplayName, existing.DisplayName)
newTagline := mergeStr(req.Tagline, existing.Tagline)
newPronouns := mergeStr(req.Pronouns, existing.Pronouns)
newAccentColor := mergeStr(req.AccentColor, existing.AccentColor)
newStatus := mergeStr(req.Status, existing.Status)
newBannerURL := mergeStr(req.BannerURL, existing.BannerURL)
newAvatarURL := mergeStr(req.AvatarURL, existing.AvatarURL)
if len(newBio) > 250 {
http.Error(w, `{"error":"bio too long"}`, http.StatusBadRequest)
return
}
if len(req.StatusText) > 128 {
if len(newStatusText) > 128 {
http.Error(w, `{"error":"status text too long"}`, http.StatusBadRequest)
return
}
if len(req.DisplayName) > 32 {
if len(newDisplayName) > 32 {
http.Error(w, `{"error":"display name too long"}`, http.StatusBadRequest)
return
}
if len(req.Tagline) > 128 {
if len(newTagline) > 128 {
http.Error(w, `{"error":"tagline too long"}`, http.StatusBadRequest)
return
}
if len(req.Pronouns) > 40 {
if len(newPronouns) > 40 {
http.Error(w, `{"error":"pronouns too long"}`, http.StatusBadRequest)
return
}
if req.AccentColor != "" && len(req.AccentColor) != 7 {
if newAccentColor != "" && len(newAccentColor) != 7 {
http.Error(w, `{"error":"accent color must be 7 char hex"}`, http.StatusBadRequest)
return
}
if req.Status != "" && !isValidStatus(req.Status) {
if newStatus != "" && !isValidStatus(newStatus) {
http.Error(w, `{"error":"invalid status"}`, http.StatusBadRequest)
return
}
socialLinksJSON, _ := json.Marshal(req.SocialLinks)
_, err := h.db.ExecContext(r.Context(), `
var socialLinksJSON []byte
if req.SocialLinks != nil {
socialLinksJSON, _ = json.Marshal(*req.SocialLinks)
} else {
socialLinksJSON, _ = json.Marshal(existing.SocialLinks)
}
_, err = h.db.ExecContext(r.Context(), `
UPDATE users
SET display_name = $2, bio = $3, accent_color = $4, status_text = $5, status = COALESCE(NULLIF($6, ''), status),
banner_url = NULLIF($7, ''), tagline = NULLIF($8, ''), pronouns = NULLIF($9, ''), social_links = $10, avatar = COALESCE(NULLIF($11, ''), avatar)
SET display_name = $2, bio = $3, accent_color = $4, status_text = $5, status = $6,
banner_url = $7, tagline = $8, pronouns = $9, social_links = $10, avatar = $11
WHERE id = $1
`, userID, req.DisplayName, req.Bio, req.AccentColor, req.StatusText, req.Status, req.BannerURL, req.Tagline, req.Pronouns, socialLinksJSON, req.AvatarURL)
`, userID, newDisplayName, newBio, newAccentColor, newStatusText, newStatus, newBannerURL, newTagline, newPronouns, socialLinksJSON, newAvatarURL)
if err != nil {
http.Error(w, `{"error":"update failed"}`, http.StatusInternalServerError)
return
}
if req.Status != "" && h.hub != nil {
if req.Status != nil && h.hub != nil {
user, err := h.getProfile(r.Context(), userID)
if err == nil {
h.hub.SetUserStatus(userID, req.Status)
h.hub.BroadcastPresence(userID, user.Username, req.Status)
h.hub.SetUserStatus(userID, *req.Status)
h.hub.BroadcastPresence(userID, user.Username, *req.Status)
}
}
+10 -1
View File
@@ -204,7 +204,16 @@ func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
return
}
channels = append(channels, ch)
// Check VIEW_CHANNEL permission
allowed, err := h.checker.CheckChannelPermission(r.Context(), serverID, userID, ch.ID, permissions.VIEW_CHANNEL)
if err != nil {
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
return
}
if allowed {
channels = append(channels, ch)
}
}
if err := rows.Err(); err != nil {
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
+14
View File
@@ -51,6 +51,14 @@ type Config struct {
WebPush WebPushConfig
Session SessionConfig
Giphy GiphyConfig
SMTP SMTPConfig
}
type SMTPConfig struct {
Host string
Port string
Username string
Password string
}
type DatabaseConfig struct {
@@ -134,6 +142,12 @@ func Load() *Config {
Giphy: GiphyConfig{
APIKey: getEnv("GIPHY_API_KEY", ""),
},
SMTP: SMTPConfig{
Host: getEnv("SMTP_HOST", ""),
Port: getEnv("SMTP_PORT", ""),
Username: getEnv("SMTP_USERNAME", ""),
Password: getEnv("SMTP_PASSWORD", ""),
},
}
}
+11
View File
@@ -47,8 +47,19 @@ CREATE TABLE IF NOT EXISTS users (
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT FALSE;
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE TABLE IF NOT EXISTS user_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(255) NOT NULL UNIQUE,
type VARCHAR(32) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+2 -2
View File
@@ -228,14 +228,14 @@ func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
func (h *Handler) buildResponse(ctx context.Context, convID string) (conversationResponse, error) {
var resp conversationResponse
err := h.db.QueryRowContext(ctx, `
SELECT id, type, name, created_at::text FROM conversations WHERE id = $1
SELECT id, type, COALESCE(name, ''), created_at::text FROM conversations WHERE id = $1
`, convID).Scan(&resp.ID, &resp.Type, &resp.Name, &resp.CreatedAt)
if err != nil {
return resp, err
}
memberRows, err := h.db.QueryContext(ctx, `
SELECT u.id, u.username, u.display_name, COALESCE(u.avatar, '')
SELECT u.id, u.username, COALESCE(u.display_name, ''), COALESCE(u.avatar, '')
FROM conversation_members cm
JOIN users u ON u.id = cm.user_id
WHERE cm.conversation_id = $1
+74
View File
@@ -0,0 +1,74 @@
package email
import (
"fmt"
"net/smtp"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
)
type Mailer struct {
cfg config.SMTPConfig
}
func NewMailer(cfg config.SMTPConfig) *Mailer {
return &Mailer{cfg: cfg}
}
func (m *Mailer) Send(to, subject, body string) error {
if m.cfg.Host == "" || m.cfg.Port == "" {
return fmt.Errorf("SMTP is not configured")
}
auth := smtp.PlainAuth("", m.cfg.Username, m.cfg.Password, m.cfg.Host)
msg := []byte("To: " + to + "\r\n" +
"Subject: " + subject + "\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"\r\n" +
body + "\r\n")
addr := m.cfg.Host + ":" + m.cfg.Port
// Some servers (like STARTTLS on 587) require standard Dial, then StartTLS
// We'll use smtp.SendMail which does STARTTLS automatically if the server supports it
err := smtp.SendMail(addr, auth, m.cfg.Username, []string{to}, msg)
if err != nil {
// If standard SendMail fails due to TLS issues, fallback to manual TLS/STARTTLS if needed
// But smtp.SendMail is usually sufficient for standard STARTTLS on port 587.
return fmt.Errorf("failed to send email: %w", err)
}
return nil
}
func (m *Mailer) SendVerificationEmail(to, username, token, appURL string) error {
subject := "Verify your dumpsterChat email"
link := fmt.Sprintf("%s/verify-email?token=%s", appURL, token)
body := fmt.Sprintf(`
<h1>Welcome to dumpsterChat, %s!</h1>
<p>Please verify your email address by clicking the link below:</p>
<p><a href="%s">%s</a></p>
<p>If you did not sign up for this account, you can safely ignore this email.</p>
`, username, link, link)
return m.Send(to, subject, body)
}
func (m *Mailer) SendPasswordResetEmail(to, username, token, appURL string) error {
subject := "Reset your dumpsterChat password"
link := fmt.Sprintf("%s/reset-password?token=%s", appURL, token)
body := fmt.Sprintf(`
<h1>Password Reset Request</h1>
<p>Hi %s,</p>
<p>We received a request to reset your password. Click the link below to set a new password:</p>
<p><a href="%s">%s</a></p>
<p>If you did not request this, please ignore this email.</p>
`, username, link, link)
return m.Send(to, subject, body)
}
func (m *Mailer) SendAnnouncement(to, subject, body string) error {
return m.Send(to, subject, body)
}
+4 -3
View File
@@ -96,13 +96,14 @@ func (c *Checker) CheckChannelPermission(ctx context.Context, serverID, userID,
}
// Layer channel overrides: apply deny first (subtract), then allow (add).
// Role-based overrides from all user roles, then user-specific override.
// Role-based overrides from all user roles, plus the @everyone role, then user-specific override.
rows, err := c.db.QueryContext(ctx, `
SELECT co.allow_bitflags, co.deny_bitflags
FROM channel_overrides co
WHERE co.channel_id = $1 AND co.target_type = 'role'
AND co.target_id IN (
SELECT mr.role_id FROM member_roles mr WHERE mr.user_id = $2 AND mr.server_id = $3
AND (
co.target_id IN (SELECT mr.role_id FROM member_roles mr WHERE mr.user_id = $2 AND mr.server_id = $3)
OR co.target_id = (SELECT id FROM roles WHERE server_id = $3 AND is_default = TRUE LIMIT 1)
)
`, channelID, userID, serverID)
if err != nil {
+5 -4
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"log/slog"
"net/http"
"github.com/go-chi/chi/v5"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
webpush "github.com/SherClockHolmes/webpush-go"
@@ -31,10 +32,10 @@ func NewHandler(db *sql.DB, vapidPub, vapidPriv, vapidSubj string, logger *slog.
}
}
func (h *Handler) RegisterRoutes(r *http.ServeMux) {
r.HandleFunc("POST /push/subscribe", h.Subscribe)
r.HandleFunc("POST /push/unsubscribe", h.Unsubscribe)
r.HandleFunc("GET /push/vapid-public-key", h.GetPublicKey)
func (h *Handler) RegisterRoutes(r chi.Router) {
r.Post("/push/subscribe", h.Subscribe)
r.Post("/push/unsubscribe", h.Unsubscribe)
r.Get("/push/vapid-public-key", h.GetPublicKey)
}
// @Summary Get VAPID public key
+25 -3
View File
@@ -358,12 +358,34 @@ func (h *RoleHandler) GetMemberRoles(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(roles)
}
// SeedDefaultRole inserts the @everyone role for a newly created server.
// SeedDefaultRole inserts the @everyone, Admin, and Moderator roles for a newly created server.
// Call this after creating a server (e.g. from the server creation handler).
func SeedDefaultRole(ctx context.Context, db *sql.DB, serverID string) error {
// @everyone
_, err := db.ExecContext(ctx, `
INSERT INTO roles (server_id, name, permissions, position, is_default)
VALUES ($1, '@everyone', $2, 0, TRUE)
INSERT INTO roles (server_id, name, permissions, position, is_default, color)
VALUES ($1, '@everyone', $2, 0, TRUE, NULL)
`, serverID, permissions.DefaultEveryonePermissions)
if err != nil {
return err
}
// Admin
adminPerms := permissions.ADMINISTRATOR
_, err = db.ExecContext(ctx, `
INSERT INTO roles (server_id, name, permissions, position, is_default, color)
VALUES ($1, 'Admin', $2, 99, FALSE, '#e06c75')
`, serverID, adminPerms)
if err != nil {
return err
}
// Moderator
modPerms := permissions.VIEW_CHANNEL | permissions.SEND_MESSAGES | permissions.MANAGE_MESSAGES | permissions.KICK_MEMBERS | permissions.BAN_MEMBERS | permissions.MUTE_MEMBERS | permissions.CONNECT_VOICE | permissions.SPEAK_VOICE
_, err = db.ExecContext(ctx, `
INSERT INTO roles (server_id, name, permissions, position, is_default, color)
VALUES ($1, 'Moderator', $2, 50, FALSE, '#61afef')
`, serverID, modPerms)
return err
}