Switching to MySQL

This commit is contained in:
2025-10-22 22:43:35 +01:00
parent 752db0b89d
commit d53e27eea8
10 changed files with 0 additions and 0 deletions

40
storage/sqlite/admin.go Normal file
View File

@@ -0,0 +1,40 @@
package storage
import (
"database/sql"
"log"
"net/http"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/middleware"
)
func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok || !securityHelpers.IsAdmin(db, userID) {
log.Printf("⛔️ Unauthorized admin attempt: user_id=%v, IP=%s, Path=%s", userID, r.RemoteAddr, r.URL.Path)
templateHelpers.RenderError(w, r, http.StatusForbidden)
return
}
ip := r.RemoteAddr
ua := r.UserAgent()
path := r.URL.Path
_, err := db.Exec(`
INSERT INTO admin_access_log (user_id, path, ip, user_agent)
VALUES (?, ?, ?, ?)`,
userID, path, ip, ua,
)
if err != nil {
log.Printf("⚠️ Failed to log admin access: %v", err)
}
log.Printf("🛡️ Admin access: user_id=%d IP=%s Path=%s", userID, ip, path)
next(w, r)
})
}

22
storage/sqlite/audit.go Normal file
View File

@@ -0,0 +1,22 @@
package storage
import (
"net/http"
"time"
"synlotto-website/logging"
)
func LogLoginAttempt(r *http.Request, username string, success bool) {
ip := r.RemoteAddr
userAgent := r.UserAgent()
_, err := db.Exec(
`INSERT INTO audit_login (username, success, ip, user_agent, timestamp)
VALUES (?, ?, ?, ?, ?)`,
username, success, ip, userAgent, time.Now().UTC(),
)
if err != nil {
logging.Info("❌ Failed to log login:", err)
}
}

53
storage/sqlite/db.go Normal file
View File

@@ -0,0 +1,53 @@
package storage
import (
"database/sql"
"log"
"synlotto-website/config"
"synlotto-website/logging"
_ "modernc.org/sqlite"
)
var db *sql.DB
func InitDB(filepath string) *sql.DB {
var err error
cfg := config.Get()
db, err = sql.Open("sqlite", filepath)
if err != nil {
log.Fatal("❌ Failed to open DB:", err)
}
schemas := []string{
SchemaUsers,
SchemaThunderballResults,
SchemaThunderballPrizes,
SchemaLottoResults,
SchemaMyTickets,
SchemaUsersMessages,
SchemaUsersNotifications,
SchemaAuditLog,
SchemaAuditLogin,
SchemaLogTicketMatching,
SchemaAdminAccessLog,
SchemaNewAuditLog,
SchemaSyndicates,
SchemaSyndicateMembers,
SchemaSyndicateInvites,
SchemaSyndicateInviteTokens,
}
if cfg == nil {
logging.Error("❌ config is nil — did config.Init() run before InitDB?")
panic("config not ready")
}
for _, stmt := range schemas {
if _, err := db.Exec(stmt); err != nil {
log.Fatalf("❌ Failed to apply schema: %v", err)
}
}
return db
}

91
storage/sqlite/insert.go Normal file
View File

