OMG all the things are horrible
parent
8dcb4be8a8
commit
c5b6971447
30
auth/auth.go
30
auth/auth.go
|
@ -16,6 +16,7 @@ type Auther interface {
|
||||||
AuthenticateToken(token string) (*User, error)
|
AuthenticateToken(token string) (*User, error)
|
||||||
CreateToken(user *User) (string, error)
|
CreateToken(user *User) (string, error)
|
||||||
RemoveToken(user *User) error
|
RemoveToken(user *User) error
|
||||||
|
ChangeSettings(user *User) error
|
||||||
|
|
||||||
// Authorize returns nil if the given user has access to the given topic using the desired
|
// Authorize returns nil if the given user has access to the given topic using the desired
|
||||||
// permission. The user param may be nil to signal an anonymous user.
|
// permission. The user param may be nil to signal an anonymous user.
|
||||||
|
@ -60,12 +61,29 @@ type Manager interface {
|
||||||
|
|
||||||
// User is a struct that represents a user
|
// User is a struct that represents a user
|
||||||
type User struct {
|
type User struct {
|
||||||
Name string
|
Name string
|
||||||
Hash string // password hash (bcrypt)
|
Hash string // password hash (bcrypt)
|
||||||
Token string // Only set if token was used to log in
|
Token string // Only set if token was used to log in
|
||||||
Role Role
|
Role Role
|
||||||
Grants []Grant
|
Grants []Grant
|
||||||
Language string
|
Prefs *UserPrefs
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserPrefs struct {
|
||||||
|
Language string `json:"language,omitempty"`
|
||||||
|
Notification *UserNotificationPrefs `json:"notification,omitempty"`
|
||||||
|
Subscriptions []*UserSubscription `json:"subscriptions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSubscription struct {
|
||||||
|
BaseURL string `json:"base_url"`
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserNotificationPrefs struct {
|
||||||
|
Sound string `json:"sound"`
|
||||||
|
MinPriority string `json:"min_priority"`
|
||||||
|
DeleteAfter int `json:"delete_after"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grant is a struct that represents an access control entry to a topic
|
// Grant is a struct that represents an access control entry to a topic
|
||||||
|
|
|
@ -2,6 +2,7 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
||||||
|
@ -32,10 +33,7 @@ const (
|
||||||
user TEXT NOT NULL,
|
user TEXT NOT NULL,
|
||||||
pass TEXT NOT NULL,
|
pass TEXT NOT NULL,
|
||||||
role TEXT NOT NULL,
|
role TEXT NOT NULL,
|
||||||
language TEXT,
|
settings JSON,
|
||||||
notification_sound TEXT,
|
|
||||||
notification_min_priority INT,
|
|
||||||
notification_delete_after INT,
|
|
||||||
FOREIGN KEY (plan_id) REFERENCES plan (id)
|
FOREIGN KEY (plan_id) REFERENCES plan (id)
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX idx_user ON user (user);
|
CREATE UNIQUE INDEX idx_user ON user (user);
|
||||||
|
@ -47,12 +45,6 @@ const (
|
||||||
PRIMARY KEY (user_id, topic),
|
PRIMARY KEY (user_id, topic),
|
||||||
FOREIGN KEY (user_id) REFERENCES user (id)
|
FOREIGN KEY (user_id) REFERENCES user (id)
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS user_subscription (
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
base_url TEXT NOT NULL,
|
|
||||||
topic TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (user_id, base_url, topic)
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS user_token (
|
CREATE TABLE IF NOT EXISTS user_token (
|
||||||
user_id INT NOT NULL,
|
user_id INT NOT NULL,
|
||||||
token TEXT NOT NULL,
|
token TEXT NOT NULL,
|
||||||
|
@ -68,12 +60,12 @@ const (
|
||||||
COMMIT;
|
COMMIT;
|
||||||
`
|
`
|
||||||
selectUserByNameQuery = `
|
selectUserByNameQuery = `
|
||||||
SELECT user, pass, role, language
|
SELECT user, pass, role, settings
|
||||||
FROM user
|
FROM user
|
||||||
WHERE user = ?
|
WHERE user = ?
|
||||||
`
|
`
|
||||||
selectUserByTokenQuery = `
|
selectUserByTokenQuery = `
|
||||||
SELECT user, pass, role, language
|
SELECT user, pass, role, settings
|
||||||
FROM user
|
FROM user
|
||||||
JOIN user_token on user.id = user_token.user_id
|
JOIN user_token on user.id = user_token.user_id
|
||||||
WHERE token = ?
|
WHERE token = ?
|
||||||
|
@ -101,8 +93,9 @@ const (
|
||||||
deleteUserAccessQuery = `DELETE FROM user_access WHERE user_id = (SELECT id FROM user WHERE user = ?)`
|
deleteUserAccessQuery = `DELETE FROM user_access WHERE user_id = (SELECT id FROM user WHERE user = ?)`
|
||||||
deleteTopicAccessQuery = `DELETE FROM user_access WHERE user_id = (SELECT id FROM user WHERE user = ?) AND topic = ?`
|
deleteTopicAccessQuery = `DELETE FROM user_access WHERE user_id = (SELECT id FROM user WHERE user = ?) AND topic = ?`
|
||||||
|
|
||||||
insertTokenQuery = `INSERT INTO user_token (user_id, token, expires) VALUES ((SELECT id FROM user WHERE user = ?), ?, ?)`
|
insertTokenQuery = `INSERT INTO user_token (user_id, token, expires) VALUES ((SELECT id FROM user WHERE user = ?), ?, ?)`
|
||||||
deleteTokenQuery = `DELETE FROM user_token WHERE user_id = (SELECT id FROM user WHERE user = ?) AND token = ?`
|
deleteTokenQuery = `DELETE FROM user_token WHERE user_id = (SELECT id FROM user WHERE user = ?) AND token = ?`
|
||||||
|
updateUserSettingsQuery = `UPDATE user SET settings = ? WHERE user = ?`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Schema management queries
|
// Schema management queries
|
||||||
|
@ -186,6 +179,17 @@ func (a *SQLiteAuth) RemoveToken(user *User) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *SQLiteAuth) ChangeSettings(user *User) error {
|
||||||
|
settings, err := json.Marshal(user.Prefs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := a.db.Exec(updateUserSettingsQuery, string(settings), user.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Authorize returns nil if the given user has access to the given topic using the desired
|
// Authorize returns nil if the given user has access to the given topic using the desired
|
||||||
// permission. The user param may be nil to signal an anonymous user.
|
// permission. The user param may be nil to signal an anonymous user.
|
||||||
func (a *SQLiteAuth) Authorize(user *User, topic string, perm Permission) error {
|
func (a *SQLiteAuth) Authorize(user *User, topic string, perm Permission) error {
|
||||||
|
@ -314,11 +318,11 @@ func (a *SQLiteAuth) userByToken(token string) (*User, error) {
|
||||||
func (a *SQLiteAuth) readUser(rows *sql.Rows) (*User, error) {
|
func (a *SQLiteAuth) readUser(rows *sql.Rows) (*User, error) {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var username, hash, role string
|
var username, hash, role string
|
||||||
var language sql.NullString
|
var prefs sql.NullString
|
||||||
if !rows.Next() {
|
if !rows.Next() {
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
if err := rows.Scan(&username, &hash, &role, &language); err != nil {
|
if err := rows.Scan(&username, &hash, &role, &prefs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if err := rows.Err(); err != nil {
|
} else if err := rows.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -327,13 +331,19 @@ func (a *SQLiteAuth) readUser(rows *sql.Rows) (*User, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &User{
|
user := &User{
|
||||||
Name: username,
|
Name: username,
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
Role: Role(role),
|
Role: Role(role),
|
||||||
Grants: grants,
|
Grants: grants,
|
||||||
Language: language.String,
|
}
|
||||||
}, nil
|
if prefs.Valid {
|
||||||
|
user.Prefs = &UserPrefs{}
|
||||||
|
if err := json.Unmarshal([]byte(prefs.String), user.Prefs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SQLiteAuth) everyoneUser() (*User, error) {
|
func (a *SQLiteAuth) everyoneUser() (*User, error) {
|
||||||
|
|
|
@ -323,6 +323,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||||
return s.handleUserTokenDelete(w, r, v)
|
return s.handleUserTokenDelete(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == userAccountPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == userAccountPath {
|
||||||
return s.handleUserAccount(w, r, v)
|
return s.handleUserAccount(w, r, v)
|
||||||
|
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == userAccountPath {
|
||||||
|
return s.handleUserAccountUpdate(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
|
||||||
return s.handleMatrixDiscovery(w)
|
return s.handleMatrixDiscovery(w)
|
||||||
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
||||||
|
@ -453,29 +455,16 @@ func (s *Server) handleUserTokenDelete(w http.ResponseWriter, r *http.Request, v
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type userSubscriptionResponse struct {
|
|
||||||
BaseURL string `json:"base_url"`
|
|
||||||
Topic string `json:"topic"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userNotificationSettingsResponse struct {
|
|
||||||
Sound string `json:"sound"`
|
|
||||||
MinPriority string `json:"min_priority"`
|
|
||||||
DeleteAfter int `json:"delete_after"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userPlanResponse struct {
|
type userPlanResponse struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type userAccountResponse struct {
|
type userAccountResponse struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Role string `json:"role,omitempty"`
|
Role string `json:"role,omitempty"`
|
||||||
Language string `json:"language,omitempty"`
|
Plan *userPlanResponse `json:"plan,omitempty"`
|
||||||
Plan *userPlanResponse `json:"plan,omitempty"`
|
Settings *auth.UserPrefs `json:"settings,omitempty"`
|
||||||
Notification *userNotificationSettingsResponse `json:"notification,omitempty"`
|
|
||||||
Subscriptions []*userSubscriptionResponse `json:"subscriptions,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleUserAccount(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleUserAccount(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
|
@ -485,10 +474,7 @@ func (s *Server) handleUserAccount(w http.ResponseWriter, r *http.Request, v *vi
|
||||||
if v.user != nil {
|
if v.user != nil {
|
||||||
response.Username = v.user.Name
|
response.Username = v.user.Name
|
||||||
response.Role = string(v.user.Role)
|
response.Role = string(v.user.Role)
|
||||||
response.Language = v.user.Language
|
response.Settings = v.user.Prefs
|
||||||
response.Notification = &userNotificationSettingsResponse{
|
|
||||||
Sound: "dadum",
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
response = &userAccountResponse{
|
response = &userAccountResponse{
|
||||||
Username: auth.Everyone,
|
Username: auth.Everyone,
|
||||||
|
@ -501,6 +487,41 @@ func (s *Server) handleUserAccount(w http.ResponseWriter, r *http.Request, v *vi
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserAccountUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
|
if v.user == nil {
|
||||||
|
return errors.New("no user")
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
|
||||||
|
body, err := util.Peek(r.Body, 4096) // FIXME
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
var newPrefs auth.UserPrefs
|
||||||
|
if err := json.NewDecoder(body).Decode(&newPrefs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if v.user.Prefs == nil {
|
||||||
|
v.user.Prefs = &auth.UserPrefs{}
|
||||||
|
}
|
||||||
|
prefs := v.user.Prefs
|
||||||
|
if newPrefs.Language != "" {
|
||||||
|
prefs.Language = newPrefs.Language
|
||||||
|
}
|
||||||
|
if newPrefs.Notification != nil {
|
||||||
|
if prefs.Notification == nil {
|
||||||
|
prefs.Notification = &auth.UserNotificationPrefs{}
|
||||||
|
}
|
||||||
|
if newPrefs.Notification.DeleteAfter > 0 {
|
||||||
|
prefs.Notification.DeleteAfter = newPrefs.Notification.DeleteAfter
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
return s.auth.ChangeSettings(v.user)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
||||||
r.URL.Path = webSiteDir + r.URL.Path
|
r.URL.Path = webSiteDir + r.URL.Path
|
||||||
util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
|
util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
|
||||||
|
|
|
@ -172,6 +172,20 @@ class Api {
|
||||||
console.log(`[Api] Account`, account);
|
console.log(`[Api] Account`, account);
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateUserAccount(baseUrl, token, payload) {
|
||||||
|
const url = userAccountUrl(baseUrl);
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
|
console.log(`[Api] Updating user account ${url}: ${body}`);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: maybeWithBearerAuth({}, token),
|
||||||
|
body: body
|
||||||
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Unexpected server response ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = new Api();
|
const api = new Api();
|
||||||
|
|
|
@ -34,6 +34,8 @@ import DialogActions from "@mui/material/DialogActions";
|
||||||
import userManager from "../app/UserManager";
|
import userManager from "../app/UserManager";
|
||||||
import {playSound, shuffle, sounds, validTopic, validUrl} from "../app/utils";
|
import {playSound, shuffle, sounds, validTopic, validUrl} from "../app/utils";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
|
import api from "../app/Api";
|
||||||
|
import session from "../app/Session";
|
||||||
|
|
||||||
const Preferences = () => {
|
const Preferences = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -443,7 +445,9 @@ const Language = () => {
|
||||||
|
|
||||||
const handleChange = async (ev) => {
|
const handleChange = async (ev) => {
|
||||||
await i18n.changeLanguage(ev.target.value);
|
await i18n.changeLanguage(ev.target.value);
|
||||||
//api.update
|
await api.updateUserAccount("http://localhost:2586", session.token(), {
|
||||||
|
language: ev.target.value
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remember: Flags are not languages. Don't put flags next to the language in the list.
|
// Remember: Flags are not languages. Don't put flags next to the language in the list.
|
||||||
|
|
Loading…
Reference in New Issue