mirror of
https://github.com/go-shiori/shiori.git
synced 2025-02-22 15:06:04 +08:00
Add account management in web interface
This commit is contained in:
parent
fcc77e2db8
commit
3503484c2b
9 changed files with 455 additions and 26 deletions
|
@ -46,12 +46,18 @@ type DB interface {
|
||||||
// GetBookmark fetchs bookmark based on its ID or URL.
|
// GetBookmark fetchs bookmark based on its ID or URL.
|
||||||
GetBookmark(id int, url string) (model.Bookmark, bool)
|
GetBookmark(id int, url string) (model.Bookmark, bool)
|
||||||
|
|
||||||
// GetAccounts fetch list of accounts with matching keyword.
|
// SaveAccount saves new account in database
|
||||||
|
SaveAccount(username, password string) error
|
||||||
|
|
||||||
|
// GetAccounts fetch list of account (without its password) with matching keyword.
|
||||||
GetAccounts(keyword string) ([]model.Account, error)
|
GetAccounts(keyword string) ([]model.Account, error)
|
||||||
|
|
||||||
// GetAccount fetch account with matching username.
|
// GetAccount fetch account with matching username.
|
||||||
GetAccount(username string) (model.Account, bool)
|
GetAccount(username string) (model.Account, bool)
|
||||||
|
|
||||||
|
// DeleteAccounts removes all record with matching usernames
|
||||||
|
DeleteAccounts(usernames ...string) error
|
||||||
|
|
||||||
// GetTags fetch list of tags and its frequency from database.
|
// GetTags fetch list of tags and its frequency from database.
|
||||||
GetTags() ([]model.Tag, error)
|
GetTags() ([]model.Tag, error)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/go-shiori/shiori/internal/model"
|
"github.com/go-shiori/shiori/internal/model"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SQLiteDatabase is implementation of Database interface
|
// SQLiteDatabase is implementation of Database interface
|
||||||
|
@ -422,11 +423,29 @@ func (db *SQLiteDatabase) GetBookmark(id int, url string) (model.Bookmark, bool)
|
||||||
return book, book.ID != 0
|
return book, book.ID != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccounts fetch list of accounts with matching keyword.
|
// SaveAccount saves new account to database. Returns error if any happened.
|
||||||
|
func (db *SQLiteDatabase) SaveAccount(username, password string) (err error) {
|
||||||
|
// Hash password with bcrypt
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert account to database
|
||||||
|
_, err = db.Exec(`INSERT INTO account
|
||||||
|
(username, password) VALUES (?, ?)
|
||||||
|
ON CONFLICT(username) DO UPDATE SET
|
||||||
|
password = ?`,
|
||||||
|
username, hashedPassword, hashedPassword)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccounts fetch list of account (without its password) with matching keyword.
|
||||||
func (db *SQLiteDatabase) GetAccounts(keyword string) ([]model.Account, error) {
|
func (db *SQLiteDatabase) GetAccounts(keyword string) ([]model.Account, error) {
|
||||||
// Create query
|
// Create query
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
query := `SELECT id, username, password FROM account WHERE 1`
|
query := `SELECT id, username FROM account WHERE 1`
|
||||||
|
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
query += " AND username LIKE ?"
|
query += " AND username LIKE ?"
|
||||||
|
@ -456,6 +475,37 @@ func (db *SQLiteDatabase) GetAccount(username string) (model.Account, bool) {
|
||||||
return account, account.ID != 0
|
return account, account.ID != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteAccounts removes all record with matching usernames.
|
||||||
|
func (db *SQLiteDatabase) DeleteAccounts(usernames ...string) (err error) {
|
||||||
|
// Begin transaction
|
||||||
|
tx, err := db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure to rollback if panic ever happened
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
panicErr, _ := r.(error)
|
||||||
|
tx.Rollback()
|
||||||
|
|
||||||
|
err = panicErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Delete account
|
||||||
|
stmtDelete, _ := tx.Preparex(`DELETE FROM account WHERE username = ?`)
|
||||||
|
for _, username := range usernames {
|
||||||
|
stmtDelete.MustExec(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
err = tx.Commit()
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// GetTags fetch list of tags and their frequency.
|
// GetTags fetch list of tags and their frequency.
|
||||||
func (db *SQLiteDatabase) GetTags() ([]model.Tag, error) {
|
func (db *SQLiteDatabase) GetTags() ([]model.Tag, error) {
|
||||||
tags := []model.Tag{}
|
tags := []model.Tag{}
|
||||||
|
|
|
@ -27,7 +27,7 @@ type Bookmark struct {
|
||||||
type Account struct {
|
type Account struct {
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Username string `db:"username" json:"username"`
|
Username string `db:"username" json:"username"`
|
||||||
Password string `db:"password" json:"password"`
|
Password string `db:"password" json:"password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginRequest is request from user to access web interface.
|
// LoginRequest is request from user to access web interface.
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,11 +1,6 @@
|
||||||
var template = `
|
var template = `
|
||||||
<div id="page-setting">
|
<div id="page-setting">
|
||||||
<div class="page-header">
|
<h1 class="page-header">Settings</h1>
|
||||||
<p>Settings</p>
|
|
||||||
<a href="#" title="Refresh setting">
|
|
||||||
<i class="fas fa-fw fa-sync-alt"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="setting-container">
|
<div class="setting-container">
|
||||||
<details open class="setting-group" id="setting-display">
|
<details open class="setting-group" id="setting-display">
|
||||||
<summary>Display</summary>
|
<summary>Display</summary>
|
||||||
|
@ -22,6 +17,25 @@ var template = `
|
||||||
Use dark theme
|
Use dark theme
|
||||||
</label>
|
</label>
|
||||||
</details>
|
</details>
|
||||||
|
<details 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>
|
||||||
|
<a title="Change password" @click="showDialogChangePassword(account)">
|
||||||
|
<i class="fa fas fa-fw fa-key"></i>
|
||||||
|
</a>
|
||||||
|
<a title="Delete account" @click="showDialogDeleteAccount(account, idx)">
|
||||||
|
<i class="fa fas fa-fw fa-trash-alt"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="setting-group-footer">
|
||||||
|
<a @click="loadAccounts">Refresh accounts</a>
|
||||||
|
<a @click="showDialogNewAccount">Add new account</a>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div class="loading-overlay" v-if="loading"><i class="fas fa-fw fa-spin fa-spinner"></i></div>
|
<div class="loading-overlay" v-if="loading"><i class="fas fa-fw fa-spin fa-spinner"></i></div>
|
||||||
<custom-dialog v-bind="dialog"/>
|
<custom-dialog v-bind="dialog"/>
|
||||||
|
@ -39,6 +53,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
accounts: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -48,6 +63,214 @@ export default {
|
||||||
listMode: this.displayOptions.listMode,
|
listMode: this.displayOptions.listMode,
|
||||||
nightMode: this.displayOptions.nightMode,
|
nightMode: this.displayOptions.nightMode,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
loadAccounts() {
|
||||||
|
if (this.loading) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
fetch("/api/accounts")
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw response;
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
this.loading = false;
|
||||||
|
this.accounts = json;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.loading = false;
|
||||||
|
err.text().then(msg => {
|
||||||
|
this.showErrorDialog(msg, err.status);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showDialogNewAccount() {
|
||||||
|
this.showDialog({
|
||||||
|
title: "New Account",
|
||||||
|
content: "Input new account's data :",
|
||||||
|
fields: [{
|
||||||
|
name: "username",
|
||||||
|
label: "Username",
|
||||||
|
value: "",
|
||||||
|
}, {
|
||||||
|
name: "password",
|
||||||
|
label: "Password",
|
||||||
|
type: "password",
|
||||||
|
value: "",
|
||||||
|
}, {
|
||||||
|
name: "repeat",
|
||||||
|
label: "Repeat password",
|
||||||
|
type: "password",
|
||||||
|
value: "",
|
||||||
|
}],
|
||||||
|
mainText: "OK",
|
||||||
|
secondText: "Cancel",
|
||||||
|
mainClick: (data) => {
|
||||||
|
if (data.username === "") {
|
||||||
|
this.showErrorDialog("Username must not empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.password === "") {
|
||||||
|
this.showErrorDialog("Password must not empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.password !== data.repeat) {
|
||||||
|
this.showErrorDialog("Password does not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
username: data.username,
|
||||||
|
password: data.password
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialog.loading = true;
|
||||||
|
fetch("/api/accounts", {
|
||||||
|
method: "post",
|
||||||
|
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;
|
||||||
|
|
||||||
|
this.accounts.push({ username: data.username });
|
||||||
|
this.accounts.sort((a, b) => {
|
||||||
|
var nameA = a.username.toLowerCase(),
|
||||||
|
nameB = b.username.toLowerCase();
|
||||||
|
|
||||||
|
if (nameA < nameB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameA > nameB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.dialog.loading = false;
|
||||||
|
err.text().then(msg => {
|
||||||
|
this.showErrorDialog(msg, err.status);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showDialogChangePassword(account) {
|
||||||
|
this.showDialog({
|
||||||
|
title: "Change Password",
|
||||||
|
content: "Input new password :",
|
||||||
|
fields: [{
|
||||||
|
name: "oldPassword",
|
||||||
|
label: "Old password",
|
||||||
|
type: "password",
|
||||||
|
value: "",
|
||||||
|
}, {
|
||||||
|
name: "password",
|
||||||
|
label: "New password",
|
||||||
|
type: "password",
|
||||||
|
value: "",
|
||||||
|
}, {
|
||||||
|
name: "repeat",
|
||||||
|
label: "Repeat password",
|
||||||
|
type: "password",
|
||||||
|
value: "",
|
||||||
|
}],
|
||||||
|
mainText: "OK",
|
||||||
|
secondText: "Cancel",
|
||||||
|
mainClick: (data) => {
|
||||||
|
if (data.oldPassword === "") {
|
||||||
|
this.showErrorDialog("Old password must not empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.password === "") {
|
||||||
|
this.showErrorDialog("New password must not empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.password !== data.repeat) {
|
||||||
|
this.showErrorDialog("Password does not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
username: account.username,
|
||||||
|
oldPassword: data.oldPassword,
|
||||||
|
newPassword: data.password
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialog.loading = true;
|
||||||
|
fetch("/api/accounts", {
|
||||||
|
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;
|
||||||
|
err.text().then(msg => {
|
||||||
|
this.showErrorDialog(msg, err.status);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showDialogDeleteAccount(account, idx) {
|
||||||
|
this.showDialog({
|
||||||
|
title: "Delete Account",
|
||||||
|
content: `Delete account "${account.username}" ?`,
|
||||||
|
mainText: "Yes",
|
||||||
|
secondText: "No",
|
||||||
|
mainClick: () => {
|
||||||
|
this.dialog.loading = true;
|
||||||
|
fetch(`/api/accounts`, {
|
||||||
|
method: "delete",
|
||||||
|
body: JSON.stringify([account.username]),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw response;
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.dialog.loading = false;
|
||||||
|
this.dialog.visible = false;
|
||||||
|
this.accounts.splice(idx, 1);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.dialog.loading = false;
|
||||||
|
err.text().then(msg => {
|
||||||
|
this.showErrorDialog(msg, err.status);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadAccounts();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -55,7 +55,7 @@ body {
|
||||||
>.error-message {
|
>.error-message {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
font-size: 0.9em;
|
font-size: 1em;
|
||||||
background-color: var(--contentBg);
|
background-color: var(--contentBg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
@ -557,13 +557,13 @@ body {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--mainDark);
|
color: var(--main);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
color: var(--mainDark);
|
color: var(--main);
|
||||||
border-bottom: 1px dashed var(--mainDark);
|
border-bottom: 1px dashed var(--main);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -598,4 +598,40 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#setting-accounts {
|
||||||
|
summary {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 1em;
|
||||||
|
color: var(--color);
|
||||||
|
flex: 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: var(--colorLink);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--main);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
|
@ -571,3 +571,112 @@ func (h *handler) apiUpdateBookmarkTags(w http.ResponseWriter, r *http.Request,
|
||||||
err = json.NewEncoder(w).Encode(&bookmarks)
|
err = json.NewEncoder(w).Encode(&bookmarks)
|
||||||
checkError(err)
|
checkError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apiGetAccounts is handler for GET /api/accounts
|
||||||
|
func (h *handler) apiGetAccounts(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
// Make sure session still valid
|
||||||
|
err := h.validateSession(r)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Get list of usernames from database
|
||||||
|
accounts, err := h.DB.GetAccounts("")
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err = json.NewEncoder(w).Encode(&accounts)
|
||||||
|
checkError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiInsertAccount is handler for POST /api/accounts
|
||||||
|
func (h *handler) apiInsertAccount(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
// Make sure session still valid
|
||||||
|
err := h.validateSession(r)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Decode request
|
||||||
|
var account model.Account
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&account)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Save account to database
|
||||||
|
err = h.DB.SaveAccount(account.Username, account.Password)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
fmt.Fprint(w, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiUpdateAccount is handler for PUT /api/accounts
|
||||||
|
func (h *handler) apiUpdateAccount(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
// Make sure session still valid
|
||||||
|
err := h.validateSession(r)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Decode request
|
||||||
|
request := struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
OldPassword string `json:"oldPassword"`
|
||||||
|
NewPassword string `json:"newPassword"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&request)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Get existing account data from database
|
||||||
|
account, exist := h.DB.GetAccount(request.Username)
|
||||||
|
if !exist {
|
||||||
|
panic(fmt.Errorf("username doesn't exist"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare old password with database
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(request.OldPassword))
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("old password doesn't match"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save new password to database
|
||||||
|
err = h.DB.SaveAccount(request.Username, request.NewPassword)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Delete user's sessions
|
||||||
|
if val, found := h.UserCache.Get(request.Username); found {
|
||||||
|
userSessions := val.([]string)
|
||||||
|
for _, session := range userSessions {
|
||||||
|
h.SessionCache.Delete(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.UserCache.Delete(request.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiDeleteAccount is handler for DELETE /api/accounts
|
||||||
|
func (h *handler) apiDeleteAccount(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
// Make sure session still valid
|
||||||
|
err := h.validateSession(r)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Decode request
|
||||||
|
usernames := []string{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&usernames)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Delete accounts
|
||||||
|
err = h.DB.DeleteAccounts(usernames...)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Delete user's sessions
|
||||||
|
userSessions := []string{}
|
||||||
|
for _, username := range usernames {
|
||||||
|
if val, found := h.UserCache.Get(username); found {
|
||||||
|
userSessions = val.([]string)
|
||||||
|
for _, session := range userSessions {
|
||||||
|
h.SessionCache.Delete(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.UserCache.Delete(username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, 1)
|
||||||
|
}
|
||||||
|
|
|
@ -45,6 +45,11 @@ func ServeApp(DB database.DB, dataDir string, port int) error {
|
||||||
router.PUT("/api/archive", hdl.apiUpdateArchive)
|
router.PUT("/api/archive", hdl.apiUpdateArchive)
|
||||||
router.PUT("/api/bookmarks/tags", hdl.apiUpdateBookmarkTags)
|
router.PUT("/api/bookmarks/tags", hdl.apiUpdateBookmarkTags)
|
||||||
|
|
||||||
|
router.GET("/api/accounts", hdl.apiGetAccounts)
|
||||||
|
router.PUT("/api/accounts", hdl.apiUpdateAccount)
|
||||||
|
router.POST("/api/accounts", hdl.apiInsertAccount)
|
||||||
|
router.DELETE("/api/accounts", hdl.apiDeleteAccount)
|
||||||
|
|
||||||
// Route for panic
|
// Route for panic
|
||||||
router.PanicHandler = func(w http.ResponseWriter, r *http.Request, arg interface{}) {
|
router.PanicHandler = func(w http.ResponseWriter, r *http.Request, arg interface{}) {
|
||||||
http.Error(w, fmt.Sprint(arg), 500)
|
http.Error(w, fmt.Sprint(arg), 500)
|
||||||
|
|
Loading…
Reference in a new issue