@@ -0,0 +1,91 @@
package storage
import (
"database/sql"
"log"
"strings"
"synlotto-website/helpers"
"synlotto-website/models"
)
func InsertThunderballResult(db *sql.DB, res models.ThunderballResult) error {
stmt := `
INSERT INTO results_thunderball (
draw_date, machine, ballset,
ball1, ball2, ball3, ball4, ball5, thunderball
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`
_, err := db.Exec(stmt,
res.DrawDate, res.Machine, res.BallSet,
res.Ball1, res.Ball2, res.Ball3, res.Ball4, res.Ball5, res.Thunderball,
)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
log.Printf("⚠️ Draw for %s already exists. Skipping insert.\n", res.DrawDate)
return nil
}
log.Println("❌ InsertThunderballResult error:", err)
}
return err
}
func InsertTicket(db *sql.DB, ticket models.Ticket) error {
var bonus1Val interface{}
var bonus2Val interface{}
if ticket.Bonus1 != nil {
bonus1Val = helpers.Nullable(*ticket.Bonus1)
} else {
bonus1Val = nil
}
if ticket.Bonus2 != nil {
bonus2Val = helpers.Nullable(*ticket.Bonus2)
} else {
bonus2Val = nil
}
query := `
SELECT COUNT(*) FROM my_tickets
WHERE game_type = ? AND draw_date = ?
AND ball1 = ? AND ball2 = ? AND ball3 = ?
AND ball4 = ? AND ball5 = ? AND bonus1 IS ? AND bonus2 IS ?;`
var count int
err := db.QueryRow(query,
ticket.GameType,
ticket.DrawDate,
ticket.Ball1,
ticket.Ball2,
ticket.Ball3,
ticket.Ball4,
ticket.Ball5,
bonus1Val,
bonus2Val,
).Scan(&count)
isDuplicate := count > 0
insert := `
INSERT INTO my_tickets (
game_type, draw_date,
ball1, ball2, ball3, ball4, ball5,
bonus1, bonus2, duplicate
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
_, err = db.Exec(insert,
ticket.GameType, ticket.DrawDate,
ticket.Ball1, ticket.Ball2, ticket.Ball3,
ticket.Ball4, ticket.Ball5,
bonus1Val, bonus2Val,
isDuplicate,
)
if err != nil {
log.Println("❌ Failed to insert ticket:", err)
} else if isDuplicate {
log.Println("⚠️ Duplicate ticket detected and flagged.")
}
return err
}

178
storage/sqlite/messages.go Normal file
View File

@@ -0,0 +1,178 @@
package storage
import (
"database/sql"
"fmt"
"synlotto-website/models"
)
func GetMessageCount(db *sql.DB, userID int) (int, error) {
var count int
err := db.QueryRow(`
SELECT COUNT(*) FROM users_messages
WHERE recipientId = ? AND is_read = FALSE AND is_archived = FALSE
`, userID).Scan(&count)
return count, err
}
func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
rows, err := db.Query(`
SELECT id, senderId, recipientId, subject, message, is_read, created_at
FROM users_messages
WHERE recipientId = ? AND is_archived = FALSE
ORDER BY created_at DESC
LIMIT ?
`, userID, limit)
if err != nil {
return nil
}
defer rows.Close()
var messages []models.Message
for rows.Next() {
var m models.Message
err := rows.Scan(
&m.ID,
&m.SenderId,
&m.RecipientId,
&m.Subject,
&m.Message,
&m.IsRead,
&m.CreatedAt,
)
if err == nil {
messages = append(messages, m)
}
}
return messages
}
func GetMessageByID(db *sql.DB, userID, messageID int) (*models.Message, error) {
row := db.QueryRow(`
SELECT id, senderId, recipientId, subject, message, is_read, created_at
FROM users_messages
WHERE id = ? AND recipientId = ?
`, messageID, userID)
var m models.Message
err := row.Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Message, &m.IsRead, &m.CreatedAt)
if err != nil {
return nil, err
}
return &m, nil
}
func MarkMessageAsRead(db *sql.DB, messageID, userID int) error {
result, err := db.Exec(`
UPDATE users_messages
SET is_read = TRUE
WHERE id = ? AND recipientId = ?
`, messageID, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return fmt.Errorf("no matching message found for user_id=%d and message_id=%d", userID, messageID)
}
return nil
}
func ArchiveMessage(db *sql.DB, userID, messageID int) error {
_, err := db.Exec(`
UPDATE users_messages
SET is_archived = TRUE, archived_at = CURRENT_TIMESTAMP
WHERE id = ? AND recipientId = ?
`, messageID, userID)
return err
}
func SendMessage(db *sql.DB, senderID, recipientID int, subject, message string) error {
_, err := db.Exec(`
INSERT INTO users_messages (senderId, recipientId, subject, message)
VALUES (?, ?, ?, ?)
`, senderID, recipientID, subject, message)
return err
}
func GetArchivedMessages(db *sql.DB, userID int, page, perPage int) []models.Message {
offset := (page - 1) * perPage
rows, err := db.Query(`
SELECT id, senderId, recipientId, subject, message, is_read, created_at, archived_at
FROM users_messages
WHERE recipientId = ? AND is_archived = TRUE
ORDER BY archived_at DESC
LIMIT ? OFFSET ?
`, userID, perPage, offset)
if err != nil {
return nil
}
defer rows.Close()
var messages []models.Message
for rows.Next() {
var m models.Message
err := rows.Scan(
&m.ID, &m.SenderId, &m.RecipientId,
&m.Subject, &m.Message, &m.IsRead,
&m.CreatedAt, &m.ArchivedAt,
)
if err == nil {
messages = append(messages, m)
}
}
return messages
}
func GetInboxMessages(db *sql.DB, userID int, page, perPage int) []models.Message {
offset := (page - 1) * perPage
rows, err := db.Query(`
SELECT id, senderId, recipientId, subject, message, is_read, created_at
FROM users_messages
WHERE recipientId = ? AND is_archived = FALSE
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`, userID, perPage, offset)
if err != nil {
return nil
}
defer rows.Close()
var messages []models.Message
for rows.Next() {
var m models.Message
err := rows.Scan(
&m.ID, &m.SenderId, &m.RecipientId,
&m.Subject, &m.Message, &m.IsRead, &m.CreatedAt,
)
if err == nil {
messages = append(messages, m)
}
}
return messages
}
func GetInboxMessageCount(db *sql.DB, userID int) int {
var count int
err := db.QueryRow(`
SELECT COUNT(*) FROM users_messages
WHERE recipientId = ? AND is_archived = FALSE
`, userID).Scan(&count)
if err != nil {
return 0
}
return count
}
func RestoreMessage(db *sql.DB, userID, messageID int) error {
_, err := db.Exec(`
UPDATE users_messages
SET is_archived = FALSE, archived_at = NULL
WHERE id = ? AND recipientId = ?
`, messageID, userID)
return err
}

View File

@@ -0,0 +1,85 @@
package storage
import (
"database/sql"
"fmt"
"log"
"synlotto-website/models"
)
func GetNotificationCount(db *sql.DB, userID int) int {
var count int
err := db.QueryRow(`
SELECT COUNT(*) FROM users_notification
WHERE user_id = ? AND is_read = FALSE`, userID).Scan(&count)
if err != nil {
log.Println("⚠️ Failed to count notifications:", err)
return 0
}
return count
}
func GetRecentNotifications(db *sql.DB, userID int, limit int) []models.Notification {
rows, err := db.Query(`
SELECT id, subject, body, is_read, created_at
FROM users_notification
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ?`, userID, limit)
if err != nil {
log.Println("⚠️ Failed to get notifications:", err)
return nil
}
defer rows.Close()
var notifications []models.Notification
for rows.Next() {
var n models.Notification
if err := rows.Scan(&n.ID, &n.Subject, &n.Body, &n.IsRead, &n.CreatedAt); err == nil {
notifications = append(notifications, n)
}
}
return notifications
}
func MarkNotificationAsRead(db *sql.DB, userID int, notificationID int) error {
result, err := db.Exec(`
UPDATE users_notification
SET is_read = TRUE
WHERE id = ? AND user_id = ?
`, notificationID, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return fmt.Errorf("no matching notification for user_id=%d and id=%d", userID, notificationID)
}
return nil
}
func GetNotificationByID(db *sql.DB, userID, notificationID int) (*models.Notification, error) {
row := db.QueryRow(`
SELECT id, user_id, subject, body, is_read
FROM users_notification
WHERE id = ? AND user_id = ?
`, notificationID, userID)
var n models.Notification
err := row.Scan(&n.ID, &n.UserId, &n.Subject, &n.Body, &n.IsRead)
if err != nil {
return nil, err
}
return &n, nil
}

238
storage/sqlite/schema.go Normal file
View File

@@ -0,0 +1,238 @@
package storage
const SchemaUsers = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
is_admin BOOLEAN
);`
const SchemaThunderballResults = `
CREATE TABLE IF NOT EXISTS results_thunderball (
id INTEGER PRIMARY KEY AUTOINCREMENT,
draw_date TEXT NOT NULL UNIQUE,
draw_id INTEGER NOT NULL UNIQUE,
machine TEXT,
ballset TEXT,
ball1 INTEGER,
ball2 INTEGER,
ball3 INTEGER,
ball4 INTEGER,
ball5 INTEGER,
thunderball INTEGER
);`
const SchemaThunderballPrizes = `
CREATE TABLE IF NOT EXISTS prizes_thunderball (
id INTEGER PRIMARY KEY AUTOINCREMENT,
draw_id INTEGER NOT NULL,
draw_date TEXT,
prize1 TEXT,
prize1_winners INTEGER,
prize1_per_winner INTEGER,
prize1_fund INTEGER,
prize2 TEXT,
prize2_winners INTEGER,
prize2_per_winner INTEGER,
prize2_fund INTEGER,
prize3 TEXT,
prize3_winners INTEGER,
prize3_per_winner INTEGER,
prize3_fund INTEGER,
prize4 TEXT,
prize4_winners INTEGER,
prize4_per_winner INTEGER,
prize4_fund INTEGER,
prize5 TEXT,
prize5_winners INTEGER,
prize5_per_winner INTEGER,
prize5_fund INTEGER,
prize6 TEXT,
prize6_winners INTEGER,
prize6_per_winner INTEGER,
prize6_fund INTEGER,
prize7 TEXT,
prize7_winners INTEGER,
prize7_per_winner INTEGER,
prize7_fund INTEGER,
prize8 TEXT,
prize8_winners INTEGER,
prize8_per_winner INTEGER,
prize8_fund INTEGER,
prize9 TEXT,
prize9_winners INTEGER,
prize9_per_winner INTEGER,
prize9_fund INTEGER,
total_winners INTEGER,
total_prize_fund INTEGER,
FOREIGN KEY (draw_date) REFERENCES results_thunderball(draw_date)
);`
const SchemaLottoResults = `
CREATE TABLE IF NOT EXISTS results_lotto (
id INTEGER PRIMARY KEY AUTOINCREMENT,
draw_date TEXT NOT NULL UNIQUE,
draw_id INTEGER NOT NULL UNIQUE,
machine TEXT,
ballset TEXT,
ball1 INTEGER,
ball2 INTEGER,
ball3 INTEGER,
ball4 INTEGER,
ball5 INTEGER,
ball6 INTEGER,
bonusball INTEGER
);`
const SchemaMyTickets = `
CREATE TABLE IF NOT EXISTS my_tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
game_type TEXT NOT NULL,
draw_date TEXT NOT NULL,
ball1 INTEGER,
ball2 INTEGER,
ball3 INTEGER,
ball4 INTEGER,
ball5 INTEGER,
ball6 INTEGER,
bonus1 INTEGER,
bonus2 INTEGER,
duplicate BOOLEAN DEFAULT 0,
purchase_date TEXT,
purchase_method TEXT,
image_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
matched_main INTEGER,
matched_bonus INTEGER,
prize_tier TEXT,
is_winner BOOLEAN,
prize_amount INTEGER,
prize_label TEXT,
syndicate_id INTEGER,
FOREIGN KEY (userId) REFERENCES users(id)
);`
const SchemaUsersMessages = `
CREATE TABLE IF NOT EXISTS users_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
senderId INTEGER NOT NULL REFERENCES users(id),
recipientId INTEGER NOT NULL REFERENCES users(id),
subject TEXT NOT NULL,
message TEXT,
is_read BOOLEAN DEFAULT FALSE,
is_archived BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
archived_at TIMESTAMP
);`
const SchemaUsersNotifications = `
CREATE TABLE IF NOT EXISTS users_notification (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
subject TEXT,
body TEXT,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
const SchemaAuditLog = `
CREATE TABLE IF NOT EXISTS auditlog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
success INTEGER,
timestamp TEXT
);`
const SchemaLogTicketMatching = `
CREATE TABLE IF NOT EXISTS log_ticket_matching (
id INTEGER PRIMARY KEY AUTOINCREMENT,
triggered_by TEXT,
run_at DATETIME DEFAULT CURRENT_TIMESTAMP,
tickets_matched INTEGER,
winners_found INTEGER,
notes TEXT
);`
const SchemaAdminAccessLog = `
CREATE TABLE IF NOT EXISTS admin_access_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
accessed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
path TEXT,
ip TEXT,
user_agent TEXT
);`
const SchemaNewAuditLog = `
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
username TEXT,
action TEXT,
path TEXT,
ip TEXT,
user_agent TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);`
const SchemaAuditLogin = `
CREATE TABLE IF NOT EXISTS audit_login (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
success BOOLEAN,
ip TEXT,
user_agent TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);`
const SchemaSyndicates = `
CREATE TABLE IF NOT EXISTS syndicates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
owner_id INTEGER NOT NULL,
join_code TEXT UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (owner_id) REFERENCES users(id)
);`
const SchemaSyndicateMembers = `
CREATE TABLE IF NOT EXISTS syndicate_members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
syndicate_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role TEXT DEFAULT 'member', -- owner, manager, member
status TEXT DEFAULT 'active',
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);`
const SchemaSyndicateInvites = `
CREATE TABLE IF NOT EXISTS syndicate_invites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
syndicate_id INTEGER NOT NULL,
invited_user_id INTEGER NOT NULL,
sent_by_user_id INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(syndicate_id) REFERENCES syndicates(id),
FOREIGN KEY(invited_user_id) REFERENCES users(id)
);`
const SchemaSyndicateInviteTokens = `
CREATE TABLE IF NOT EXISTS syndicate_invite_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
syndicate_id INTEGER NOT NULL,
token TEXT NOT NULL UNIQUE,
invited_by_user_id INTEGER NOT NULL,
accepted_by_user_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
accepted_at TIMESTAMP,
expires_at TIMESTAMP,
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id),
FOREIGN KEY (invited_by_user_id) REFERENCES users(id),
FOREIGN KEY (accepted_by_user_id) REFERENCES users(id)
);`

287
storage/sqlite/syndicate.go Normal file
View File

@@ -0,0 +1,287 @@
package storage
import (
"database/sql"
"fmt"
"synlotto-website/models"
"time"
)
func GetSyndicatesByOwner(db *sql.DB, ownerID int) []models.Syndicate {
rows, err := db.Query(`
SELECT id, name, description, created_at, owner_id
FROM syndicates
WHERE owner_id = ?`, ownerID)
if err != nil {
return nil
}
defer rows.Close()
var syndicates []models.Syndicate
for rows.Next() {
var s models.Syndicate
err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.CreatedAt, &s.OwnerID)
if err == nil {
syndicates = append(syndicates, s)
}
}
return syndicates
}
func GetSyndicatesByMember(db *sql.DB, userID int) []models.Syndicate {
rows, err := db.Query(`
SELECT s.id, s.name, s.description, s.created_at, s.owner_id
FROM syndicates s
JOIN syndicate_members m ON s.id = m.syndicate_id
WHERE m.user_id = ?`, userID)
if err != nil {
return nil
}
defer rows.Close()
var syndicates []models.Syndicate
for rows.Next() {
var s models.Syndicate
err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.CreatedAt, &s.OwnerID)
if err == nil {
syndicates = append(syndicates, s)
}
}
return syndicates
}
func GetSyndicateByID(db *sql.DB, id int) (*models.Syndicate, error) {
row := db.QueryRow(`SELECT id, name, description, owner_id, created_at FROM syndicates WHERE id = ?`, id)
var s models.Syndicate
err := row.Scan(&s.ID, &s.Name, &s.Description, &s.OwnerID, &s.CreatedAt)
if err != nil {
return nil, err
}
return &s, nil
}
func IsSyndicateManager(db *sql.DB, syndicateID, userID int) bool {
var count int
err := db.QueryRow(`
SELECT COUNT(*) FROM syndicates
WHERE id = ? AND owner_id = ?
`, syndicateID, userID).Scan(&count)
return err == nil && count > 0
}
func GetSyndicateMembers(db *sql.DB, syndicateID int) []models.SyndicateMember {
rows, err := db.Query(`
SELECT m.user_id, u.username, m.joined_at
FROM syndicate_members m
JOIN users u ON u.id = m.user_id
WHERE m.syndicate_id = ?
`, syndicateID)
if err != nil {
return nil
}
defer rows.Close()
var members []models.SyndicateMember
for rows.Next() {
var m models.SyndicateMember
err := rows.Scan(&m.UserID, &m.UserID, &m.JoinedAt)
if err == nil {
members = append(members, m)
}
}
return members
}
func IsSyndicateMember(db *sql.DB, syndicateID, userID int) bool {
var count int
err := db.QueryRow(`SELECT COUNT(*) FROM syndicate_members WHERE syndicate_id = ? AND user_id = ?`, syndicateID, userID).Scan(&count)
return err == nil && count > 0
}
func AddMemberToSyndicate(db *sql.DB, syndicateID, userID int) error {
_, err := db.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, joined_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
`, syndicateID, userID)
return err
}
func GetSyndicateTickets(db *sql.DB, syndicateID int) []models.Ticket {
rows, err := db.Query(`
SELECT id, userId, syndicateId, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, ball6,
bonus1, bonus2, matched_main, matched_bonus, prize_tier, prize_amount, prize_label, is_winner
FROM my_tickets
WHERE syndicateId = ?
ORDER BY draw_date DESC
`, syndicateID)
if err != nil {
return nil
}
defer rows.Close()
var tickets []models.Ticket
for rows.Next() {
var t models.Ticket
err := rows.Scan(
&t.Id, &t.UserId, &t.SyndicateId, &t.GameType, &t.DrawDate,
&t.Ball1, &t.Ball2, &t.Ball3, &t.Ball4, &t.Ball5, &t.Ball6,
&t.Bonus1, &t.Bonus2, &t.MatchedMain, &t.MatchedBonus,
&t.PrizeTier, &t.PrizeAmount, &t.PrizeLabel, &t.IsWinner,
)
if err == nil {
tickets = append(tickets, t)
}
}
return tickets
}
func InviteUserToSyndicate(db *sql.DB, syndicateID, invitedUserID, senderID int) error {
_, err := db.Exec(`
INSERT INTO syndicate_invites (syndicate_id, invited_user_id, sent_by_user_id)
VALUES (?, ?, ?)
`, syndicateID, invitedUserID, senderID)
return err
}
func GetPendingInvites(db *sql.DB, userID int) []models.SyndicateInvite {
rows, err := db.Query(`
SELECT id, syndicate_id, invited_user_id, sent_by_user_id, status, created_at
FROM syndicate_invites
WHERE invited_user_id = ? AND status = 'pending'
`, userID)
if err != nil {
return nil
}
defer rows.Close()
var invites []models.SyndicateInvite
for rows.Next() {
var i models.SyndicateInvite
rows.Scan(&i.ID, &i.SyndicateID, &i.InvitedUserID, &i.SentByUserID, &i.Status, &i.CreatedAt)
invites = append(invites, i)
}
return invites
}
func UpdateInviteStatus(db *sql.DB, inviteID int, status string) error {
_, err := db.Exec(`
UPDATE syndicate_invites
SET status = ?
WHERE id = ?
`, status, inviteID)
return err
}
func AcceptInvite(db *sql.DB, inviteID, userID int) error {
var syndicateID int
err := db.QueryRow(`
SELECT syndicate_id FROM syndicate_invites
WHERE id = ? AND invited_user_id = ? AND status = 'pending'
`, inviteID, userID).Scan(&syndicateID)
if err != nil {
return err
}
if err := UpdateInviteStatus(db, inviteID, "accepted"); err != nil {
return err
}
_, err = db.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, joined_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
`, syndicateID, userID)
return err
}
func CreateSyndicate(db *sql.DB, ownerID int, name, description string) (int64, error) {
tx, err := db.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
result, err := tx.Exec(`
INSERT INTO syndicates (name, description, owner_id, created_at)
VALUES (?, ?, ?, ?)
`, name, description, ownerID, time.Now())
if err != nil {
return 0, fmt.Errorf("failed to create syndicate: %w", err)
}
syndicateID, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("failed to get syndicate ID: %w", err)
}
_, err = tx.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, role, joined_at)
VALUES (?, ?, 'manager', CURRENT_TIMESTAMP)
`, syndicateID, ownerID)
if err != nil {
return 0, fmt.Errorf("failed to add owner as member: %w", err)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("commit failed: %w", err)
}
return syndicateID, nil
}
func InviteToSyndicate(db *sql.DB, inviterID, syndicateID int, username string) error {
var inviteeID int
err := db.QueryRow(`
SELECT id FROM users WHERE username = ?
`, username).Scan(&inviteeID)
if err == sql.ErrNoRows {
return fmt.Errorf("user not found")
} else if err != nil {
return err
}
var count int
err = db.QueryRow(`
SELECT COUNT(*) FROM syndicate_members
WHERE syndicate_id = ? AND user_id = ?
`, syndicateID, inviteeID).Scan(&count)
if err != nil {
return err
}
if count > 0 {
return fmt.Errorf("user already a member or invited")
}
_, err = db.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, is_manager, status)
VALUES (?, ?, 0, 'invited')
`, syndicateID, inviteeID)
return err
}
func GetInviteTokensForSyndicate(db *sql.DB, syndicateID int) []models.SyndicateInviteToken {
rows, err := db.Query(`
SELECT token, invited_by_user_id, accepted_by_user_id, created_at, expires_at, accepted_at
FROM syndicate_invite_tokens
WHERE syndicate_id = ?
ORDER BY created_at DESC
`, syndicateID)
if err != nil {
return nil
}
defer rows.Close()
var tokens []models.SyndicateInviteToken
for rows.Next() {
var t models.SyndicateInviteToken
_ = rows.Scan(
&t.Token,
&t.InvitedByUserID,
&t.AcceptedByUserID,
&t.CreatedAt,
&t.ExpiresAt,
&t.AcceptedAt,
)
tokens = append(tokens, t)
}
return tokens
}

View File

@@ -0,0 +1,266 @@
package storage
// ToDo: The last seen statistic is done in days, maybe change or add in how many draws x days ways for ease.
// Top 5 main numbers since inception of the game.
const top5AllTime = `
SELECT ball AS Number, COUNT(*) AS Frequency
FROM (
SELECT ball1 AS ball FROM results_thunderball
UNION ALL SELECT ball2 FROM results_thunderball
UNION ALL SELECT ball3 FROM results_thunderball
UNION ALL SELECT ball4 FROM results_thunderball
UNION ALL SELECT ball5 FROM results_thunderball
)
GROUP BY ball
ORDER BY Frequency DESC, Number
LIMIT 5;`
// Top 5 main numbers since the ball count change on May 9th 2010.
const top5Since = `
SELECT ball AS Number, COUNT(*) AS Frequency
FROM (
SELECT ball1 AS ball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT ball2 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT ball3 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT ball4 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT ball5 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
)
GROUP BY ball
ORDER BY Frequency DESC, Number
LIMIT 5;`
// Top 5 main numbers in the last 180 draws.
const top5Last180draws = `
SELECT ball AS Number, COUNT(*) AS Frequency
FROM (
SELECT ball1 AS ball FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
UNION ALL
SELECT ball2 FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
UNION ALL
SELECT ball3 FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
UNION ALL
SELECT ball4 FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
UNION ALL
SELECT ball5 FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
)
GROUP BY ball
ORDER BY Frequency DESC
LIMIT 5;`
// The top 5 thunderballs drawn since the inception of the game.
const top5ThunderballAllTime = `
SELECT thunderball AS Number, COUNT(*) AS Frequency
FROM (
SELECT thunderball AS thunderball FROM results_thunderball
)
GROUP BY thunderball
ORDER BY Frequency DESC, Number
LIMIT 5;`
// The top 5 thunderballs drawn since the ball count change on May 9th 2010.
const top5ThunderballSince = `
SELECT thunderball AS Number, COUNT(*) AS Frequency
FROM (
SELECT thunderball AS thunderball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
)
GROUP BY thunderball
ORDER BY Frequency DESC, Number
LIMIT 5;`
const top5TunderballLast180draws = `
SELECT thunderball AS Number, COUNT(*) AS Frequency
FROM (
SELECT thunderball AS thunderball FROM (
SELECT * FROM results_thunderball ORDER BY date(draw_date) DESC LIMIT 180
)
)
GROUP BY thunderball
ORDER BY Frequency DESC
LIMIT 5;`
const thunderballMainLastSeen = `
SELECT
n.ball AS Number,
julianday('now') - julianday(MAX(r.draw_date)) AS DaysSinceLastDrawn,
MAX(r.draw_date) AS LastDrawDate
FROM (
SELECT ball1 AS ball, draw_date FROM results_thunderball
UNION ALL
SELECT ball2, draw_date FROM results_thunderball
UNION ALL
SELECT ball3, draw_date FROM results_thunderball
UNION ALL
SELECT ball4, draw_date FROM results_thunderball
UNION ALL
SELECT ball5, draw_date FROM results_thunderball
) AS r
JOIN (
-- This generates a list of all possible ball numbers (139)
SELECT 1 AS ball UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL
SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL
SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL
SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18 UNION ALL SELECT 19 UNION ALL SELECT 20 UNION ALL
SELECT 21 UNION ALL SELECT 22 UNION ALL SELECT 23 UNION ALL SELECT 24 UNION ALL SELECT 25 UNION ALL
SELECT 26 UNION ALL SELECT 27 UNION ALL SELECT 28 UNION ALL SELECT 29 UNION ALL SELECT 30 UNION ALL
SELECT 31 UNION ALL SELECT 32 UNION ALL SELECT 33 UNION ALL SELECT 34 UNION ALL SELECT 35 UNION ALL
SELECT 36 UNION ALL SELECT 37 UNION ALL SELECT 38 UNION ALL SELECT 39
) AS n ON n.ball = r.ball
GROUP BY n.ball
ORDER BY DaysSinceLastDrawn DESC;`
const thunderballLastSeen = `
SELECT
n.thunderball AS Number,
julianday('now') - julianday(MAX(r.draw_date)) AS DaysSinceLastDrawn,
MAX(r.draw_date) AS LastDrawDate
FROM (
SELECT thunderball AS thunderball, draw_date FROM results_thunderball
) AS r
JOIN (
SELECT 1 AS thunderball UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL
SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL
SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14
) AS n ON n.thunderball = r.thunderball
GROUP BY n.thunderball
ORDER BY DaysSinceLastDrawn DESC;`
const thunderballCommonPairsSince = `
WITH unpivot AS (
SELECT draw_date, ball1 AS ball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball2 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball3 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball4 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball5 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS ball_a,
MAX(a.ball, b.ball) AS ball_b
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
)
SELECT ball_a, ball_b, COUNT(*) AS frequency
FROM pairs
GROUP BY ball_a, ball_b
ORDER BY frequency DESC, ball_a, ball_b
LIMIT 25;`
const thunderballCommonPairsLast180 = `
WITH recent AS (
SELECT * FROM results_thunderball
ORDER BY date(draw_date) DESC
LIMIT 180
),
unpivot AS (
SELECT draw_date, ball1 AS ball FROM recent
UNION ALL SELECT draw_date, ball2 FROM recent
UNION ALL SELECT draw_date, ball3 FROM recent
UNION ALL SELECT draw_date, ball4 FROM recent
UNION ALL SELECT draw_date, ball5 FROM recent
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS ball_a,
MAX(a.ball, b.ball) AS ball_b
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
)
SELECT ball_a, ball_b, COUNT(*) AS frequency
FROM pairs
GROUP BY ball_a, ball_b
ORDER BY frequency DESC, ball_a, ball_b
LIMIT 25;`
// Best pair balls if you choose x try picking these numbers that are frequencly seen with it (ToDo: Update this description)
// ToDo No All Time for this, go back and ensure everything has an all time for completeness.
const thunderballSepecificCommonPairsSince = `
WITH unpivot AS (
SELECT draw_date, ball1 AS ball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball2 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball3 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball4 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball5 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS ball_a,
MAX(a.ball, b.ball) AS ball_b
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
)
SELECT
CASE WHEN ball_a = 26 THEN ball_b ELSE ball_a END AS partner,
COUNT(*) AS frequency
FROM pairs
WHERE ball_a = 26 OR ball_b = 26
GROUP BY partner
ORDER BY frequency DESC, partner
LIMIT 20;`
const thunderballCommonConsecutiveNumbersAllTime = `
WITH unpivot AS (
SELECT draw_date, ball1 AS ball FROM results_thunderball
UNION ALL SELECT draw_date, ball2 FROM results_thunderball
UNION ALL SELECT draw_date, ball3 FROM results_thunderball
UNION ALL SELECT draw_date, ball4 FROM results_thunderball
UNION ALL SELECT draw_date, ball5 FROM results_thunderball
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS a_ball,
MAX(a.ball, b.ball) AS b_ball
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
AND ABS(a.ball - b.ball) = 1 -- consecutive only
)
SELECT a_ball AS num1, b_ball AS num2, COUNT(*) AS frequency
FROM pairs
GROUP BY a_ball, b_ball
ORDER BY frequency DESC, num1, num2
LIMIT 25;
`
const thunderballCommonConsecutiveNumbersSince = `
WITH unpivot AS (
SELECT draw_date, ball1 AS ball FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball2 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball3 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball4 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
UNION ALL SELECT draw_date, ball5 FROM results_thunderball WHERE date(draw_date) >= '2010-05-09'
),
pairs AS (
SELECT a.draw_date,
MIN(a.ball, b.ball) AS a_ball,
MAX(a.ball, b.ball) AS b_ball
FROM unpivot a
JOIN unpivot b
ON a.draw_date = b.draw_date
AND a.ball < b.ball
AND ABS(a.ball - b.ball) = 1 -- consecutive only
)
SELECT a_ball AS num1, b_ball AS num2, COUNT(*) AS frequency
FROM pairs
GROUP BY a_ball, b_ball
ORDER BY frequency DESC, num1, num2
LIMIT 25;
`
// Wait, double check common number queries, consecutive and consecutive numbers make sure ive not mixed them up

34
storage/sqlite/users.go Normal file
View File

@@ -0,0 +1,34 @@
package storage
import (
"database/sql"
"synlotto-website/logging"
"synlotto-website/models"
)
func GetUserByID(db *sql.DB, id int) *models.User {
row := db.QueryRow("SELECT id, username, password_hash, is_admin FROM users WHERE id = ?", id)
var user models.User
err := row.Scan(&user.Id, &user.Username, &user.PasswordHash, &user.IsAdmin)
if err != nil {
if err != sql.ErrNoRows {
logging.Error("DB error:", err)
}
return nil
}
return &user
}
func GetUserByUsername(db *sql.DB, username string) *models.User {
row := db.QueryRow(`SELECT id, username, password_hash, is_admin FROM users WHERE username = ?`, username)
var u models.User
err := row.Scan(&u.Id, &u.Username, &u.PasswordHash, &u.IsAdmin)
if err != nil {
return nil
}
return &u
}