Basic features done
This commit is contained in:
@@ -3,25 +3,16 @@ package lib
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
_ "turso.tech/database/tursogo"
|
||||
)
|
||||
|
||||
var DB *sql.DB
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Username string
|
||||
PasswordHash string
|
||||
}
|
||||
|
||||
// init sqlite db — always creates app.db at project root (run from root)
|
||||
func InitDB() {
|
||||
var err error
|
||||
DB, err = sql.Open("sqlite3", "./app.db")
|
||||
DB, err = sql.Open("turso", "app.db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -50,72 +41,66 @@ func InitDB() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// hash password
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// check password
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// create new user
|
||||
func CreateUser(username, password string) error {
|
||||
hash, err := HashPassword(password)
|
||||
// make rooms table
|
||||
roomTable := `
|
||||
CREATE TABLE IF NOT EXISTS rooms (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
code TEXT UNIQUE NOT NULL,
|
||||
scale TEXT NOT NULL DEFAULT 'fibonacci',
|
||||
owner_id INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
active_story_id INTEGER,
|
||||
FOREIGN KEY(owner_id) REFERENCES users(id)
|
||||
);`
|
||||
_, err = DB.Exec(roomTable)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Fatal(err)
|
||||
}
|
||||
_, err = DB.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", username, hash)
|
||||
return err
|
||||
}
|
||||
|
||||
// get user by name
|
||||
func GetUserByUsername(username string) (*User, error) {
|
||||
row := DB.QueryRow("SELECT id, username, password_hash FROM users WHERE username = ?", username)
|
||||
u := &User{}
|
||||
err := row.Scan(&u.ID, &u.Username, &u.PasswordHash)
|
||||
// make room_members table
|
||||
memberTable := `
|
||||
CREATE TABLE IF NOT EXISTS room_members (
|
||||
room_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
joined_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (room_id, user_id),
|
||||
FOREIGN KEY(room_id) REFERENCES rooms(id),
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
);`
|
||||
_, err = DB.Exec(memberTable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Fatal(err)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// create session token
|
||||
func CreateSession(userID int) (string, error) {
|
||||
sessionID := uuid.New().String()
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
|
||||
_, err := DB.Exec("INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)", sessionID, userID, expiresAt)
|
||||
// make stories table
|
||||
storyTable := `
|
||||
CREATE TABLE IF NOT EXISTS stories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
points INTEGER,
|
||||
voted INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(room_id) REFERENCES rooms(id)
|
||||
);`
|
||||
_, err = DB.Exec(storyTable)
|
||||
if err != nil {
|
||||
return "", err
|
||||
log.Fatal(err)
|
||||
}
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
// get user from session
|
||||
func GetUserFromSession(sessionID string) (*User, error) {
|
||||
row := DB.QueryRow(`
|
||||
SELECT u.id, u.username, u.password_hash
|
||||
FROM users u
|
||||
JOIN sessions s ON u.id = s.user_id
|
||||
WHERE s.id = ? AND s.expires_at > ?
|
||||
`, sessionID, time.Now())
|
||||
|
||||
u := &User{}
|
||||
err := row.Scan(&u.ID, &u.Username, &u.PasswordHash)
|
||||
// make votes table
|
||||
voteTable := `
|
||||
CREATE TABLE IF NOT EXISTS votes (
|
||||
story_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY (story_id, user_id),
|
||||
FOREIGN KEY(story_id) REFERENCES stories(id),
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
);`
|
||||
_, err = DB.Exec(voteTable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Fatal(err)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// delete session
|
||||
func DeleteSession(sessionID string) error {
|
||||
_, err := DB.Exec("DELETE FROM sessions WHERE id = ?", sessionID)
|
||||
return err
|
||||
}
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Room struct {
|
||||
ID int
|
||||
Name string
|
||||
Code string
|
||||
Scale string
|
||||
OwnerID int
|
||||
CreatedAt time.Time
|
||||
ActiveStoryID *int
|
||||
}
|
||||
|
||||
type RoomMember struct {
|
||||
RoomID int
|
||||
UserID int
|
||||
JoinedAt time.Time
|
||||
}
|
||||
|
||||
// generate unique room code
|
||||
func generateRoomCode() string {
|
||||
code := uuid.New().String()[:8]
|
||||
return code
|
||||
}
|
||||
|
||||
// create room
|
||||
func CreateRoom(name string, ownerID int, scale string) (*Room, error) {
|
||||
code := generateRoomCode()
|
||||
_, err := DB.Exec("INSERT INTO rooms (name, code, scale, owner_id, created_at, active_story_id) VALUES (?, ?, ?, ?, ?, NULL)", name, code, scale, ownerID, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
room := &Room{Name: name, Code: code, Scale: scale, OwnerID: ownerID, ActiveStoryID: nil}
|
||||
row := DB.QueryRow("SELECT id, name, code, scale, owner_id, created_at, active_story_id FROM rooms WHERE code = ?", code)
|
||||
err = row.Scan(&room.ID, &room.Name, &room.Code, &room.Scale, &room.OwnerID, &room.CreatedAt, &room.ActiveStoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// add owner as member
|
||||
DB.Exec("INSERT INTO room_members (room_id, user_id, joined_at) VALUES (?, ?, ?)", room.ID, ownerID, time.Now())
|
||||
return room, nil
|
||||
}
|
||||
|
||||
// get room by id
|
||||
func GetRoomByID(id int) (*Room, error) {
|
||||
row := DB.QueryRow("SELECT id, name, code, scale, owner_id, created_at, active_story_id FROM rooms WHERE id = ?", id)
|
||||
r := &Room{}
|
||||
err := row.Scan(&r.ID, &r.Name, &r.Code, &r.Scale, &r.OwnerID, &r.CreatedAt, &r.ActiveStoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// get room by code
|
||||
func GetRoomByCode(code string) (*Room, error) {
|
||||
row := DB.QueryRow("SELECT id, name, code, scale, owner_id, created_at, active_story_id FROM rooms WHERE code = ?", code)
|
||||
r := &Room{}
|
||||
err := row.Scan(&r.ID, &r.Name, &r.Code, &r.Scale, &r.OwnerID, &r.CreatedAt, &r.ActiveStoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// get rooms for user
|
||||
func GetRoomsForUser(userID int) ([]Room, error) {
|
||||
rows, err := DB.Query(`
|
||||
SELECT r.id, r.name, r.code, r.scale, r.owner_id, r.created_at, r.active_story_id, COUNT(rm.user_id) as member_count
|
||||
FROM rooms r
|
||||
LEFT JOIN room_members rm ON r.id = rm.room_id
|
||||
WHERE r.owner_id = ?
|
||||
GROUP BY r.id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var rooms []Room
|
||||
for rows.Next() {
|
||||
var r Room
|
||||
var memberCount int
|
||||
err := rows.Scan(&r.ID, &r.Name, &r.Code, &r.Scale, &r.OwnerID, &r.CreatedAt, &r.ActiveStoryID, &memberCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rooms = append(rooms, r)
|
||||
}
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
// set active story for room
|
||||
func SetActiveStory(roomID, storyID int) error {
|
||||
_, err := DB.Exec("UPDATE rooms SET active_story_id = ? WHERE id = ?", storyID, roomID)
|
||||
return err
|
||||
}
|
||||
|
||||
// add user to room
|
||||
func AddUserToRoom(roomID, userID int) error {
|
||||
_, err := DB.Exec("INSERT OR IGNORE INTO room_members (room_id, user_id, joined_at) VALUES (?, ?, ?)", roomID, userID, time.Now())
|
||||
return err
|
||||
}
|
||||
|
||||
// get room members
|
||||
func GetRoomMembers(roomID int) ([]User, error) {
|
||||
rows, err := DB.Query(`
|
||||
SELECT u.id, u.username, u.password_hash
|
||||
FROM users u
|
||||
JOIN room_members rm ON u.id = rm.user_id
|
||||
WHERE rm.room_id = ?
|
||||
`, roomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var members []User
|
||||
for rows.Next() {
|
||||
var u User
|
||||
err := rows.Scan(&u.ID, &u.Username, &u.PasswordHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
members = append(members, u)
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Story struct {
|
||||
ID int
|
||||
RoomID int
|
||||
Title string
|
||||
Points *int
|
||||
Voted bool
|
||||
}
|
||||
|
||||
type Vote struct {
|
||||
ID int
|
||||
StoryID int
|
||||
UserID int
|
||||
Value string
|
||||
}
|
||||
|
||||
// create story
|
||||
func CreateStory(roomID int, title string) (*Story, error) {
|
||||
_, err := DB.Exec("INSERT INTO stories (room_id, title, voted) VALUES (?, ?, 0)", roomID, title)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &Story{RoomID: roomID, Title: title}
|
||||
row := DB.QueryRow("SELECT id, room_id, title, points, voted FROM stories WHERE room_id = ? ORDER BY id DESC LIMIT 1", roomID)
|
||||
err = row.Scan(&s.ID, &s.RoomID, &s.Title, &s.Points, &s.Voted)
|
||||
return s, err
|
||||
}
|
||||
|
||||
// get stories for room
|
||||
func GetStoriesForRoom(roomID int) ([]Story, error) {
|
||||
rows, err := DB.Query("SELECT id, room_id, title, points, voted FROM stories WHERE room_id = ? ORDER BY id", roomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var stories []Story
|
||||
for rows.Next() {
|
||||
var s Story
|
||||
var points sql.NullInt64
|
||||
var voted int
|
||||
err := rows.Scan(&s.ID, &s.RoomID, &s.Title, &points, &voted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if points.Valid {
|
||||
p := int(points.Int64)
|
||||
s.Points = &p
|
||||
}
|
||||
s.Voted = voted == 1
|
||||
stories = append(stories, s)
|
||||
}
|
||||
return stories, nil
|
||||
}
|
||||
|
||||
// vote on story
|
||||
func VoteOnStory(storyID, userID int, value string) error {
|
||||
_, err := DB.Exec("INSERT OR REPLACE INTO votes (story_id, user_id, value) VALUES (?, ?, ?)", storyID, userID, value)
|
||||
return err
|
||||
}
|
||||
|
||||
// get votes for story
|
||||
func GetVotesForStory(storyID int) ([]Vote, error) {
|
||||
rows, err := DB.Query("SELECT story_id, user_id, value FROM votes WHERE story_id = ?", storyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var votes []Vote
|
||||
for rows.Next() {
|
||||
var v Vote
|
||||
err := rows.Scan(&v.StoryID, &v.UserID, &v.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
votes = append(votes, v)
|
||||
}
|
||||
return votes, nil
|
||||
}
|
||||
|
||||
// reveal votes — just marks the story as revealed
|
||||
func RevealVotes(storyID int) error {
|
||||
_, err := DB.Exec("UPDATE stories SET voted = 1 WHERE id = ?", storyID)
|
||||
return err
|
||||
}
|
||||
|
||||
type VoteView struct {
|
||||
Username string
|
||||
Value string
|
||||
}
|
||||
|
||||
// get votes with usernames for display
|
||||
func GetVotesWithUsernames(storyID int) ([]VoteView, error) {
|
||||
rows, err := DB.Query(`
|
||||
SELECT u.username, v.value
|
||||
FROM votes v
|
||||
JOIN users u ON v.user_id = u.id
|
||||
WHERE v.story_id = ?
|
||||
`, storyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var views []VoteView
|
||||
for rows.Next() {
|
||||
var vv VoteView
|
||||
err := rows.Scan(&vv.Username, &vv.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
views = append(views, vv)
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
// unreveal story — set voted back to 0 and clear points
|
||||
func UnrevealStory(storyID int) error {
|
||||
_, err := DB.Exec("UPDATE stories SET voted = 0, points = NULL WHERE id = ?", storyID)
|
||||
return err
|
||||
}
|
||||
|
||||
// clear all votes for a story
|
||||
func ClearVotesForStory(storyID int) error {
|
||||
_, err := DB.Exec("DELETE FROM votes WHERE story_id = ?", storyID)
|
||||
return err
|
||||
}
|
||||
|
||||
// rename story
|
||||
func RenameStory(storyID int, title string) error {
|
||||
_, err := DB.Exec("UPDATE stories SET title = ? WHERE id = ?", title, storyID)
|
||||
return err
|
||||
}
|
||||
|
||||
// delete story and its votes
|
||||
func DeleteStory(storyID int) error {
|
||||
_, err := DB.Exec("DELETE FROM votes WHERE story_id = ?", storyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = DB.Exec("DELETE FROM stories WHERE id = ?", storyID)
|
||||
return err
|
||||
}
|
||||
|
||||
func tshirtToPoints(s string) float64 {
|
||||
switch s {
|
||||
case "XS":
|
||||
return 1
|
||||
case "S":
|
||||
return 3
|
||||
case "M":
|
||||
return 5
|
||||
case "L":
|
||||
return 8
|
||||
case "XL":
|
||||
return 13
|
||||
case "XXL":
|
||||
return 21
|
||||
case "?":
|
||||
return 0
|
||||
default:
|
||||
var n int
|
||||
fmt.Sscanf(s, "%d", &n)
|
||||
return float64(n)
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Username string
|
||||
PasswordHash string
|
||||
}
|
||||
|
||||
// hash password
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// check password
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// create new user
|
||||
func CreateUser(username, password string) error {
|
||||
hash, err := HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = DB.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", username, hash)
|
||||
return err
|
||||
}
|
||||
|
||||
// get user by name
|
||||
func GetUserByUsername(username string) (*User, error) {
|
||||
row := DB.QueryRow("SELECT id, username, password_hash FROM users WHERE username = ?", username)
|
||||
u := &User{}
|
||||
err := row.Scan(&u.ID, &u.Username, &u.PasswordHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// create session token
|
||||
func CreateSession(userID int) (string, error) {
|
||||
sessionID := uuid.New().String()
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
|
||||
_, err := DB.Exec("INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)", sessionID, userID, expiresAt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
// get user from session
|
||||
func GetUserFromSession(sessionID string) (*User, error) {
|
||||
row := DB.QueryRow(`
|
||||
SELECT u.id, u.username, u.password_hash
|
||||
FROM users u
|
||||
JOIN sessions s ON u.id = s.user_id
|
||||
WHERE s.id = ? AND s.expires_at > ?
|
||||
`, sessionID, time.Now())
|
||||
|
||||
u := &User{}
|
||||
err := row.Scan(&u.ID, &u.Username, &u.PasswordHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// delete session
|
||||
func DeleteSession(sessionID string) error {
|
||||
_, err := DB.Exec("DELETE FROM sessions WHERE id = ?", sessionID)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user