Refactor and remove sqlite and replace with MySQL
This commit is contained in:
@@ -5,6 +5,17 @@ type Config struct {
|
||||
CSRFKey string `json:"csrfKey"`
|
||||
} `json:"csrf"`
|
||||
|
||||
Database struct {
|
||||
Server string `json:"server"`
|
||||
Port int `json:"port"`
|
||||
DatabaseNamed string `json:"databaseName"`
|
||||
MaxOpenConnections int `json:"maxOpenConnections"`
|
||||
MaxIdleConnections int `json:"maxIdleConnections"`
|
||||
ConnectionMaxLifetime string `json:"connectionMaxLifetime"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
HttpServer struct {
|
||||
Port int `json:"port"`
|
||||
Address string `json:"address"`
|
||||
34
internal/models/user.go
Normal file
34
internal/models/user.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
Username string
|
||||
PasswordHash string
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
// ToDo: should be in a notification model?
|
||||
type Notification struct {
|
||||
ID int
|
||||
UserId int
|
||||
Subject string
|
||||
Body string
|
||||
IsRead bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// ToDo: should be in a message model?
|
||||
type Message struct {
|
||||
ID int
|
||||
SenderId int
|
||||
RecipientId int
|
||||
Subject string
|
||||
Message string
|
||||
IsRead bool
|
||||
CreatedAt time.Time
|
||||
ArchivedAt *time.Time
|
||||
}
|
||||
43
internal/rules/thunderball/rules.go
Normal file
43
internal/rules/thunderball/rules.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"synlotto-website/internal/models"
|
||||
"synlotto-website/internal/rules"
|
||||
)
|
||||
|
||||
var ThunderballPrizeRules = []models.PrizeRule{
|
||||
{Game: rules.GameThunderball, MainMatches: 0, BonusMatches: 1, Tier: "Tier 1"},
|
||||
{Game: rules.GameThunderball, MainMatches: 1, BonusMatches: 1, Tier: "Tier 2"},
|
||||
{Game: rules.GameThunderball, MainMatches: 2, BonusMatches: 1, Tier: "Tier 3"},
|
||||
{Game: rules.GameThunderball, MainMatches: 3, BonusMatches: 0, Tier: "Tier 4"},
|
||||
{Game: rules.GameThunderball, MainMatches: 3, BonusMatches: 1, Tier: "Tier 5"},
|
||||
{Game: rules.GameThunderball, MainMatches: 4, BonusMatches: 0, Tier: "Tier 6"},
|
||||
{Game: rules.GameThunderball, MainMatches: 4, BonusMatches: 1, Tier: "Tier 7"},
|
||||
{Game: rules.GameThunderball, MainMatches: 5, BonusMatches: 0, Tier: "Second"},
|
||||
{Game: rules.GameThunderball, MainMatches: 5, BonusMatches: 1, Tier: "Jackpot"},
|
||||
}
|
||||
|
||||
func GetThunderballPrizeIndex(main, bonus int) (int, bool) {
|
||||
switch {
|
||||
case main == 0 && bonus == 1:
|
||||
return 9, true
|
||||
case main == 1 && bonus == 1:
|
||||
return 8, true
|
||||
case main == 2 && bonus == 1:
|
||||
return 7, true
|
||||
case main == 3 && bonus == 0:
|
||||
return 6, true
|
||||
case main == 3 && bonus == 1:
|
||||
return 5, true
|
||||
case main == 4 && bonus == 0:
|
||||
return 4, true
|
||||
case main == 4 && bonus == 1:
|
||||
return 3, true
|
||||
case main == 5 && bonus == 0:
|
||||
return 2, true
|
||||
case main == 5 && bonus == 1:
|
||||
return 1, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
23
internal/rules/types.go
Normal file
23
internal/rules/types.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package rules
|
||||
|
||||
// PrizeRule defines how many matches correspond to which prize tier.
|
||||
type PrizeRule struct {
|
||||
Game string
|
||||
MainMatches int
|
||||
BonusMatches int
|
||||
Tier string
|
||||
}
|
||||
|
||||
// ToDo: should this struct not be in a model~?
|
||||
// PrizeInfo describes the tier and payout details.
|
||||
type PrizeInfo struct {
|
||||
Tier string
|
||||
Amount float64
|
||||
Label string
|
||||
}
|
||||
|
||||
// Game names (use constants to avoid typos). ToDo: should this not be in my constants folder or avoid constands folder as it may end up as a junk draw
|
||||
const (
|
||||
GameThunderball = "Thunderball"
|
||||
GameEuroJackpot = "EuroJackpot"
|
||||
)
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
securityHelpers "synlotto-website/helpers/security"
|
||||
templateHelpers "synlotto-website/helpers/template"
|
||||
"synlotto-website/internal/logging"
|
||||
|
||||
"synlotto-website/middleware"
|
||||
)
|
||||
@@ -38,3 +40,17 @@ func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
|
||||
next(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
78
internal/storage/mysql/db.go
Normal file
78
internal/storage/mysql/db.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/mysql"
|
||||
iofs "github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationFiles embed.FS
|
||||
|
||||
var DB *sql.DB
|
||||
|
||||
// InitDB connects to MySQL, runs migrations, and returns the DB handle.
|
||||
func InitDB() *sql.DB {
|
||||
cfg := getDSNFromEnv()
|
||||
|
||||
db, err := sql.Open("mysql", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Failed to connect to MySQL: %v", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
log.Fatalf("❌ MySQL not reachable: %v", err)
|
||||
}
|
||||
|
||||
if err := runMigrations(db); err != nil {
|
||||
log.Fatalf("❌ Migration failed: %v", err)
|
||||
}
|
||||
|
||||
DB = db
|
||||
return db
|
||||
}
|
||||
|
||||
// runMigrations applies any pending .sql files in migrations/
|
||||
func runMigrations(db *sql.DB) error {
|
||||
driver, err := mysql.WithInstance(db, &mysql.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := iofs.New(migrationFiles, "migrations")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithInstance("iofs", src, "mysql", driver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.Up()
|
||||
if err == migrate.ErrNoChange {
|
||||
log.Println("✅ Database schema up to date.")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getDSNFromEnv() string {
|
||||
user := os.Getenv("DB_USER")
|
||||
pass := os.Getenv("DB_PASS")
|
||||
host := os.Getenv("DB_HOST") // e.g. localhost or 127.0.0.1
|
||||
port := os.Getenv("DB_PORT") // e.g. 3306
|
||||
name := os.Getenv("DB_NAME") // e.g. synlotto
|
||||
params := "parseTime=true&multiStatements=true"
|
||||
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s",
|
||||
user, pass, host, port, name, params)
|
||||
return dsn
|
||||
}
|
||||
13
internal/storage/mysql/messages/create.go
Normal file
13
internal/storage/mysql/messages/create.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
2
internal/storage/mysql/messages/delete.go
Normal file
2
internal/storage/mysql/messages/delete.go
Normal file
@@ -0,0 +1,2 @@
|
||||
//Currently no delete functions, only archiving to remove from user view but they can pull them back. Consider a soft delete which hides them from being unarchived for 5 years?
|
||||
// Then systematically delete after 5 years? or delete sooner but retain backups
|
||||
@@ -2,8 +2,8 @@ package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"synlotto-website/models"
|
||||
|
||||
"synlotto-website/internal/models"
|
||||
)
|
||||
|
||||
func GetMessageCount(db *sql.DB, userID int) (int, error) {
|
||||
@@ -62,43 +62,6 @@ func GetMessageByID(db *sql.DB, userID, messageID int) (*models.Message, error)
|
||||
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(`
|
||||
@@ -167,12 +130,3 @@ func GetInboxMessageCount(db *sql.DB, userID int) int {
|
||||
}
|
||||
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
|
||||
}
|
||||
44
internal/storage/mysql/messages/update.go
Normal file
44
internal/storage/mysql/messages/update.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
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 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 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
|
||||
}
|
||||
281
internal/storage/mysql/migrations/0001_initial_create.up.sql
Normal file
281
internal/storage/mysql/migrations/0001_initial_create.up.sql
Normal file
@@ -0,0 +1,281 @@
|
||||
-- 0001_initial.up.sql
|
||||
-- Engine/charset notes:
|
||||
-- - InnoDB for FK support
|
||||
-- - utf8mb4 for full Unicode
|
||||
-- Booleans are TINYINT(1). Dates use DATE/DATETIME/TIMESTAMP as appropriate.
|
||||
|
||||
-- USERS
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(191) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_admin TINYINT(1) NOT NULL DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- THUNDERBALL RESULTS
|
||||
CREATE TABLE IF NOT EXISTS results_thunderball (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
draw_date DATE NOT NULL UNIQUE,
|
||||
draw_id BIGINT UNSIGNED NOT NULL UNIQUE,
|
||||
machine VARCHAR(50),
|
||||
ballset VARCHAR(50),
|
||||
ball1 TINYINT UNSIGNED,
|
||||
ball2 TINYINT UNSIGNED,
|
||||
ball3 TINYINT UNSIGNED,
|
||||
ball4 TINYINT UNSIGNED,
|
||||
ball5 TINYINT UNSIGNED,
|
||||
thunderball TINYINT UNSIGNED
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- THUNDERBALL PRIZES
|
||||
CREATE TABLE IF NOT EXISTS prizes_thunderball (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
draw_id BIGINT UNSIGNED NOT NULL,
|
||||
draw_date DATE,
|
||||
prize1 VARCHAR(50),
|
||||
prize1_winners INT UNSIGNED,
|
||||
prize1_per_winner INT UNSIGNED,
|
||||
prize1_fund BIGINT UNSIGNED,
|
||||
prize2 VARCHAR(50),
|
||||
prize2_winners INT UNSIGNED,
|
||||
prize2_per_winner INT UNSIGNED,
|
||||
prize2_fund BIGINT UNSIGNED,
|
||||
prize3 VARCHAR(50),
|
||||
prize3_winners INT UNSIGNED,
|
||||
prize3_per_winner INT UNSIGNED,
|
||||
prize3_fund BIGINT UNSIGNED,
|
||||
prize4 VARCHAR(50),
|
||||
prize4_winners INT UNSIGNED,
|
||||
prize4_per_winner INT UNSIGNED,
|
||||
prize4_fund BIGINT UNSIGNED,
|
||||
prize5 VARCHAR(50),
|
||||
prize5_winners INT UNSIGNED,
|
||||
prize5_per_winner INT UNSIGNED,
|
||||
prize5_fund BIGINT UNSIGNED,
|
||||
prize6 VARCHAR(50),
|
||||
prize6_winners INT UNSIGNED,
|
||||
prize6_per_winner INT UNSIGNED,
|
||||
prize6_fund BIGINT UNSIGNED,
|
||||
prize7 VARCHAR(50),
|
||||
prize7_winners INT UNSIGNED,
|
||||
prize7_per_winner INT UNSIGNED,
|
||||
prize7_fund BIGINT UNSIGNED,
|
||||
prize8 VARCHAR(50),
|
||||
prize8_winners INT UNSIGNED,
|
||||
prize8_per_winner INT UNSIGNED,
|
||||
prize8_fund BIGINT UNSIGNED,
|
||||
prize9 VARCHAR(50),
|
||||
prize9_winners INT UNSIGNED,
|
||||
prize9_per_winner INT UNSIGNED,
|
||||
prize9_fund BIGINT UNSIGNED,
|
||||
total_winners INT UNSIGNED,
|
||||
total_prize_fund BIGINT UNSIGNED,
|
||||
CONSTRAINT fk_prizes_tb_drawdate
|
||||
FOREIGN KEY (draw_date) REFERENCES results_thunderball(draw_date)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- LOTTO RESULTS
|
||||
CREATE TABLE IF NOT EXISTS results_lotto (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
draw_date DATE NOT NULL UNIQUE,
|
||||
draw_id BIGINT UNSIGNED NOT NULL UNIQUE,
|
||||
machine VARCHAR(50),
|
||||
ballset VARCHAR(50),
|
||||
ball1 TINYINT UNSIGNED,
|
||||
ball2 TINYINT UNSIGNED,
|
||||
ball3 TINYINT UNSIGNED,
|
||||
ball4 TINYINT UNSIGNED,
|
||||
ball5 TINYINT UNSIGNED,
|
||||
ball6 TINYINT UNSIGNED,
|
||||
bonusball TINYINT UNSIGNED
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- MY TICKETS
|
||||
CREATE TABLE IF NOT EXISTS my_tickets (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
userId BIGINT UNSIGNED NOT NULL,
|
||||
game_type VARCHAR(32) NOT NULL,
|
||||
draw_date DATE NOT NULL,
|
||||
ball1 TINYINT UNSIGNED,
|
||||
ball2 TINYINT UNSIGNED,
|
||||
ball3 TINYINT UNSIGNED,
|
||||
ball4 TINYINT UNSIGNED,
|
||||
ball5 TINYINT UNSIGNED,
|
||||
ball6 TINYINT UNSIGNED,
|
||||
bonus1 TINYINT UNSIGNED,
|
||||
bonus2 TINYINT UNSIGNED,
|
||||
duplicate TINYINT(1) NOT NULL DEFAULT 0,
|
||||
purchase_date DATETIME,
|
||||
purchase_method VARCHAR(50),
|
||||
image_path VARCHAR(255),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
matched_main TINYINT UNSIGNED,
|
||||
matched_bonus TINYINT UNSIGNED,
|
||||
prize_tier VARCHAR(50),
|
||||
is_winner TINYINT(1) NOT NULL DEFAULT 0,
|
||||
prize_amount BIGINT,
|
||||
prize_label VARCHAR(100),
|
||||
syndicate_id BIGINT UNSIGNED,
|
||||
CONSTRAINT fk_my_tickets_user
|
||||
FOREIGN KEY (userId) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- USERS MESSAGES
|
||||
CREATE TABLE IF NOT EXISTS users_messages (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
senderId BIGINT UNSIGNED NOT NULL,
|
||||
recipientId BIGINT UNSIGNED NOT NULL,
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
message MEDIUMTEXT,
|
||||
is_read TINYINT(1) NOT NULL DEFAULT 0,
|
||||
is_archived TINYINT(1) NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
archived_at DATETIME NULL,
|
||||
CONSTRAINT fk_users_messages_sender
|
||||
FOREIGN KEY (senderId) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_users_messages_recipient
|
||||
FOREIGN KEY (recipientId) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- USERS NOTIFICATIONS
|
||||
CREATE TABLE IF NOT EXISTS users_notification (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
subject VARCHAR(255),
|
||||
body MEDIUMTEXT,
|
||||
is_read TINYINT(1) NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_users_notification_user
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- AUDITLOG
|
||||
CREATE TABLE IF NOT EXISTS auditlog (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(191),
|
||||
success TINYINT(1),
|
||||
timestamp DATETIME
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- LOG: TICKET MATCHING
|
||||
CREATE TABLE IF NOT EXISTS log_ticket_matching (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
triggered_by VARCHAR(191),
|
||||
run_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
tickets_matched INT,
|
||||
winners_found INT,
|
||||
notes TEXT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ADMIN ACCESS LOG
|
||||
CREATE TABLE IF NOT EXISTS admin_access_log (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED,
|
||||
accessed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
path VARCHAR(255),
|
||||
ip VARCHAR(64),
|
||||
user_agent VARCHAR(255),
|
||||
CONSTRAINT fk_admin_access_user
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- AUDIT LOG (new)
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED,
|
||||
username VARCHAR(191),
|
||||
action VARCHAR(191),
|
||||
path VARCHAR(255),
|
||||
ip VARCHAR(64),
|
||||
user_agent VARCHAR(255),
|
||||
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_audit_log_user
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- AUDIT LOGIN (new)
|
||||
CREATE TABLE IF NOT EXISTS audit_login (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(191),
|
||||
success TINYINT(1),
|
||||
ip VARCHAR(64),
|
||||
user_agent VARCHAR(255),
|
||||
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- SYNDICATES
|
||||
CREATE TABLE IF NOT EXISTS syndicates (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(191) NOT NULL,
|
||||
description TEXT,
|
||||
owner_id BIGINT UNSIGNED NOT NULL,
|
||||
join_code VARCHAR(191) UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_syndicates_owner
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- SYNDICATE MEMBERS
|
||||
CREATE TABLE IF NOT EXISTS syndicate_members (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
syndicate_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
role VARCHAR(32) NOT NULL DEFAULT 'member',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_synmem_syn
|
||||
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_synmem_user
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT uq_synmem UNIQUE (syndicate_id, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- SYNDICATE INVITES
|
||||
CREATE TABLE IF NOT EXISTS syndicate_invites (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
syndicate_id BIGINT UNSIGNED NOT NULL,
|
||||
invited_user_id BIGINT UNSIGNED NOT NULL,
|
||||
sent_by_user_id BIGINT UNSIGNED NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_syninv_syn
|
||||
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_syninv_invited
|
||||
FOREIGN KEY (invited_user_id) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_syninv_sender
|
||||
FOREIGN KEY (sent_by_user_id) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- SYNDICATE INVITE TOKENS
|
||||
CREATE TABLE IF NOT EXISTS syndicate_invite_tokens (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
syndicate_id BIGINT UNSIGNED NOT NULL,
|
||||
token VARCHAR(191) NOT NULL UNIQUE,
|
||||
invited_by_user_id BIGINT UNSIGNED NOT NULL,
|
||||
accepted_by_user_id BIGINT UNSIGNED,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
accepted_at DATETIME,
|
||||
expires_at DATETIME,
|
||||
CONSTRAINT fk_syninvtoken_syn
|
||||
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_syninvtoken_invitedby
|
||||
FOREIGN KEY (invited_by_user_id) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_syninvtoken_acceptedby
|
||||
FOREIGN KEY (accepted_by_user_id) REFERENCES users(id)
|
||||
ON UPDATE CASCADE ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
3
internal/storage/mysql/notifications/create.go
Normal file
3
internal/storage/mysql/notifications/create.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package storage
|
||||
|
||||
// ToDo: somethign must create notifications?
|
||||
3
internal/storage/mysql/notifications/delete.go
Normal file
3
internal/storage/mysql/notifications/delete.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package storage
|
||||
|
||||
// ToDo: not used, check messages and do something similar maybe dont store them?
|
||||
@@ -1,13 +1,28 @@
|
||||
package storage
|
||||
|
||||
//ToDo: should be using my own logging wrapper?
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/internal/models"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func GetNotificationCount(db *sql.DB, userID int) int {
|
||||
var count int
|
||||
err := db.QueryRow(`
|
||||
@@ -46,40 +61,3 @@ func GetRecentNotifications(db *sql.DB, userID int, limit int) []models.Notifica
|
||||
|
||||
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
|
||||
}
|
||||
29
internal/storage/mysql/notifications/update.go
Normal file
29
internal/storage/mysql/notifications/update.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package storage
|
||||
|
||||
// ToDo: Should be logging fmt to my loggign wrapper
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
30
internal/storage/mysql/results/thunderball/create.go
Normal file
30
internal/storage/mysql/results/thunderball/create.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"synlotto-website/internal/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
|
||||
}
|
||||
1
internal/storage/mysql/schema.go
Normal file
1
internal/storage/mysql/schema.go
Normal file
@@ -0,0 +1 @@
|
||||
package storage
|
||||
31
internal/storage/mysql/seeds/thunderball_seed.go
Normal file
31
internal/storage/mysql/seeds/thunderball_seed.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
)
|
||||
|
||||
// this is an example currenlty not true data
|
||||
func Seed(db *sql.DB) error {
|
||||
if _, err := db.Exec(`
|
||||
INSERT INTO settings (k, v)
|
||||
SELECT 'site_name', 'SynLotto'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE k='site_name');
|
||||
`); err != nil {
|
||||
// settings table is optional; remove if you don't use it
|
||||
}
|
||||
|
||||
// 2) admin user (idempotent + secret from env)
|
||||
adminUser := "admin"
|
||||
adminHash := os.Getenv("ADMIN_BCRYPT_HASH") // or build from ADMIN_PASSWORD
|
||||
if adminHash == "" {
|
||||
// skip silently if you don’t want to hard-require it
|
||||
return nil
|
||||
}
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO users (username, password_hash, is_admin)
|
||||
VALUES (?, ?, 1)
|
||||
ON DUPLICATE KEY UPDATE is_admin=VALUES(is_admin)
|
||||
`, adminUser, adminHash)
|
||||
return err
|
||||
}
|
||||
21
internal/storage/mysql/syndicate/create.go
Normal file
21
internal/storage/mysql/syndicate/create.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
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 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
|
||||
}
|
||||
146
internal/storage/mysql/syndicate/read.go
Normal file
146
internal/storage/mysql/syndicate/read.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"synlotto-website/internal/models"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func GetPendingSyndicateInvites(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 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 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 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 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 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 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
|
||||
}
|
||||
14
internal/storage/mysql/syndicate/update.go
Normal file
14
internal/storage/mysql/syndicate/update.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func UpdateInviteStatus(db *sql.DB, inviteID int, status string) error {
|
||||
_, err := db.Exec(`
|
||||
UPDATE syndicate_invites
|
||||
SET status = ?
|
||||
WHERE id = ?
|
||||
`, status, inviteID)
|
||||
return err
|
||||
}
|
||||
@@ -3,32 +3,11 @@ package storage
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"strings"
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/internal/helpers"
|
||||
"synlotto-website/internal/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
|
||||
}
|
||||
|
||||
// ToDo: Has both insert and select need to break into read and write.
|
||||
func InsertTicket(db *sql.DB, ticket models.Ticket) error {
|
||||
var bonus1Val interface{}
|
||||
var bonus2Val interface{}
|
||||
22
internal/storage/mysql/users/create.go
Normal file
22
internal/storage/mysql/users/create.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package storage
|
||||
|
||||
// ToDo.. "errors" should this not be using my custom log wrapper
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
type UsersRepo struct{ db *sql.DB}
|
||||
|
||||
func NewUsersRepo(db *.sql.DB) *UsersRepo { return &UsersRepo{db: db} }
|
||||
|
||||
func (r *UsersRepo) Create(ctx context.Context, username, passwordHash string, isAdmin bool) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, ?)`,
|
||||
username, passwordHash, isAdmin,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package storage
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"synlotto-website/logging"
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/internal/logging"
|
||||
"synlotto-website/internal/models"
|
||||
)
|
||||
|
||||
func GetUserByID(db *sql.DB, id int) *models.User {
|
||||
125
internal/storage/sqlite/syndicate.go
Normal file
125
internal/storage/sqlite/syndicate.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"synlotto-website/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
// todo should be a ticket function?
|
||||
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
|
||||
}
|
||||
|
||||
// both a read and inset break up
|
||||
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
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
Username string
|
||||
PasswordHash string
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID int
|
||||
UserId int
|
||||
Subject string
|
||||
Body string
|
||||
IsRead bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID int
|
||||
SenderId int
|
||||
RecipientId int
|
||||
Subject string
|
||||
Message string
|
||||
IsRead bool
|
||||
CreatedAt time.Time
|
||||
ArchivedAt *time.Time
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
func SetDB(database *sql.DB) {
|
||||
db = database
|
||||
}
|
||||
|
||||
func CreateUser(username, passwordHash string) error {
|
||||
_, err := db.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", username, passwordHash)
|
||||
//ToDo: Why is SQL in here?
|
||||
return err
|
||||
}
|
||||
|
||||
func GetUserByUsername(username string) *User {
|
||||
row := db.QueryRow("SELECT id, username, password_hash FROM users WHERE username = ?", username)
|
||||
|
||||
var user User
|
||||
err := row.Scan(&user.Id, &user.Username, &user.PasswordHash)
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
log.Println("DB error:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return &user
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package rules
|
||||
|
||||
import "synlotto-website/models"
|
||||
|
||||
type PrizeInfo struct {
|
||||
Tier string
|
||||
Amount float64
|
||||
Label string
|
||||
}
|
||||
|
||||
var ThunderballPrizeRules = []models.PrizeRule{
|
||||
{Game: "Thunderball", MainMatches: 0, BonusMatches: 1, Tier: "Tier 1"},
|
||||
{Game: "Thunderball", MainMatches: 1, BonusMatches: 1, Tier: "Tier 2"},
|
||||
{Game: "Thunderball", MainMatches: 2, BonusMatches: 1, Tier: "Tier 3"},
|
||||
{Game: "Thunderball", MainMatches: 3, BonusMatches: 0, Tier: "Tier 4"},
|
||||
{Game: "Thunderball", MainMatches: 3, BonusMatches: 1, Tier: "Tier 5"},
|
||||
{Game: "Thunderball", MainMatches: 4, BonusMatches: 0, Tier: "Tier 6"},
|
||||
{Game: "Thunderball", MainMatches: 4, BonusMatches: 1, Tier: "Tier 7"},
|
||||
{Game: "Thunderball", MainMatches: 5, BonusMatches: 0, Tier: "Second"},
|
||||
{Game: "Thunderball", MainMatches: 5, BonusMatches: 1, Tier: "Jackpot"},
|
||||
}
|
||||
|
||||
func GetThunderballPrizeIndex(main, bonus int) (int, bool) {
|
||||
switch {
|
||||
case main == 0 && bonus == 1:
|
||||
return 9, true
|
||||
case main == 1 && bonus == 1:
|
||||
return 8, true
|
||||
case main == 2 && bonus == 1:
|
||||
return 7, true
|
||||
case main == 3 && bonus == 0:
|
||||
return 6, true
|
||||
case main == 3 && bonus == 1:
|
||||
return 5, true
|
||||
case main == 4 && bonus == 0:
|
||||
return 4, true
|
||||
case main == 4 && bonus == 1:
|
||||
return 3, true
|
||||
case main == 5 && bonus == 0:
|
||||
return 2, true
|
||||
case main == 5 && bonus == 1:
|
||||
return 1, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user