1139 lines
35 KiB
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
|