Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow per-user settings and store them in database #639

Merged
merged 68 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
3d5778f
create needed field in sqlite database
Monirzadeh Jul 9, 2023
c574a7e
update account model
Monirzadeh Jul 10, 2023
53314e5
update Account struct for save Account options
Monirzadeh Jul 10, 2023
ab0b922
update sqlite database return account settings
Monirzadeh Jul 10, 2023
c6fdddc
save configure in sqlite as text and return that
Monirzadeh Jul 16, 2023
8d68fb5
read configure from user account and defualt configure for shiori
Monirzadeh Jul 16, 2023
f32dfd8
add api/ui for update settings in database user can save settings in …
Monirzadeh Jul 16, 2023
c106749
check configures be in json format before save in database
Monirzadeh Jul 17, 2023
440a125
support MariaDB
Monirzadeh Jul 17, 2023
b872736
fix wrong comment
Monirzadeh Jul 17, 2023
e950ff4
support PostgreSQL
Monirzadeh Jul 17, 2023
fcaa29b
revert unneeded change in new logic
Monirzadeh Jul 17, 2023
a77fe7e
change configures to config
Monirzadeh Jul 18, 2023
3c2a961
change SaveAccount to SaveAccountSettings
Monirzadeh Jul 18, 2023
741af23
add migrate database scripts
Monirzadeh Jul 18, 2023
353b10b
change default in migration scrtipts
Monirzadeh Jul 19, 2023
dbe0773
update model
Monirzadeh Jul 19, 2023
e7ca00a
read config field as json from database
Monirzadeh Jul 22, 2023
17854a2
fix parse value config value & update config update
Monirzadeh Jul 22, 2023
e215a58
update default value for new user
Monirzadeh Jul 22, 2023
8e8ab72
update settings variable name to reflect database value in UI
Monirzadeh Jul 22, 2023
f0fd7a9
fix typo
Monirzadeh Jul 22, 2023
e880efb
not panic if user not exist and update worng comment
Monirzadeh Jul 22, 2023
463684e
visitor user can update there settings now
Monirzadeh Jul 22, 2023
d4a2449
remove unneeded loading dialog
Monirzadeh Jul 22, 2023
b17a3b3
fix typo
Monirzadeh Jul 22, 2023
75777dd
update function for pg and mysql
Monirzadeh Jul 22, 2023
3e0519e
remove IsJson
Monirzadeh Jul 22, 2023
7a37dcb
move scan method to model
Monirzadeh Jul 22, 2023
5a9bb0b
simplify jsonify
Monirzadeh Jul 22, 2023
039be53
simplify assignees value to account.Config
Monirzadeh Jul 22, 2023
1398279
missing part of function
Monirzadeh Jul 22, 2023
0f4df09
fix some typo and unneeded field in struct
Monirzadeh Jul 23, 2023
c70dd9d
add down migrate script for all database
Monirzadeh Jul 23, 2023
0121f62
change createEbook to CreateEbook
Monirzadeh Jul 23, 2023
e4ec3f2
use json instead of text in mysql and postgres
Monirzadeh Jul 23, 2023
006f52c
implement
Monirzadeh Jul 23, 2023
80a84c3
remove unneeded part
Monirzadeh Jul 23, 2023
26f7232
remove unneeded jsonify in code
Monirzadeh Jul 23, 2023
30f75b3
return SelectContext and GetContext
Monirzadeh Jul 23, 2023
7eba991
remove defualt config in reques for new user it will be set in backend
Monirzadeh Jul 23, 2023
04299a0
merge with master and resolved conflict
Monirzadeh Jul 24, 2023
a06e8a2
New API
Monirzadeh Jul 24, 2023
c108f25
remove legacy API
Monirzadeh Jul 24, 2023
ec72e75
remove validateSessionWithoutOwnerStatus
Monirzadeh Jul 24, 2023
465abde
remove Jsonify function don't need that anymore
Monirzadeh Jul 24, 2023
f6d06b7
add unit test for database
Monirzadeh Jul 24, 2023
567a376
update migrate script name
Monirzadeh Jul 25, 2023
84ab1fc
change put to patch
Monirzadeh Jul 25, 2023
6bdb106
return PUT
Monirzadeh Jul 25, 2023
93d4cd7
fix Patch problem and now use PATCH instead of PUT
Monirzadeh Jul 25, 2023
cb186e7
remove unneeded retuen
Monirzadeh Jul 25, 2023
9c57905
more cleaner code for request new settings
Monirzadeh Jul 26, 2023
34c7426
fix bug to handle string in Scan method thanks to fmartingr
Monirzadeh Jul 26, 2023
5043acb
fix Authorization & use GetAccount & remove username from request
Monirzadeh Aug 1, 2023
0b611ad
shiori-settings remove and it read from shiori-account
Monirzadeh Aug 1, 2023
f9c60e2
add swagger documentation
Monirzadeh Aug 1, 2023
bce2321
API unit test
Monirzadeh Aug 1, 2023
3f60ad5
fix typo
Monirzadeh Aug 1, 2023
f9c91be
remove unneeded coment
Monirzadeh Aug 12, 2023
9f62b1e
better Documentation
Monirzadeh Aug 12, 2023
f6314ae
shiori-toke remove on logout
Monirzadeh Aug 12, 2023
f256392
fix typo
Monirzadeh Aug 12, 2023
8f564cd
add unit test check update config in database
Monirzadeh Aug 14, 2023
4a3b07b
update swag documentation
Monirzadeh Aug 14, 2023
c2572b5
Merge branch 'master' into settings-per-user
Monirzadeh Aug 14, 2023
aa1b558
fix swag formaing error
Monirzadeh Aug 14, 2023
f268858
Merge branch 'master' into settings-per-user
Monirzadeh Oct 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package database
import (
"context"
"embed"
"encoding/json"
"log"

"github.com/go-shiori/shiori/internal/model"
Expand Down Expand Up @@ -39,8 +40,16 @@ type GetBookmarksOptions struct {

// GetAccountsOptions is options for fetching accounts from database.
type GetAccountsOptions struct {
Keyword string
Owner bool
Keyword string
Owner bool
Showid bool
Monirzadeh marked this conversation as resolved.
Show resolved Hide resolved
Listmode bool
Hidethumbnail bool
Hideexcerpt bool
Nightmode bool
Keepmetadata bool
Usearchive bool
Makepublic bool
}

// DB is interface for accessing and manipulating data in database.
Expand All @@ -66,6 +75,9 @@ type DB interface {
// SaveAccount saves new account in database
SaveAccount(ctx context.Context, a model.Account) error

// SaveSettings saves settings for specific user in database
SaveSettings(ctx context.Context, a model.Account) error
fmartingr marked this conversation as resolved.
Show resolved Hide resolved

// GetAccounts fetch list of account (without its password) with matching keyword.
GetAccounts(ctx context.Context, opts GetAccountsOptions) ([]model.Account, error)

Expand Down Expand Up @@ -108,3 +120,14 @@ func (db *dbbase) withTx(ctx context.Context, fn func(tx *sqlx.Tx) error) error

return err
}

// get a string and return errors if it is not in json format
func IsJson(input string) error {
Monirzadeh marked this conversation as resolved.
Show resolved Hide resolved
var jsonData interface{}
err := json.Unmarshal([]byte(input), &jsonData)
if err != nil {
return err
} else {
return nil
}
}
9 changes: 5 additions & 4 deletions internal/database/migrations/mysql/0001_initial.up.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
CREATE TABLE IF NOT EXISTS account(
id INT(11) NOT NULL AUTO_INCREMENT,
username VARCHAR(250) NOT NULL,
password BINARY(80) NOT NULL,
owner TINYINT(1) NOT NULL DEFAULT '0',
id INT(11) NOT NULL AUTO_INCREMENT,
username VARCHAR(250) NOT NULL,
password BINARY(80) NOT NULL,
owner TINYINT(1) NOT NULL DEFAULT '0',
configures VARCHAR(500) NOT NULL,
Monirzadeh marked this conversation as resolved.
Show resolved Hide resolved
PRIMARY KEY (id),
UNIQUE KEY account_username_UNIQUE (username))
CHARACTER SET utf8mb4;
3 changes: 2 additions & 1 deletion internal/database/migrations/postgres/0001_initial.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS account(
username VARCHAR(250) NOT NULL,
password BYTEA NOT NULL,
owner BOOLEAN NOT NULL DEFAULT FALSE,
configures VARCHAR(500) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT account_username_UNIQUE UNIQUE (username));

Expand Down Expand Up @@ -33,4 +34,4 @@ CREATE TABLE IF NOT EXISTS bookmark_tag(
CONSTRAINT bookmark_tag_tag_id_FK FOREIGN KEY (tag_id) REFERENCES tag (id));

CREATE INDEX IF NOT EXISTS bookmark_tag_bookmark_id_FK ON bookmark_tag (bookmark_id);
CREATE INDEX IF NOT EXISTS bookmark_tag_tag_id_FK ON bookmark_tag (tag_id);
CREATE INDEX IF NOT EXISTS bookmark_tag_tag_id_FK ON bookmark_tag (tag_id);
1 change: 1 addition & 0 deletions internal/database/migrations/sqlite/0001_initial.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS account(
username TEXT NOT NULL,
password TEXT NOT NULL,
owner INTEGER NOT NULL DEFAULT 0,
configures TEXT NOT NULL,
CONSTRAINT account_PK PRIMARY KEY(id),
CONSTRAINT account_username_UNIQUE UNIQUE(username)
);
Expand Down
23 changes: 19 additions & 4 deletions internal/database/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,11 +532,26 @@ func (db *MySQLDatabase) SaveAccount(ctx context.Context, account model.Account)

// Insert account to database
_, err = db.ExecContext(ctx, `INSERT INTO account
(username, password, owner) VALUES (?, ?, ?)
(username, password, owner, configures) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
password = VALUES(password),
owner = VALUES(owner)`,
account.Username, hashedPassword, account.Owner)
account.Username, hashedPassword, account.Owner, account.Configures)

return errors.WithStack(err)
}

