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) (any, error) { 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 nil, err } 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 nil, err } } data, err := fn(ctx, tx) if err != nil { tx.Rollback() if debug { logger.Error("database operation failed", "error", err) } return nil, err } if err := tx.Commit(); err != nil { if debug { logger.Error("failed to commit transaction", "error", err) } return nil, err } return data, nil } // 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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) (any, error) { 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