mirror of
https://github.com/go-shiori/shiori.git
synced 2025-09-08 22:15:58 +08:00
Add basic account level management
This commit is contained in:
parent
326f04d19e
commit
0c4d75f773
18 changed files with 177 additions and 117 deletions
|
@ -48,7 +48,7 @@ type DB interface {
|
|||
GetBookmark(id int, url string) (model.Bookmark, bool)
|
||||
|
||||
// SaveAccount saves new account in database
|
||||
SaveAccount(username, password string) error
|
||||
SaveAccount(model.Account) error
|
||||
|
||||
// GetAccounts fetch list of account (without its password) with matching keyword.
|
||||
GetAccounts(keyword string) ([]model.Account, error)
|
||||
|
|
|
@ -43,9 +43,10 @@ func OpenMySQLDatabase(username, password, dbName string) (mysqlDB *MySQLDatabas
|
|||
|
||||
// Create tables
|
||||
tx.MustExec(`CREATE TABLE IF NOT EXISTS account(
|
||||
id INT(11) NOT NULL,
|
||||
username VARCHAR(250) NOT NULL,
|
||||
password BINARY(80) NOT NULL,
|
||||
id INT(11) NOT NULL,
|
||||
username VARCHAR(250) NOT NULL,
|
||||
password BINARY(80) NOT NULL,
|
||||
owner TINYINT(1) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY account_username_UNIQUE (username))`)
|
||||
|
||||
|
@ -509,19 +510,20 @@ func (db *MySQLDatabase) GetBookmark(id int, url string) (model.Bookmark, bool)
|
|||
}
|
||||
|
||||
// SaveAccount saves new account to database. Returns error if any happened.
|
||||
func (db *MySQLDatabase) SaveAccount(username, password string) (err error) {
|
||||
func (db *MySQLDatabase) SaveAccount(account model.Account) (err error) {
|
||||
// Hash password with bcrypt
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(account.Password), 10)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert account to database
|
||||
_, err = db.Exec(`INSERT INTO account
|
||||
(username, password) VALUES (?, ?)
|
||||
(username, password, owner) VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
password = VALUES(password)`,
|
||||
username, hashedPassword)
|
||||
password = VALUES(password),
|
||||
owner = VALUES(owner)`,
|
||||
account.Username, hashedPassword, account.Owner)
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -530,7 +532,7 @@ func (db *MySQLDatabase) SaveAccount(username, password string) (err error) {
|
|||
func (db *MySQLDatabase) GetAccounts(keyword string) ([]model.Account, error) {
|
||||
// Create query
|
||||
args := []interface{}{}
|
||||
query := `SELECT id, username FROM account WHERE 1`
|
||||
query := `SELECT id, username, owner FROM account WHERE 1`
|
||||
|
||||
if keyword != "" {
|
||||
query += " AND username LIKE ?"
|
||||
|
@ -554,7 +556,7 @@ func (db *MySQLDatabase) GetAccounts(keyword string) ([]model.Account, error) {
|
|||
func (db *MySQLDatabase) GetAccount(username string) (model.Account, bool) {
|
||||
account := model.Account{}
|
||||
db.Get(&account, `SELECT
|
||||
id, username, password FROM account WHERE username = ?`,
|
||||
id, username, password, owner FROM account WHERE username = ?`,
|
||||
username)
|
||||
|
||||
return account, account.ID != 0
|
||||
|
|
|
@ -43,6 +43,7 @@ func OpenSQLiteDatabase(databasePath string) (sqliteDB *SQLiteDatabase, err erro
|
|||
id INTEGER NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
owner INTEGER NOT NULL DEFAULT 0,
|
||||
CONSTRAINT account_PK PRIMARY KEY(id),
|
||||
CONSTRAINT account_username_UNIQUE UNIQUE(username))`)
|
||||
|
||||
|
@ -521,19 +522,20 @@ func (db *SQLiteDatabase) GetBookmark(id int, url string) (model.Bookmark, bool)
|
|||
}
|
||||
|
||||
// SaveAccount saves new account to database. Returns error if any happened.
|
||||
func (db *SQLiteDatabase) SaveAccount(username, password string) (err error) {
|
||||
func (db *SQLiteDatabase) SaveAccount(account model.Account) (err error) {
|
||||
// Hash password with bcrypt
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(account.Password), 10)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert account to database
|
||||
_, err = db.Exec(`INSERT INTO account
|
||||
(username, password) VALUES (?, ?)
|
||||
(username, password, owner) VALUES (?, ?, ?)
|
||||
ON CONFLICT(username) DO UPDATE SET
|
||||
password = ?`,
|
||||
username, hashedPassword, hashedPassword)
|
||||
password = ?, owner = ?`,
|
||||
account.Username, hashedPassword, account.Owner,
|
||||
hashedPassword, account.Owner)
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -542,7 +544,7 @@ func (db *SQLiteDatabase) SaveAccount(username, password string) (err error) {
|
|||
func (db *SQLiteDatabase) GetAccounts(keyword string) ([]model.Account, error) {
|
||||
// Create query
|
||||
args := []interface{}{}
|
||||
query := `SELECT id, username FROM account WHERE 1`
|
||||
query := `SELECT id, username, owner FROM account WHERE 1`
|
||||
|
||||
if keyword != "" {
|
||||
query += " AND username LIKE ?"
|
||||
|
@ -566,7 +568,7 @@ func (db *SQLiteDatabase) GetAccounts(keyword string) ([]model.Account, error) {
|
|||
func (db *SQLiteDatabase) GetAccount(username string) (model.Account, bool) {
|
||||
account := model.Account{}
|
||||
db.Get(&account, `SELECT
|
||||
id, username, password FROM account WHERE username = ?`,
|
||||
id, username, password, owner FROM account WHERE username = ?`,
|
||||
username)
|
||||
|
||||
return account, account.ID != 0
|
||||
|
|
|
@ -31,11 +31,5 @@ type Account struct {
|
|||
ID int `db:"id" json:"id"`
|
||||
Username string `db:"username" json:"username"`
|
||||
Password string `db:"password" json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// LoginRequest is request from user to access web interface.
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Remember int `json:"remember"`
|
||||
Owner bool `db:"owner" json:"owner"`
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
</head>
|
||||
|
||||
<body class="night">
|
||||
<div id="content-scene" :class="{night: displayOptions.nightMode}">
|
||||
<div id="content-scene" :class="{night: appOptions.nightMode}">
|
||||
<div id="header">
|
||||
<p id="metadata" v-cloak>Added {{localtime()}}</p>
|
||||
<p id="title">$$.Title$$</p>
|
||||
|
@ -61,7 +61,7 @@
|
|||
nightMode = (typeof opts.nightMode === "boolean") ? opts.nightMode : false,
|
||||
useArchive = (typeof opts.useArchive === "boolean") ? opts.useArchive : false;
|
||||
|
||||
this.displayOptions = {
|
||||
this.appOptions = {
|
||||
showId: showId,
|
||||
listMode: listMode,
|
||||
nightMode: nightMode,
|
||||
|
|
|
@ -1 +1 @@
|
|||
:root{--dialogHeaderBg:#292929;--colorDialogHeader:#FFF}.custom-dialog-overlay{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;min-width:0;min-height:0;overflow:hidden;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10001;background-color:rgba(0,0,0,0.6);padding:32px}.custom-dialog-overlay .custom-dialog{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;width:100%;max-width:400px;min-height:0;max-height:100%;overflow:auto;background-color:var(--contentBg);font-size:16px}.custom-dialog-overlay .custom-dialog .custom-dialog-header{padding:16px;color:var(--colorDialogHeader);background-color:var(--dialogHeaderBg);font-weight:600;font-size:1em;text-transform:uppercase;border-bottom:1px solid var(--border)}.custom-dialog-overlay .custom-dialog .custom-dialog-body{padding:16px 16px 0;display:grid;max-height:100%;min-height:80px;min-width:0;overflow:auto;font-size:1em;grid-template-columns:max-content 1fr;-webkit-box-align:baseline;align-items:baseline;grid-gap:16px}.custom-dialog-overlay .custom-dialog .custom-dialog-body::after{content:"";display:block;min-height:1px;grid-column-end:-1;grid-column-start:1}.custom-dialog-overlay .custom-dialog .custom-dialog-body .custom-dialog-content{grid-column-end:-1;grid-column-start:1;color:var(--color);align-self:baseline}.custom-dialog-overlay .custom-dialog .custom-dialog-body>label{color:var(--color);padding:8px 0;font-size:1em}.custom-dialog-overlay .custom-dialog .custom-dialog-body>input[type="text"],.custom-dialog-overlay .custom-dialog .custom-dialog-body>textarea{color:var(--color);padding:8px;font-size:1em;border:1px solid var(--border);background-color:var(--contentBg);min-width:0}.custom-dialog-overlay .custom-dialog .custom-dialog-body .checkbox-field{color:var(--color);font-size:1em;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;padding:0;grid-column-start:1;grid-column-end:-1;cursor:pointer}.custom-dialog-overlay .custom-dialog .custom-dialog-body .checkbox-field:hover,.custom-dialog-overlay .custom-dialog .custom-dialog-body .checkbox-field:focus{text-decoration:underline;-webkit-text-decoration-color:var(--main);text-decoration-color:var(--main)}.custom-dialog-overlay .custom-dialog .custom-dialog-body .checkbox-field>input[type="checkbox"]{margin-right:8px}.custom-dialog-overlay .custom-dialog .custom-dialog-body>textarea{height:6em;min-height:37px;resize:vertical}.custom-dialog-overlay .custom-dialog .custom-dialog-body>.suggestion{position:absolute;display:block;padding:8px;background-color:var(--contentBg);border:1px solid var(--border);color:var(--color);font-size:.9em}.custom-dialog-overlay .custom-dialog .custom-dialog-footer{padding:16px;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;-webkit-box-pack:end;justify-content:flex-end;border-top:1px solid var(--border)}.custom-dialog-overlay .custom-dialog .custom-dialog-footer>a{padding:0 8px;font-size:.9em;font-weight:600;color:var(--color);text-transform:uppercase}.custom-dialog-overlay .custom-dialog .custom-dialog-footer>a:hover,.custom-dialog-overlay .custom-dialog .custom-dialog-footer>a:focus{outline:none;color:var(--main)}.custom-dialog-overlay .custom-dialog .custom-dialog-footer>i.fa-spinner.fa-spin{width:19px;line-height:19px;text-align:center;color:var(--color)}
|
||||
:root{--dialogHeaderBg:#292929;--colorDialogHeader:#FFF}.custom-dialog-overlay{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;min-width:0;min-height:0;overflow:hidden;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10001;background-color:rgba(0,0,0,0.6);padding:32px}.custom-dialog-overlay .custom-dialog{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;width:100%;max-width:400px;min-height:0;max-height:100%;overflow:auto;background-color:var(--contentBg);font-size:16px}.custom-dialog-overlay .custom-dialog .custom-dialog-header{padding:16px;color:var(--colorDialogHeader);background-color:var(--dialogHeaderBg);font-weight:600;font-size:1em;text-transform:uppercase;border-bottom:1px solid var(--border)}.custom-dialog-overlay .custom-dialog .custom-dialog-body{padding:16px 16px 0;display:grid;max-height:100%;min-height:80px;min-width:0;overflow:auto;font-size:1em;grid-template-columns:max-content 1fr;-webkit-box-align:baseline;align-items:baseline;grid-gap:16px}.custom-dialog-overlay .custom-dialog .custom-dialog-body::after{content:"";display:block;min-height:1px;grid-column-end:-1;grid-column-start:1}.custom-dialog-overlay .custom-dialog .custom-dialog-body .custom-dialog-content{grid-column-end:-1;grid-column-start:1;color:var(--color);align-self:baseline}.custom-dialog-overlay .custom-dialog .custom-dialog-body>label{color:var(--color);padding:8px 0;font-size:1em}.custom-dialog-overlay .custom-dialog .custom-dialog-body>input[type="text"],.custom-dialog-overlay .custom-dialog .custom-dialog-body>input[type="password"],.custom-dialog-overlay .custom-dialog .custom-dialog-body>textarea{color:var(--color);padding:8px;font-size:1em;border:1px solid var(--border);background-color:var(--contentBg);min-width:0}.custom-dialog-overlay .custom-dialog .custom-dialog-body .checkbox-field{color:var(--color);font-size:1em;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;padding:0;grid-column-start:1;grid-column-end:-1;cursor:pointer}.custom-dialog-overlay .custom-dialog .custom-dialog-body .checkbox-field:hover,.custom-dialog-overlay .custom-dialog .custom-dialog-body .checkbox-field:focus{text-decoration:underline;-webkit-text-decoration-color:var(--main);text-decoration-color:var(--main)}.custom-dialog-overlay .custom-dialog .custom-dialog-body .checkbox-field>input[type="checkbox"]{margin-right:8px}.custom-dialog-overlay .custom-dialog .custom-dialog-body>textarea{height:6em;min-height:37px;resize:vertical}.custom-dialog-overlay .custom-dialog .custom-dialog-body>.suggestion{position:absolute;display:block;padding:8px;background-color:var(--contentBg);border:1px solid var(--border);color:var(--color);font-size:.9em}.custom-dialog-overlay .custom-dialog .custom-dialog-footer{padding:16px;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;-webkit-box-pack:end;justify-content:flex-end;border-top:1px solid var(--border)}.custom-dialog-overlay .custom-dialog .custom-dialog-footer>a{padding:0 8px;font-size:.9em;font-weight:600;color:var(--color);text-transform:uppercase}.custom-dialog-overlay .custom-dialog .custom-dialog-footer>a:hover,.custom-dialog-overlay .custom-dialog .custom-dialog-footer>a:focus{outline:none;color:var(--main)}.custom-dialog-overlay .custom-dialog .custom-dialog-footer>i.fa-spinner.fa-spin{width:19px;line-height:19px;text-align:center;color:var(--color)}
|
File diff suppressed because one or more lines are too long
|
@ -24,7 +24,7 @@
|
|||
</head>
|
||||
|
||||
<body class="night">
|
||||
<div id="main-scene" :class="{night: displayOptions.nightMode}">
|
||||
<div id="main-scene" :class="{night: appOptions.nightMode}">
|
||||
<div id="main-sidebar">
|
||||
<a v-for="item in sidebarItems" :title="item.title" :class="{active: activePage === item.page}" @click="switchPage(item.page)">
|
||||
<i class="fas fa-fw" :class="item.icon"></i>
|
||||
|
@ -35,7 +35,7 @@
|
|||
</a>
|
||||
</div>
|
||||
<keep-alive>
|
||||
<component :is="activePage" :display-options="displayOptions" @setting-changed="saveSetting"></component>
|
||||
<component :is="activePage" :active-account="activeAccount" :app-options="appOptions" @setting-changed="saveSetting"></component>
|
||||
</keep-alive>
|
||||
<custom-dialog v-bind="dialog" />
|
||||
</div>
|
||||
|
@ -88,6 +88,7 @@
|
|||
return response;
|
||||
})
|
||||
.then(() => {
|
||||
localStorage.removeItem("shiori-account");
|
||||
document.cookie = "session-id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
||||
location.href = "/login";
|
||||
})
|
||||
|
@ -102,7 +103,7 @@
|
|||
},
|
||||
saveSetting(opts) {
|
||||
localStorage.setItem("shiori-setting", JSON.stringify(opts));
|
||||
this.displayOptions = opts;
|
||||
this.appOptions = opts;
|
||||
document.body.className = opts.nightMode ? "night" : "";
|
||||
},
|
||||
loadSetting() {
|
||||
|
@ -114,7 +115,7 @@
|
|||
useArchive = (typeof opts.useArchive === "boolean") ? opts.useArchive : false,
|
||||
makePublic = (typeof opts.makePublic === "boolean") ? opts.makePublic : false;
|
||||
|
||||
this.displayOptions = {
|
||||
this.appOptions = {
|
||||
showId: showId,
|
||||
listMode: listMode,
|
||||
nightMode: nightMode,
|
||||
|
@ -124,11 +125,24 @@
|
|||
};
|
||||
|
||||
document.body.className = nightMode ? "night" : "";
|
||||
},
|
||||
loadAccount() {
|
||||
var account = JSON.parse(localStorage.getItem("shiori-account")) || {},
|
||||
id = (typeof account.id === "number") ? account.id : 0,
|
||||
username = (typeof account.username === "string") ? account.username : "",
|
||||
owner = (typeof account.owner === "boolean") ? account.owner : false;
|
||||
|
||||
this.activeAccount = {
|
||||
id: id,
|
||||
username: username,
|
||||
owner: owner,
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Load setting
|
||||
this.loadSetting();
|
||||
this.loadAccount();
|
||||
|
||||
// Prepare history state watcher
|
||||
var stateWatcher = (e) => {
|
||||
|
|
|
@ -22,7 +22,7 @@ var template = `
|
|||
<a class="url" :href="url" target="_blank" rel="noopener">
|
||||
{{hostnameURL}}
|
||||
</a>
|
||||
<template v-if="!editMode">
|
||||
<template v-if="!editMode && menuVisible">
|
||||
<a title="Edit bookmark" @click="editBookmark">
|
||||
<i class="fas fa-fw fa-pencil-alt"></i>
|
||||
</a>
|
||||
|
@ -52,6 +52,7 @@ export default {
|
|||
editMode: Boolean,
|
||||
listMode: Boolean,
|
||||
selected: Boolean,
|
||||
menuVisible: Boolean,
|
||||
tags: {
|
||||
type: Array,
|
||||
default () {
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
export default {
|
||||
props: {
|
||||
displayOptions: {
|
||||
activeAccount: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
id: 0,
|
||||
username: "",
|
||||
owner: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
appOptions: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
|
|
|
@ -5,13 +5,13 @@ var template = `
|
|||
<a title="Refresh storage" @click="reloadData">
|
||||
<i class="fas fa-fw fa-sync-alt" :class="loading && 'fa-spin'"></i>
|
||||
</a>
|
||||
<a title="Add new bookmark" @click="showDialogAdd">
|
||||
<a v-if="activeAccount.owner" title="Add new bookmark" @click="showDialogAdd">
|
||||
<i class="fas fa-fw fa-plus-circle"></i>
|
||||
</a>
|
||||
<a v-if="tags.length > 0" title="Show tags" @click="showDialogTags">
|
||||
<i class="fas fa-fw fa-tags"></i>
|
||||
</a>
|
||||
<a title="Batch edit" @click="toggleEditMode">
|
||||
<a v-if="activeAccount.owner" title="Batch edit" @click="toggleEditMode">
|
||||
<i class="fas fa-fw fa-pencil-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -30,7 +30,7 @@ var template = `
|
|||
<i class="fas fa-fw fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div id="bookmarks-grid" ref="bookmarksGrid" :class="{list: displayOptions.listMode}">
|
||||
<div id="bookmarks-grid" ref="bookmarksGrid" :class="{list: appOptions.listMode}">
|
||||
<pagination-box v-if="maxPage > 1"
|
||||
:page="page"
|
||||
:maxPage="maxPage"
|
||||
|
@ -50,9 +50,10 @@ var template = `
|
|||
:index="index"
|
||||
:key="book.id"
|
||||
:editMode="editMode"
|
||||
:showId="displayOptions.showId"
|
||||
:listMode="displayOptions.listMode"
|
||||
:showId="appOptions.showId"
|
||||
:listMode="appOptions.listMode"
|
||||
:selected="isSelected(book.id)"
|
||||
:menuVisible="activeAccount.owner"
|
||||
@select="toggleSelection"
|
||||
@tag-clicked="bookmarkTagClicked"
|
||||
@edit="showDialogEdit"
|
||||
|
@ -284,12 +285,12 @@ export default {
|
|||
isSelected(bookId) {
|
||||
return this.selection.findIndex(el => el.id === bookId) > -1;
|
||||
},
|
||||
dialogTagClicked(event, idx, tag) {
|
||||
dialogTagClicked(event, tag) {
|
||||
if (!this.dialogTags.editMode) {
|
||||
this.filterTag(tag.name, event.altKey);
|
||||
} else {
|
||||
this.dialogTags.visible = false;
|
||||
this.showDialogRenameTag(idx, tag);
|
||||
this.showDialogRenameTag(tag);
|
||||
}
|
||||
},
|
||||
bookmarkTagClicked(event, tagName) {
|
||||
|
@ -361,12 +362,12 @@ export default {
|
|||
name: "createArchive",
|
||||
label: "Create archive",
|
||||
type: "check",
|
||||
value: this.displayOptions.useArchive,
|
||||
value: this.appOptions.useArchive,
|
||||
}, {
|
||||
name: "makePublic",
|
||||
label: "Make archive publicly available",
|
||||
type: "check",
|
||||
value: this.displayOptions.makePublic,
|
||||
value: this.appOptions.makePublic,
|
||||
}],
|
||||
mainText: "OK",
|
||||
secondText: "Cancel",
|
||||
|
@ -612,12 +613,12 @@ export default {
|
|||
name: "keepMetadata",
|
||||
label: "Keep the old title and excerpt",
|
||||
type: "check",
|
||||
value: this.displayOptions.keepMetadata,
|
||||
value: this.appOptions.keepMetadata,
|
||||
}, {
|
||||
name: "createArchive",
|
||||
label: "Update archive as well",
|
||||
type: "check",
|
||||
value: this.displayOptions.useArchive,
|
||||
value: this.appOptions.useArchive,
|
||||
}],
|
||||
mainText: "Yes",
|
||||
secondText: "No",
|
||||
|
@ -747,10 +748,11 @@ export default {
|
|||
});
|
||||
},
|
||||
showDialogTags() {
|
||||
this.dialogTags.editMode = false;
|
||||
this.dialogTags.visible = true;
|
||||
this.dialogTags.editMode = false;
|
||||
this.dialogTags.secondText = this.activeAccount.owner ? "Rename Tags" : "";
|
||||
},
|
||||
showDialogRenameTag(idx, tag) {
|
||||
showDialogRenameTag(tag) {
|
||||
this.showDialog({
|
||||
title: "Rename Tag",
|
||||
content: `Change the name for tag "#${tag.name}"`,
|
||||
|
|
|
@ -5,43 +5,41 @@ var template = `
|
|||
<details open class="setting-group" id="setting-display">
|
||||
<summary>Display</summary>
|
||||
<label>
|
||||
<input type="checkbox" v-model="displayOptions.showId" @change="saveSetting">
|
||||
<input type="checkbox" v-model="appOptions.showId" @change="saveSetting">
|
||||
Show bookmark's ID
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="displayOptions.listMode" @change="saveSetting">
|
||||
<input type="checkbox" v-model="appOptions.listMode" @change="saveSetting">
|
||||
Display bookmarks as list
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="displayOptions.nightMode" @change="saveSetting">
|
||||
<input type="checkbox" v-model="appOptions.nightMode" @change="saveSetting">
|
||||
Use dark theme
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="displayOptions.useArchive" @change="saveSetting">
|
||||
Create archive by default
|
||||
</label>
|
||||
</details>
|
||||
<details open class="setting-group" id="setting-bookmarks">
|
||||
<details v-if="activeAccount.owner" open class="setting-group" id="setting-bookmarks">
|
||||
<summary>Bookmarks</summary>
|
||||
<label>
|
||||
<input type="checkbox" v-model="displayOptions.keepMetadata" @change="saveSetting">
|
||||
<input type="checkbox" v-model="appOptions.keepMetadata" @change="saveSetting">
|
||||
Keep bookmark's metadata when updating
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="displayOptions.useArchive" @change="saveSetting">
|
||||
<input type="checkbox" v-model="appOptions.useArchive" @change="saveSetting">
|
||||
Create archive by default
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="displayOptions.makePublic" @change="saveSetting">
|
||||
<input type="checkbox" v-model="appOptions.makePublic" @change="saveSetting">
|
||||
Make archive publicly available by default
|
||||
</label>
|
||||
</details>
|
||||
<details open class="setting-group" id="setting-accounts">
|
||||
<details v-if="activeAccount.owner" open class="setting-group" id="setting-accounts">
|
||||
<summary>Accounts</summary>
|
||||
<ul>
|
||||
<li v-if="accounts.length === 0">No accounts registered</li>
|
||||
<li v-for="(account, idx) in accounts">
|
||||
<span>{{account.username}}</span>
|
||||
<p>{{account.username}}
|
||||
<span v-if="account.owner" class="account-level">(owner)</span>
|
||||
</p>
|
||||
<a title="Change password" @click="showDialogChangePassword(account)">
|
||||
<i class="fa fas fa-fw fa-key"></i>
|
||||
</a>
|
||||
|
@ -52,7 +50,7 @@ var template = `
|
|||
</ul>
|
||||
<div class="setting-group-footer">
|
||||
<a @click="loadAccounts">Refresh accounts</a>
|
||||
<a @click="showDialogNewAccount">Add new account</a>
|
||||
<a v-if="activeAccount.owner" @click="showDialogNewAccount">Add new account</a>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
@ -78,12 +76,12 @@ export default {
|
|||
methods: {
|
||||
saveSetting() {
|
||||
this.$emit("setting-changed", {
|
||||
showId: this.displayOptions.showId,
|
||||
listMode: this.displayOptions.listMode,
|
||||
nightMode: this.displayOptions.nightMode,
|
||||
keepMetadata: this.displayOptions.keepMetadata,
|
||||
useArchive: this.displayOptions.useArchive,
|
||||
makePublic: this.displayOptions.makePublic,
|
||||
showId: this.appOptions.showId,
|
||||
listMode: this.appOptions.listMode,
|
||||
nightMode: this.appOptions.nightMode,
|
||||
keepMetadata: this.appOptions.keepMetadata,
|
||||
useArchive: this.appOptions.useArchive,
|
||||
makePublic: this.appOptions.makePublic,
|
||||
});
|
||||
},
|
||||
loadAccounts() {
|
||||
|
@ -124,6 +122,11 @@ export default {
|
|||
label: "Repeat password",
|
||||
type: "password",
|
||||
value: "",
|
||||
}, {
|
||||
name: "visitor",
|
||||
label: "This account is for visitor",
|
||||
type: "check",
|
||||
value: false,
|
||||
}],
|
||||
mainText: "OK",
|
||||
secondText: "Cancel",
|
||||
|
@ -145,7 +148,8 @@ export default {
|
|||
|
||||
var request = {
|
||||
username: data.username,
|
||||
password: data.password
|
||||
password: data.password,
|
||||
owner: !data.visitor,
|
||||
}
|
||||
|
||||
this.dialog.loading = true;
|
||||
|
@ -164,7 +168,7 @@ export default {
|
|||
this.dialog.loading = false;
|
||||
this.dialog.visible = false;
|
||||
|
||||
this.accounts.push({ username: data.username });
|
||||
this.accounts.push({ username: data.username, owner: !data.visitor });
|
||||
this.accounts.sort((a, b) => {
|
||||
var nameA = a.username.toLowerCase(),
|
||||
nameB = b.username.toLowerCase();
|
||||
|
@ -230,7 +234,8 @@ export default {
|
|||
var request = {
|
||||
username: account.username,
|
||||
oldPassword: data.oldPassword,
|
||||
newPassword: data.password
|
||||
newPassword: data.password,
|
||||
owner: account.owner,
|
||||
}
|
||||
|
||||
this.dialog.loading = true;
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
}
|
||||
|
||||
>input[type="text"],
|
||||
>input[type="password"],
|
||||
>textarea {
|
||||
color: var(--color);
|
||||
padding: 8px;
|
||||
|
|
|
@ -758,10 +758,14 @@ a {
|
|||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
span {
|
||||
p {
|
||||
font-size: 1em;
|
||||
color: var(--color);
|
||||
flex: 1 0;
|
||||
|
||||
span {
|
||||
color: var(--colorLink);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
@ -84,9 +84,13 @@
|
|||
})
|
||||
.then(response => {
|
||||
if (!response.ok) throw response;
|
||||
return response;
|
||||
return response.json();
|
||||
})
|
||||
.then(() => {
|
||||
.then(json => {
|
||||
// Save account data
|
||||
localStorage.setItem("shiori-account", JSON.stringify(json));
|
||||
|
||||
// Go to destination page
|
||||
var dst = (new Url).query.dst;
|
||||
location.href = dst || "/";
|
||||
})
|
||||
|
@ -107,6 +111,7 @@
|
|||
mounted() {
|
||||
// Load setting
|
||||
this.loadSetting();
|
||||
localStorage.removeItem("shiori-account");
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -28,19 +28,24 @@ import (
|
|||
// apiLogin is handler for POST /api/login
|
||||
func (h *handler) apiLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
// Decode request
|
||||
var request model.LoginRequest
|
||||
request := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Remember int `json:"remember"`
|
||||
}{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
checkError(err)
|
||||
|
||||
// Prepare function to generate session
|
||||
genSession := func(expTime time.Duration) {
|
||||
genSession := func(ownerMode bool, account model.Account, expTime time.Duration) {
|
||||
// Create session ID
|
||||
sessionID, err := uuid.NewV4()
|
||||
checkError(err)
|
||||
|
||||
// Save session ID to cache
|
||||
strSessionID := sessionID.String()
|
||||
h.SessionCache.Set(strSessionID, request.Username, expTime)
|
||||
h.SessionCache.Set(strSessionID, ownerMode, expTime)
|
||||
|
||||
// Save user's session IDs to cache as well
|
||||
// useful for mass logout
|
||||
|
@ -59,8 +64,12 @@ func (h *handler) apiLogin(w http.ResponseWriter, r *http.Request, ps httprouter
|
|||
Expires: time.Now().Add(expTime),
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprint(w, strSessionID)
|
||||
// Send account data
|
||||
account.Password = ""
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(&account)
|
||||
checkError(err)
|
||||
}
|
||||
|
||||
// Check if user's database is empty.
|
||||
|
@ -69,7 +78,7 @@ func (h *handler) apiLogin(w http.ResponseWriter, r *http.Request, ps httprouter
|
|||
checkError(err)
|
||||
|
||||
if len(accounts) == 0 && request.Username == "shiori" && request.Password == "gopher" {
|
||||
genSession(time.Hour)
|
||||
genSession(true, model.Account{}, time.Hour)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -94,7 +103,7 @@ func (h *handler) apiLogin(w http.ResponseWriter, r *http.Request, ps httprouter
|
|||
}
|
||||
|
||||
// Create session
|
||||
genSession(expTime)
|
||||
genSession(account.Owner, account, expTime)
|
||||
}
|
||||
|
||||
// apiLogout is handler for POST /api/logout
|
||||
|
@ -741,7 +750,7 @@ func (h *handler) apiInsertAccount(w http.ResponseWriter, r *http.Request, ps ht
|
|||
checkError(err)
|
||||
|
||||
// Save account to database
|
||||
err = h.DB.SaveAccount(account.Username, account.Password)
|
||||
err = h.DB.SaveAccount(account)
|
||||
checkError(err)
|
||||
|
||||
fmt.Fprint(w, 1)
|
||||
|
@ -758,6 +767,7 @@ func (h *handler) apiUpdateAccount(w http.ResponseWriter, r *http.Request, ps ht
|
|||
Username string `json:"username"`
|
||||
OldPassword string `json:"oldPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
Owner bool `json:"owner"`
|
||||
}{}
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(&request)
|
||||
|
@ -776,7 +786,9 @@ func (h *handler) apiUpdateAccount(w http.ResponseWriter, r *http.Request, ps ht
|
|||
}
|
||||
|
||||
// Save new password to database
|
||||
err = h.DB.SaveAccount(request.Username, request.NewPassword)
|
||||
account.Password = request.NewPassword
|
||||
account.Owner = request.Owner
|
||||
err = h.DB.SaveAccount(account)
|
||||
checkError(err)
|
||||
|
||||
// Delete user's sessions
|
||||
|
|
|
@ -52,9 +52,17 @@ func (h *handler) validateSession(r *http.Request) error {
|
|||
}
|
||||
|
||||
// Make sure session is not expired yet
|
||||
if _, found := h.SessionCache.Get(sessionID.Value); !found {
|
||||
val, found := h.SessionCache.Get(sessionID.Value)
|
||||
if !found {
|
||||
return fmt.Errorf("session has been expired")
|
||||
}
|
||||
|
||||
// If this is not get request, make sure it's owner
|
||||
if r.Method != "" && r.Method != "GET" {
|
||||
if isOwner := val.(bool); !isOwner {
|
||||
return fmt.Errorf("account level is not sufficient")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue