OMG all the things are horrible
This commit is contained in:
		
							parent
							
								
									8dcb4be8a8
								
							
						
					
					
						commit
						c5b6971447
					
				
					 5 changed files with 119 additions and 52 deletions
				
			
		
							
								
								
									
										20
									
								
								auth/auth.go
									
										
									
									
									
								
							
							
						
						
									
										20
									
								
								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. | ||||||
|  | @ -65,7 +66,24 @@ type User struct { | ||||||
| 	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 = ? | ||||||
|  | @ -103,6 +95,7 @@ const ( | ||||||
| 
 | 
 | ||||||
| 	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,17 +455,6 @@ 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"` | ||||||
|  | @ -472,10 +463,8 @@ type userPlanResponse struct { | ||||||
| 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"` | ||||||
| 	Notification  *userNotificationSettingsResponse `json:"notification,omitempty"` | 	Settings *auth.UserPrefs   `json:"settings,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…
	
	Add table
		Add a link
		
	
		Reference in a new issue