1
0
Files
coconut-leaf/backend/database/sqlite_database.go

1139 lines
35 KiB
Go

package database
import (
"context"
"database/sql"
"fmt"
"strings"
"sync"
_ "github.com/mattn/go-sqlite3"
"github.com/yyc12345/coconut-leaf/backend/dt"
"github.com/yyc12345/coconut-leaf/backend/utils"
)
// sqliteSchema is the DDL of the six tables, inlined from the legacy
// sql/sqlite.sql (ccn_-free snake_case column names). Each entry is executed as
// a single statement in Init.
var sqliteSchema = []string{
`CREATE TABLE user(
[name] TEXT NOT NULL,
[password] TEXT NOT NULL,
[is_admin] TINYINT NOT NULL CHECK(is_admin = 1 OR is_admin = 0),
[salt] INTEGER NOT NULL,
PRIMARY KEY (name)
);`,
`CREATE TABLE token(
[user] TEXT NOT NULL,
[token] TEXT UNIQUE NOT NULL,
[token_expire_on] BIGINT NOT NULL,
[ua] TEXT NOT NULL,
[ip] TEXT NOT NULL,
FOREIGN KEY (user) REFERENCES user(name) ON DELETE CASCADE
);`,
`CREATE TABLE collection(
[uuid] TEXT NOT NULL,
[name] TEXT NOT NULL,
[user] TEXT NOT NULL,
[last_change] TEXT NOT NULL,
PRIMARY KEY (uuid),
FOREIGN KEY (user) REFERENCES user(name) ON DELETE CASCADE
);`,
`CREATE TABLE share(
[uuid] TEXT NOT NULL,
[target] TEXT NOT NULL,
FOREIGN KEY (uuid) REFERENCES collection(uuid) ON DELETE CASCADE
FOREIGN KEY (target) REFERENCES user(name) ON DELETE CASCADE
);`,
`CREATE TABLE calendar(
[uuid] TEXT NOT NULL,
[belong_to] TEXT NOT NULL,
[title] TEXT NOT NULL,
[description] TEXT NOT NULL,
[last_change] TEXT NOT NULL,
[event_date_time_start] BIGINT NOT NULL,
[event_date_time_end] BIGINT NOT NULL,
[timezone_offset] INT NOT NULL,
[loop_rules] TEXT NOT NULL,
[loop_date_time_start] BIGINT NOT NULL,
[loop_date_time_end] BIGINT NOT NULL,
PRIMARY KEY (uuid),
FOREIGN KEY (belong_to) REFERENCES collection(uuid) ON DELETE CASCADE
);`,
`CREATE TABLE todo(
[uuid] TEXT NOT NULL,
[belong_to] TEXT NOT NULL,
[data] TEXT NOT NULL,
[last_change] TEXT NOT NULL,
PRIMARY KEY (uuid),
FOREIGN KEY (belong_to) REFERENCES user(name) ON DELETE CASCADE
);`,
}
// dbOp is the unit of database work executed inside a safeOp transaction.
type dbOp func(ctx context.Context, tx *sql.Tx) (any, error)
// Sqlite3Database implements Database backed by a SQLite file. It owns the
// connection, a serializing mutex (SQLite is driven single-threaded here, as in
// the legacy code) and the auto token-clean bookkeeping.
type Sqlite3Database struct {
deps Deps
db *sql.DB
mu sync.Mutex
latestClean int
}
// NewSqlite3Database opens the SQLite file and applies the legacy open() PRAGMAs
// (UTF-8 encoding, foreign keys). A single connection is used to mirror the
// legacy single-connection model and keep PRAGMAs effective.
func NewSqlite3Database(deps Deps) (*Sqlite3Database, error) {
db, err := sql.Open("sqlite3", deps.Cfg.Database.Sqlite.Path)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
if _, err := db.Exec(`PRAGMA encoding = "UTF-8";`); err != nil {
db.Close()
return nil, err
}
if _, err := db.Exec(`PRAGMA foreign_keys = ON;`); err != nil {
db.Close()
return nil, err
}
if err := db.Ping(); err != nil {
db.Close()
return nil, err
}
return &Sqlite3Database{deps: deps, db: db}, nil
}
// Init creates the schema and the first admin user, mirroring the legacy init().
func (d *Sqlite3Database) Init(username, password string) error {
d.mu.Lock()
defer d.mu.Unlock()
tx, err := d.db.Begin()
if err != nil {
return err
}
for _, stmt := range sqliteSchema {
if _, err := tx.Exec(stmt); err != nil {
tx.Rollback()
return err
}
}
if _, err := tx.Exec("INSERT INTO user VALUES (?, ?, ?, ?);", username, utils.ComputePasswordHash(password), 1, utils.GenerateSalt()); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
// Close closes the underlying connection.
func (d *Sqlite3Database) Close() error {
return d.db.Close()
}
// safeOp runs fn inside a serialized transaction. It reproduces the legacy
// SafeDatabaseOperation decorator: lock, begin, periodically purge outdated
// tokens (logging when it does), run the work, commit on success / rollback on
// error. DB errors are logged only in debug mode, matching the legacy
// `if cfg.others.debug: LOGGER.exception(e)` behavior.
func (d *Sqlite3Database) safeOp(ctx context.Context, fn dbOp) ResponseBody {
d.mu.Lock()
defer d.mu.Unlock()
debug := d.deps.Cfg.Others.Debug
logger := d.deps.Logger
tx, err := d.db.BeginTx(ctx, nil)
if err != nil {
if debug {
logger.Error("failed to begin transaction", "error", err)
}
return ResponseBody{Success: false, Error: err.Error()}
}
if now := utils.GetCurrentTimestamp(); now-d.latestClean > d.deps.Cfg.Others.AutoTokenCleanDuration {
d.latestClean = now
logger.Info("Cleaning outdated token...")
if err := tokenOperClean(ctx, tx); err != nil {
tx.Rollback()
if debug {
logger.Error("failed to clean outdated tokens", "error", err)
}
return ResponseBody{Success: false, Error: err.Error()}
}
}
data, err := fn(ctx, tx)
if err != nil {
tx.Rollback()
if debug {
logger.Error("database operation failed", "error", err)
}
return ResponseBody{Success: false, Error: err.Error()}
}
if err := tx.Commit(); err != nil {
if debug {
logger.Error("failed to commit transaction", "error", err)
}
return ResponseBody{Success: false, Error: err.Error()}
}
return ResponseBody{Success: true, Data: data}
}
// region: Token-related Internal Operations
// tokenOperClean deletes outdated tokens. Mirrors tokenOper_clean.
func tokenOperClean(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, "DELETE FROM token WHERE [token_expire_on] <= ?;", utils.GetCurrentTimestamp())
return err
}
// tokenOperPostponeExpireOn pushes the token's expiry forward. Mirrors
// tokenOper_postpone_expireOn.
func tokenOperPostponeExpireOn(ctx context.Context, tx *sql.Tx, token string) error {
_, err := tx.ExecContext(ctx, "UPDATE token SET [token_expire_on] = ? WHERE [token] = ?;", utils.GetTokenExpireOn(), token)
return err
}
// tokenOperGetUsername validates token, postpones its expiry, and returns the
// owning username. Mirrors tokenOper_get_username.
func tokenOperGetUsername(ctx context.Context, tx *sql.Tx, token string) (string, error) {
var user string
err := tx.QueryRowContext(ctx, "SELECT [user] FROM token WHERE [token] = ? AND [token_expire_on] > ?;", token, utils.GetCurrentTimestamp()).Scan(&user)
if err != nil {
return "", err
}
if err := tokenOperPostponeExpireOn(ctx, tx, token); err != nil {
return "", err
}
return user, nil
}
// tokenOperCheckValid validates a token (mirrors tokenOper_check_valid).
func tokenOperCheckValid(ctx context.Context, tx *sql.Tx, token string) error {
_, err := tokenOperGetUsername(ctx, tx, token)
return err
}
// tokenOperIsAdmin reports whether the user is an admin (mirrors
// tokenOper_is_admin).
func tokenOperIsAdmin(ctx context.Context, tx *sql.Tx, username string) (bool, error) {
var isAdmin int64
err := tx.QueryRowContext(ctx, "SELECT [is_admin] FROM user WHERE [name] = ?;", username).Scan(&isAdmin)
if err != nil {
return false, err
}
return isAdmin == 1, nil
}
// endregion
// region: Operation Functions
// region: Common Namespace
func (d *Sqlite3Database) CommonSalt(ctx context.Context, username string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
salt := utils.GenerateSalt()
if _, err := tx.ExecContext(ctx, "UPDATE user SET [salt] = ? WHERE [name] = ?;", salt, username); err != nil {
return nil, err
}
return salt, nil
})
}
func (d *Sqlite3Database) CommonLogin(ctx context.Context, username, password, clientUa, clientIp string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
var gottenPassword string
var gottenSalt int64
err := tx.QueryRowContext(ctx, "SELECT [password], [salt] FROM user WHERE [name] = ?;", username).Scan(&gottenPassword, &gottenSalt)
if err != nil {
return nil, fmt.Errorf("Login authentication failed")
}
if password == utils.ComputePasswordHashWithSalt(gottenPassword, int(gottenSalt)) {
token := utils.GenerateToken(username)
if _, err := tx.ExecContext(ctx, "UPDATE user SET [salt] = ? WHERE [name] = ?;", utils.GenerateSalt(), username); err != nil {
return nil, err
}
if _, err := tx.ExecContext(ctx, "INSERT INTO token VALUES (?, ?, ?, ?, ?);", username, token, utils.GetTokenExpireOn(), clientUa, clientIp); err != nil {
return nil, err
}
return token, nil
}
return nil, fmt.Errorf("Login authentication failed")
})
}
func (d *Sqlite3Database) CommonWebLogin(ctx context.Context, username, password, clientUa, clientIp string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
logger := d.deps.Logger
logger.Debug("WebLogin username", "username", username)
logger.Debug("WebLogin password", "password", password)
passwordHash := utils.ComputePasswordHash(password)
logger.Debug("WebLogin password hash", "hash", passwordHash)
var name string
err := tx.QueryRowContext(ctx, "SELECT [name] FROM user WHERE [name] = ? AND [password] = ?;", username, passwordHash).Scan(&name)
if err != nil {
return nil, fmt.Errorf("Login authentication failed")
}
token := utils.GenerateToken(username)
if _, err := tx.ExecContext(ctx, "INSERT INTO token VALUES (?, ?, ?, ?, ?);", username, token, utils.GetTokenExpireOn(), clientUa, clientIp); err != nil {
return nil, err
}
return token, nil
})
}
func (d *Sqlite3Database) CommonLogout(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
if _, err := tx.ExecContext(ctx, "DELETE FROM token WHERE [token] = ?;", token); err != nil {
return nil, err
}
return true, nil
})
}
func (d *Sqlite3Database) CommonTokenValid(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
return true, nil
})
}
// endregion
// region: Calendar Namespace
func (d *Sqlite3Database) CalendarGetFull(ctx context.Context, token string, startDateTime, endDateTime int64) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, `SELECT calendar.* FROM calendar INNER JOIN collection
ON collection.uuid = calendar.belong_to
WHERE (collection.user = ? AND calendar.loop_date_time_end >= ? AND calendar.loop_date_time_start - (calendar.event_date_time_end - calendar.event_date_time_start) <= ?);`,
username, startDateTime, endDateTime)
if err != nil {
return nil, err
}
defer rows.Close()
result := [][]any{}
for rows.Next() {
var uuid, belongTo, title, description, lastChange, loopRules string
var eventStart, eventEnd, tzOffset, loopStart, loopEnd int64
if err := rows.Scan(&uuid, &belongTo, &title, &description, &lastChange, &eventStart, &eventEnd, &tzOffset, &loopRules, &loopStart, &loopEnd); err != nil {
return nil, err
}
result = append(result, []any{uuid, belongTo, title, description, lastChange, eventStart, eventEnd, tzOffset, loopRules, loopStart, loopEnd})
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
func (d *Sqlite3Database) CalendarGetList(ctx context.Context, token string, startDateTime, endDateTime int64) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, `SELECT calendar.uuid FROM calendar INNER JOIN collection
ON collection.uuid = calendar.belong_to
WHERE (collection.user = ? AND calendar.loop_date_time_end >= ? AND calendar.loop_date_time_start - (calendar.event_date_time_end - calendar.event_date_time_start) <= ?);`,
username, startDateTime, endDateTime)
if err != nil {
return nil, err
}
defer rows.Close()
result := []string{}
for rows.Next() {
var uuid string
if err := rows.Scan(&uuid); err != nil {
return nil, err
}
result = append(result, uuid)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
func (d *Sqlite3Database) CalendarGetDetail(ctx context.Context, token, uuid string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
var dUuid, dBelongTo, dTitle, dDescription, dLastChange, dLoopRules string
var dEventStart, dEventEnd, dTzOffset, dLoopStart, dLoopEnd int64
err := tx.QueryRowContext(ctx, "SELECT * FROM calendar WHERE [uuid] = ?;", uuid).
Scan(&dUuid, &dBelongTo, &dTitle, &dDescription, &dLastChange, &dEventStart, &dEventEnd, &dTzOffset, &dLoopRules, &dLoopStart, &dLoopEnd)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return []any{dUuid, dBelongTo, dTitle, dDescription, dLastChange, dEventStart, dEventEnd, dTzOffset, dLoopRules, dLoopStart, dLoopEnd}, nil
})
}
func (d *Sqlite3Database) CalendarUpdate(ctx context.Context, token, uuid, lastChange string, opts CalendarUpdateOptions) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
// get prev data
var uUuid, uBelongTo, uTitle, uDescription, uLastChange, uLoopRules string
var uEventStart, uEventEnd, uTzOffset, uLoopStart, uLoopEnd int64
err := tx.QueryRowContext(ctx, "SELECT * FROM calendar WHERE [uuid] = ? AND [last_change] = ?;", uuid, lastChange).
Scan(&uUuid, &uBelongTo, &uTitle, &uDescription, &uLastChange, &uEventStart, &uEventEnd, &uTzOffset, &uLoopRules, &uLoopStart, &uLoopEnd)
if err != nil {
return nil, err
}
// construct update data
newLastUpdate := utils.GenerateUUID()
sqlList := []string{"[last_change] = ?"}
args := []any{newLastUpdate}
// analyse opt arg
reAnalyseLoop := false
effEventStart, effTzOffset, effLoopRules := uEventStart, uTzOffset, uLoopRules
if opts.BelongTo != nil {
sqlList = append(sqlList, "[belong_to] = ?")
args = append(args, *opts.BelongTo)
}
if opts.Title != nil {
sqlList = append(sqlList, "[title] = ?")
args = append(args, *opts.Title)
}
if opts.Description != nil {
sqlList = append(sqlList, "[description] = ?")
args = append(args, *opts.Description)
}
if opts.EventDateTimeStart != nil {
sqlList = append(sqlList, "[event_date_time_start] = ?")
args = append(args, *opts.EventDateTimeStart)
reAnalyseLoop = true
effEventStart = *opts.EventDateTimeStart
}
if opts.EventDateTimeEnd != nil {
sqlList = append(sqlList, "[event_date_time_end] = ?")
args = append(args, *opts.EventDateTimeEnd)
}
if opts.LoopRules != nil {
sqlList = append(sqlList, "[loop_rules] = ?")
args = append(args, *opts.LoopRules)
reAnalyseLoop = true
effLoopRules = *opts.LoopRules
}
if opts.TimezoneOffset != nil {
sqlList = append(sqlList, "[timezone_offset] = ?")
args = append(args, *opts.TimezoneOffset)
reAnalyseLoop = true
effTzOffset = *opts.TimezoneOffset
}
if reAnalyseLoop {
// re-compute loop data and upload it into list
sqlList = append(sqlList, "[loop_date_time_start] = ?")
args = append(args, effEventStart)
sqlList = append(sqlList, "[loop_date_time_end] = ?")
loopEnd, err := dt.ResolveLoopStr(effLoopRules, effEventStart, effTzOffset)
if err != nil {
return nil, err
}
args = append(args, loopEnd)
}
args = append(args, uuid)
query := "UPDATE calendar SET " + strings.Join(sqlList, ", ") + " WHERE [uuid] = ?;"
res, err := tx.ExecContext(ctx, query, args...)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to update due to no matched rows or too much rows.")
}
return newLastUpdate, nil
})
}
func (d *Sqlite3Database) CalendarAdd(ctx context.Context, token, belongTo, title, description string, eventDateTimeStart, eventDateTimeEnd int64, loopRules string, timezoneOffset int64) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
newUuid := utils.GenerateUUID()
newLastUpdate := utils.GenerateUUID()
loopDateTimeStart := eventDateTimeStart
loopDateTimeEnd, err := dt.ResolveLoopStr(loopRules, eventDateTimeStart, timezoneOffset)
if err != nil {
return nil, err
}
if _, err := tx.ExecContext(ctx, "INSERT INTO calendar VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
newUuid, belongTo, title, description, newLastUpdate,
eventDateTimeStart, eventDateTimeEnd, timezoneOffset, loopRules,
loopDateTimeStart, loopDateTimeEnd); err != nil {
return nil, err
}
return newUuid, nil
})
}
func (d *Sqlite3Database) CalendarDelete(ctx context.Context, token, uuid, lastChange string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
res, err := tx.ExecContext(ctx, "DELETE FROM calendar WHERE [uuid] = ? AND [last_change] = ?;", uuid, lastChange)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to delete due to no matched rows or too much rows.")
}
return true, nil
})
}
// endregion
// region: Collection Namespace
func (d *Sqlite3Database) CollectionGetFullOwn(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ?;", username)
if err != nil {
return nil, err
}
defer rows.Close()
result := [][]any{}
for rows.Next() {
var uuid, name, lastChange string
if err := rows.Scan(&uuid, &name, &lastChange); err != nil {
return nil, err
}
result = append(result, []any{uuid, name, lastChange})
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
func (d *Sqlite3Database) CollectionGetListOwn(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT [uuid] FROM collection WHERE [user] = ?;", username)
if err != nil {
return nil, err
}
defer rows.Close()
result := []string{}
for rows.Next() {
var uuid string
if err := rows.Scan(&uuid); err != nil {
return nil, err
}
result = append(result, uuid)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
func (d *Sqlite3Database) CollectionGetDetailOwn(ctx context.Context, token, uuid string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
var dUuid, dName, dLastChange string
err = tx.QueryRowContext(ctx, "SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ? AND [uuid] = ?;", username, uuid).Scan(&dUuid, &dName, &dLastChange)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return []any{dUuid, dName, dLastChange}, nil
})
}
func (d *Sqlite3Database) CollectionAddOwn(ctx context.Context, token, name string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
newUuid := utils.GenerateUUID()
newLastUpdate := utils.GenerateUUID()
if _, err := tx.ExecContext(ctx, "INSERT INTO collection VALUES (?, ?, ?, ?);", newUuid, name, username, newLastUpdate); err != nil {
return nil, err
}
return newUuid, nil
})
}
func (d *Sqlite3Database) CollectionUpdateOwn(ctx context.Context, token, uuid, name, lastChange string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
newLastUpdate := utils.GenerateUUID()
res, err := tx.ExecContext(ctx, "UPDATE collection SET [name] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;", name, newLastUpdate, uuid, lastChange)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to update due to no matched rows or too much rows.")
}
return newLastUpdate, nil
})
}
func (d *Sqlite3Database) CollectionDeleteOwn(ctx context.Context, token, uuid, lastChange string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
res, err := tx.ExecContext(ctx, "DELETE FROM collection WHERE [uuid] = ? AND [last_change] = ?;", uuid, lastChange)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to delete due to no matched rows or too much rows.")
}
return true, nil
})
}
func (d *Sqlite3Database) CollectionGetSharing(ctx context.Context, token, uuid string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT [target] FROM share WHERE [uuid] = ?;", uuid)
if err != nil {
return nil, err
}
defer rows.Close()
result := []string{}
for rows.Next() {
var target string
if err := rows.Scan(&target); err != nil {
return nil, err
}
result = append(result, target)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
func (d *Sqlite3Database) CollectionDeleteSharing(ctx context.Context, token, uuid, target, lastChange string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
newLastUpdate := utils.GenerateUUID()
// NOTE: the legacy SQL had a stray comma ("SET [last_change] = ?, WHERE ...")
// which is a syntax error; it is corrected here.
res, err := tx.ExecContext(ctx, "UPDATE collection SET [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;", newLastUpdate, uuid, lastChange)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to delete due to no matched rows or too much rows.")
}
res, err = tx.ExecContext(ctx, "DELETE FROM share WHERE [uuid] = ? AND [target] = ?;", uuid, target)
if err != nil {
return nil, err
}
n, err = res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to delete due to no matched rows or too much rows.")
}
return newLastUpdate, nil
})
}
func (d *Sqlite3Database) CollectionAddSharing(ctx context.Context, token, uuid, target, lastChange string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
newLastUpdate := utils.GenerateUUID()
res, err := tx.ExecContext(ctx, "UPDATE collection SET [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;", newLastUpdate, uuid, lastChange)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to delete due to no matched rows or too much rows.")
}
var dupUuid, dupTarget string
err = tx.QueryRowContext(ctx, "SELECT * FROM share WHERE [uuid] = ? AND [target] = ?;", uuid, target).Scan(&dupUuid, &dupTarget)
if err == nil {
return nil, fmt.Errorf("Fail to insert duplicated item.")
} else if err != sql.ErrNoRows {
return nil, err
}
if _, err := tx.ExecContext(ctx, "INSERT INTO share VALUES (?, ?);", uuid, target); err != nil {
return nil, err
}
return newLastUpdate, nil
})
}
func (d *Sqlite3Database) CollectionGetShared(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, `SELECT collection.uuid, collection.name, collection.user
FROM share INNER JOIN collection
ON share.uuid = collection.uuid
WHERE share.target = ?;`, username)
if err != nil {
return nil, err
}
defer rows.Close()
result := [][]any{}
for rows.Next() {
var uuid, name, user string
if err := rows.Scan(&uuid, &name, &user); err != nil {
return nil, err
}
result = append(result, []any{uuid, name, user})
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
// endregion
// region: Todo Namespace
func (d *Sqlite3Database) TodoGetFull(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM todo WHERE [belong_to] = ?;", username)
if err != nil {
return nil, err
}
defer rows.Close()
result := [][]any{}
for rows.Next() {
var uuid, belongTo, data, lastChange string
if err := rows.Scan(&uuid, &belongTo, &data, &lastChange); err != nil {
return nil, err
}
result = append(result, []any{uuid, belongTo, data, lastChange})
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
func (d *Sqlite3Database) TodoGetList(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT [uuid] FROM todo WHERE [belong_to] = ?;", username)
if err != nil {
return nil, err
}
defer rows.Close()
result := []string{}
for rows.Next() {
var uuid string
if err := rows.Scan(&uuid); err != nil {
return nil, err
}
result = append(result, uuid)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
func (d *Sqlite3Database) TodoGetDetail(ctx context.Context, token, uuid string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
var dUuid, dBelongTo, dData, dLastChange string
err = tx.QueryRowContext(ctx, "SELECT * FROM todo WHERE [belong_to] = ? AND [uuid] = ?;", username, uuid).Scan(&dUuid, &dBelongTo, &dData, &dLastChange)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return []any{dUuid, dBelongTo, dData, dLastChange}, nil
})
}
func (d *Sqlite3Database) TodoAdd(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
newUuid := utils.GenerateUUID()
newLastUpdate := utils.GenerateUUID()
returnedData := []any{newUuid, username, "", newLastUpdate}
if _, err := tx.ExecContext(ctx, "INSERT INTO todo VALUES (?, ?, ?, ?);", newUuid, username, "", newLastUpdate); err != nil {
return nil, err
}
return returnedData, nil
})
}
func (d *Sqlite3Database) TodoUpdate(ctx context.Context, token, uuid, data, lastChange string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
// check valid token
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
// update
newLastChange := utils.GenerateUUID()
res, err := tx.ExecContext(ctx, "UPDATE todo SET [data] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;", data, newLastChange, uuid, lastChange)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to update due to no matched rows or too much rows.")
}
return newLastChange, nil
})
}
func (d *Sqlite3Database) TodoDelete(ctx context.Context, token, uuid, lastChange string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
// check valid token
if err := tokenOperCheckValid(ctx, tx, token); err != nil {
return nil, err
}
// delete
res, err := tx.ExecContext(ctx, "DELETE FROM todo WHERE [uuid] = ? AND [last_change] = ?;", uuid, lastChange)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to delete due to no matched rows or too much rows.")
}
return true, nil
})
}
// endregion
// region: Admin Namespace
func (d *Sqlite3Database) AdminGet(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
isAdmin, err := tokenOperIsAdmin(ctx, tx, username)
if err != nil {
return nil, err
}
if !isAdmin {
return nil, fmt.Errorf("Permission denied.")
}
rows, err := tx.QueryContext(ctx, "SELECT [name], [is_admin] FROM user;")
if err != nil {
return nil, err
}
defer rows.Close()
result := [][]any{}
for rows.Next() {
var name string
var isAdminVal int64
if err := rows.Scan(&name, &isAdminVal); err != nil {
return nil, err
}
result = append(result, []any{name, isAdminVal == 1})
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
func (d *Sqlite3Database) AdminAdd(ctx context.Context, token, username string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
caller, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
isAdmin, err := tokenOperIsAdmin(ctx, tx, caller)
if err != nil {
return nil, err
}
if !isAdmin {
return nil, fmt.Errorf("Permission denied.")
}
newPassword := utils.ComputePasswordHash(utils.GenerateUUID())
if _, err := tx.ExecContext(ctx, "INSERT INTO user VALUES (?, ?, ?, ?);", username, newPassword, 0, utils.GenerateSalt()); err != nil {
return nil, err
}
return []any{username, false}, nil
})
}
func (d *Sqlite3Database) AdminUpdate(ctx context.Context, token, username string, opts AdminUpdateOptions) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
caller, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
isAdmin, err := tokenOperIsAdmin(ctx, tx, caller)
if err != nil {
return nil, err
}
if !isAdmin {
return nil, fmt.Errorf("Permission denied.")
}
// construct data
sqlList := []string{}
args := []any{}
// analyse opt arg
if opts.Password != nil {
sqlList = append(sqlList, "[password] = ?")
args = append(args, utils.ComputePasswordHash(*opts.Password))
}
if opts.IsAdmin != nil {
sqlList = append(sqlList, "[is_admin] = ?")
if *opts.IsAdmin {
args = append(args, 1)
} else {
args = append(args, 0)
}
}
if len(sqlList) == 0 {
return nil, fmt.Errorf("No fields to update.")
}
// execute
args = append(args, username)
d.deps.Logger.Debug("admin update", "args", args)
query := "UPDATE user SET " + strings.Join(sqlList, ", ") + " WHERE [name] = ?;"
res, err := tx.ExecContext(ctx, query, args...)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to update due to no matched rows or too much rows.")
}
return true, nil
})
}
func (d *Sqlite3Database) AdminDelete(ctx context.Context, token, username string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
caller, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
isAdmin, err := tokenOperIsAdmin(ctx, tx, caller)
if err != nil {
return nil, err
}
if !isAdmin {
return nil, fmt.Errorf("Permission denied.")
}
// delete
res, err := tx.ExecContext(ctx, "DELETE FROM user WHERE [name] = ?;", username)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to delete due to no matched rows or too much rows.")
}
return true, nil
})
}
// region: Profile Namespace
func (d *Sqlite3Database) ProfileIsAdmin(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
return tokenOperIsAdmin(ctx, tx, username)
})
}
func (d *Sqlite3Database) ProfileChangePassword(ctx context.Context, token, password string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
if _, err := tx.ExecContext(ctx, "UPDATE user SET [password] = ? WHERE [name] = ?;", utils.ComputePasswordHash(password), username); err != nil {
return nil, err
}
return true, nil
})
}
func (d *Sqlite3Database) ProfileGetToken(ctx context.Context, token string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM token WHERE [user] = ?;", username)
if err != nil {
return nil, err
}
defer rows.Close()
result := [][]any{}
for rows.Next() {
var user, tokenVal, ua, ip string
var expireOn int64
if err := rows.Scan(&user, &tokenVal, &expireOn, &ua, &ip); err != nil {
return nil, err
}
result = append(result, []any{user, tokenVal, expireOn, ua, ip})
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
})
}
func (d *Sqlite3Database) ProfileDeleteToken(ctx context.Context, token, deleteToken string) ResponseBody {
return d.safeOp(ctx, func(ctx context.Context, tx *sql.Tx) (any, error) {
username, err := tokenOperGetUsername(ctx, tx, token)
if err != nil {
return nil, err
}
// delete
res, err := tx.ExecContext(ctx, "DELETE FROM token WHERE [user] = ? AND [token] = ?;", username, deleteToken)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n != 1 {
return nil, fmt.Errorf("Fail to delete due to no matched rows or too much rows.")
}
return true, nil
})
}
// endregion