Add basic account level management

This commit is contained in:
Radhi Fadlillah 2019-08-12 19:57:18 +07:00
parent 326f04d19e
commit 0c4d75f773
18 changed files with 177 additions and 117 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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"`
}

View file

@ -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,

View file

@ -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

View file

@ -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) => {

View file

@ -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 () {

View file

@ -1,6 +1,16 @@
export default {
props: {
displayOptions: {
activeAccount: {
type: Object,
default () {
return {
id: 0,
username: "",
owner: false,
}
}
},
appOptions: {
type: Object,
default () {
return {

View file

@ -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}"`,

View file

@ -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;

View file

@ -75,6 +75,7 @@
}
>input[type="text"],
>input[type="password"],
>textarea {
color: var(--color);
padding: 8px;

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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
}