Switching to MySQL
This commit is contained in:
40
storage/sqlite/admin.go
Normal file
40
storage/sqlite/admin.go
Normal 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
22
storage/sqlite/audit.go
Normal 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
53
storage/sqlite/db.go
Normal 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
91
storage/sqlite/insert.go
Normal 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
178
storage/sqlite/messages.go
Normal 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
|
||||
}
|
||||
85
storage/sqlite/notifications.go
Normal file
85
storage/sqlite/notifications.go
Normal 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
238
storage/sqlite/schema.go
Normal 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
287
storage/sqlite/syndicate.go
Normal 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
|
||||
}
|
||||
266
storage/sqlite/thunderball/statisticqueries.go
Normal file
266
storage/sqlite/thunderball/statisticqueries.go
Normal 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 (1–39)
|
||||
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
34
storage/sqlite/users.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user