// SaveSettings update settings for specific account in database. Returns error if any happened
func (db *MySQLDatabase) SaveSettings(ctx context.Context, account model.Account) (err error) {
err = IsJson(account.Configures)
if err != nil {
return errors.WithStack(err)
}
// Update account configures in database for specific user
_, err = db.ExecContext(ctx, `UPDATE account
SET configures = ?
WHERE username = ?`,
account.Configures, account.Username)

return errors.WithStack(err)
}
Expand All @@ -545,7 +560,7 @@ func (db *MySQLDatabase) SaveAccount(ctx context.Context, account model.Account)
func (db *MySQLDatabase) GetAccounts(ctx context.Context, opts GetAccountsOptions) ([]model.Account, error) {
// Create query
args := []interface{}{}
query := `SELECT id, username, owner FROM account WHERE 1`
query := `SELECT id, username, owner, configures FROM account WHERE 1`

if opts.Keyword != "" {
query += " AND username LIKE ?"
Expand Down Expand Up @@ -573,7 +588,7 @@ func (db *MySQLDatabase) GetAccounts(ctx context.Context, opts GetAccountsOption
func (db *MySQLDatabase) GetAccount(ctx context.Context, username string) (model.Account, bool, error) {
account := model.Account{}
if err := db.GetContext(ctx, &account, `SELECT
id, username, password, owner FROM account WHERE username = ?`,
id, username, password, owner, configures FROM account WHERE username = ?`,
username,
); err != nil {
return account, false, errors.WithStack(err)
Expand Down
24 changes: 20 additions & 4 deletions internal/database/pg.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,11 +542,27 @@ func (db *PGDatabase) SaveAccount(ctx context.Context, account model.Account) (e

// Insert account to database
_, err = db.ExecContext(ctx, `INSERT INTO account
(username, password, owner) VALUES ($1, $2, $3)
(username, password, owner, configures) VALUES ($1, $2, $3, $4)
ON CONFLICT(username) DO UPDATE SET
password = $2,
owner = $3`,
account.Username, hashedPassword, account.Owner)
account.Username, hashedPassword, account.Owner, account.Configures)

return errors.WithStack(err)
}

// SaveSettings update settings for specific account in database. Returns error if any happened
func (db *PGDatabase) SaveSettings(ctx context.Context, account model.Account) (err error) {
err = IsJson(account.Configures)
if err != nil {
return errors.WithStack(err)
}

// Insert account to database
_, err = db.ExecContext(ctx, `UPDATE account
SET configures = $1
WHERE username = $2`,
account.Configures, account.Username)

return errors.WithStack(err)
}
Expand All @@ -555,7 +571,7 @@ func (db *PGDatabase) SaveAccount(ctx context.Context, account model.Account) (e
func (db *PGDatabase) GetAccounts(ctx context.Context, opts GetAccountsOptions) ([]model.Account, error) {
// Create query
args := []interface{}{}
query := `SELECT id, username, owner FROM account WHERE TRUE`
query := `SELECT id, username, owner, configures FROM account WHERE TRUE`

if opts.Keyword != "" {
query += " AND username LIKE $1"
Expand Down Expand Up @@ -583,7 +599,7 @@ func (db *PGDatabase) GetAccounts(ctx context.Context, opts GetAccountsOptions)
func (db *PGDatabase) GetAccount(ctx context.Context, username string) (model.Account, bool, error) {
account := model.Account{}
if err := db.GetContext(ctx, &account, `SELECT
id, username, password, owner FROM account WHERE username = $1`,
id, username, password, owner, configures FROM account WHERE username = $1`,
username,
); err != nil {
return account, false, errors.WithStack(err)
Expand Down
31 changes: 26 additions & 5 deletions internal/database/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,11 +649,32 @@ func (db *SQLiteDatabase) SaveAccount(ctx context.Context, account model.Account

// Insert account to database
_, err = tx.Exec(`INSERT INTO account
(username, password, owner) VALUES (?, ?, ?)
(username, password, owner, configures) VALUES (?, ?, ?, ?)
ON CONFLICT(username) DO UPDATE SET
password = ?, owner = ?`,
account.Username, hashedPassword, account.Owner,
hashedPassword, account.Owner)
account.Username, hashedPassword, account.Owner, account.Configures,
hashedPassword, account.Owner, account.Configures)
return errors.WithStack(err)
}); err != nil {
return errors.WithStack(err)
}

return nil
}

// SaveSettings update settings for specific account in database. Returns error if any happened.
func (db *SQLiteDatabase) SaveSettings(ctx context.Context, account model.Account) error {
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
err := IsJson(account.Configures)
if err != nil {
return err
}

// Update account configures in database for specific user
_, err = tx.Exec(`UPDATE account
SET configures = ?
WHERE username = ?`,
account.Configures, account.Username)
return errors.WithStack(err)
}); err != nil {
return errors.WithStack(err)
Expand All @@ -666,7 +687,7 @@ func (db *SQLiteDatabase) SaveAccount(ctx context.Context, account model.Account
func (db *SQLiteDatabase) GetAccounts(ctx context.Context, opts GetAccountsOptions) ([]model.Account, error) {
// Create query
args := []interface{}{}
query := `SELECT id, username, owner FROM account WHERE 1`
query := `SELECT id, username, owner, configures FROM account WHERE 1`

if opts.Keyword != "" {
query += " AND username LIKE ?"
Expand Down Expand Up @@ -694,7 +715,7 @@ func (db *SQLiteDatabase) GetAccounts(ctx context.Context, opts GetAccountsOptio
func (db *SQLiteDatabase) GetAccount(ctx context.Context, username string) (model.Account, bool, error) {
account := model.Account{}
if err := db.GetContext(ctx, &account, `SELECT
id, username, password, owner FROM account WHERE username = ?`,
id, username, password, owner, configures FROM account WHERE username = ?`,
username,
); err != nil {
return account, false, errors.WithStack(err)
Expand Down
9 changes: 5 additions & 4 deletions internal/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ type Bookmark struct {

// Account is person that allowed to access web interface.
type Account struct {
ID int `db:"id" json:"id"`
Username string `db:"username" json:"username"`
Password string `db:"password" json:"password,omitempty"`
Owner bool `db:"owner" json:"owner"`
ID int `db:"id" json:"id"`
Username string `db:"username" json:"username"`
Password string `db:"password" json:"password,omitempty"`
Owner bool `db:"owner" json:"owner"`
Configures string `db:"configures" json:"configures"`
}
3 changes: 2 additions & 1 deletion internal/view/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
return response;
}).then(() => {
localStorage.removeItem("shiori-account");
localStorage.removeItem("shiori-setting");
document.cookie = `session-id=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
location.href = new URL("login", document.baseURI);
}).catch(err => {
Expand Down Expand Up @@ -173,4 +174,4 @@
</script>
</body>

</html>
</html>
58 changes: 47 additions & 11 deletions internal/view/js/page/setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,40 @@ export default {
saveSetting() {
this.$emit("setting-changed", {
showId: this.appOptions.showId,
listMode: this.appOptions.listMode,
hideThumbnail: this.appOptions.hideThumbnail,
hideExcerpt: this.appOptions.hideExcerpt,
nightMode: this.appOptions.nightMode,
keepMetadata: this.appOptions.keepMetadata,
useArchive: this.appOptions.useArchive,
makePublic: this.appOptions.makePublic,
});
},
loadAccounts() {
listMode: this.appOptions.listMode,
hideThumbnail: this.appOptions.hideThumbnail,
hideExcerpt: this.appOptions.hideExcerpt,
nightMode: this.appOptions.nightMode,
keepMetadata: this.appOptions.keepMetadata,
useArchive: this.appOptions.useArchive,
makePublic: this.appOptions.makePublic,
});
const request = {
username: this.activeAccount.username,
configures: JSON.stringify(this.appOptions)
};
// TODO: DO i need loading page? if no remove this.dialog.loading = false
fetch(new URL("api/accountssettings", document.baseURI), {
method: "put",
body: JSON.stringify(request),
headers: {
"Content-Type": "application/json",
},
}).then(response => {
if (!response.ok) throw response;
return response;
}).then(() => {
this.dialog.loading = false;
this.dialog.visible = false;
}).catch(err => {
this.dialog.loading = false;
this.getErrorMessage(err).then(msg => {
this.showErrorDialog(msg);
})
});

},
loadAccounts() {
if (this.loading) return;

this.loading = true;
Expand Down Expand Up @@ -155,11 +179,23 @@ export default {
this.showErrorDialog("Password does not match");
return;
}
const defaultconfigures = {
showId: false,
listMode: false,
hideThumbnail: false,
hideExcerpt: false,
nightMode: false,
keepMetadata: false,
useArchive: false,
makePublic: false
};
data.configures = JSON.stringify(defaultconfigures);

var request = {
username: data.username,
password: data.password,
owner: !data.visitor,
configures: data.configures,
}

this.dialog.loading = true;
Expand Down Expand Up @@ -301,4 +337,4 @@ export default {
mounted() {
this.loadAccounts();
}
}
}
2 changes: 2 additions & 0 deletions internal/view/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@

// Save account data
localStorage.setItem("shiori-account", JSON.stringify(json.account));
const configures = JSON.parse(json.account.configures);
localStorage.setItem("shiori-setting", JSON.stringify(configures));

// Go to destination page
var currentUrl = new Url,
Expand Down
Loading