OMG all the things are horrible
This commit is contained in:
		
							parent
							
								
									8dcb4be8a8
								
							
						
					
					
						commit
						c5b6971447
					
				
					 5 changed files with 119 additions and 52 deletions
				
			
		
							
								
								
									
										30
									
								
								auth/auth.go
									
										
									
									
									
								
							
							
						
						
									
										30
									
								
								auth/auth.go
									
										
									
									
									
								
							|  | @ -16,6 +16,7 @@ type Auther interface { | |||
| 	AuthenticateToken(token string) (*User, error) | ||||
| 	CreateToken(user *User) (string, 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 | ||||
| 	// 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 | ||||
| type User struct { | ||||
| 	Name     string | ||||
| 	Hash     string // password hash (bcrypt) | ||||
| 	Token    string // Only set if token was used to log in | ||||
| 	Role     Role | ||||
| 	Grants   []Grant | ||||
| 	Language string | ||||
| 	Name   string | ||||
| 	Hash   string // password hash (bcrypt) | ||||
| 	Token  string // Only set if token was used to log in | ||||
| 	Role   Role | ||||
| 	Grants []Grant | ||||
| 	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 | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ package auth | |||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	_ "github.com/mattn/go-sqlite3" // SQLite driver | ||||
|  | @ -32,10 +33,7 @@ const ( | |||
| 			user TEXT NOT NULL, | ||||
| 			pass TEXT NOT NULL, | ||||
| 			role TEXT NOT NULL, | ||||
| 			language TEXT, | ||||
| 			notification_sound TEXT, | ||||
| 			notification_min_priority INT, | ||||
| 			notification_delete_after INT, | ||||
| 			settings JSON, | ||||
| 		    FOREIGN KEY (plan_id) REFERENCES plan (id) | ||||
| 		); | ||||
| 		CREATE UNIQUE INDEX idx_user ON user (user); | ||||
|  | @ -47,12 +45,6 @@ const ( | |||
| 			PRIMARY KEY (user_id, topic), | ||||
| 			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 ( | ||||
| 			user_id INT NOT NULL, | ||||
| 			token TEXT NOT NULL, | ||||
|  | @ -68,12 +60,12 @@ const ( | |||
| 		COMMIT; | ||||
| 	` | ||||
| 	selectUserByNameQuery = ` | ||||
| 		SELECT user, pass, role, language  | ||||
| 		SELECT user, pass, role, settings  | ||||
| 		FROM user  | ||||
| 		WHERE user = ? | ||||
| 	` | ||||
| 	selectUserByTokenQuery = ` | ||||
| 		SELECT user, pass, role, language  | ||||
| 		SELECT user, pass, role, settings  | ||||
| 		FROM user | ||||
| 		JOIN user_token on user.id = user_token.user_id | ||||
| 		WHERE token = ? | ||||
|  | @ -101,8 +93,9 @@ const ( | |||
| 	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 = ?` | ||||
| 
 | ||||
| 	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 = ?` | ||||
| 	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 = ?` | ||||
| 	updateUserSettingsQuery = `UPDATE user SET settings = ? WHERE user = ?` | ||||
| ) | ||||
| 
 | ||||
| // Schema management queries | ||||
|  | @ -186,6 +179,17 @@ func (a *SQLiteAuth) RemoveToken(user *User) error { | |||
| 	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 | ||||
| // permission. The user param may be nil to signal an anonymous user. | ||||
| 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) { | ||||
| 	defer rows.Close() | ||||
| 	var username, hash, role string | ||||
| 	var language sql.NullString | ||||
| 	var prefs sql.NullString | ||||
| 	if !rows.Next() { | ||||
| 		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 | ||||
| 	} else if err := rows.Err(); err != nil { | ||||
| 		return nil, err | ||||
|  | @ -327,13 +331,19 @@ func (a *SQLiteAuth) readUser(rows *sql.Rows) (*User, error) { | |||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &User{ | ||||
| 		Name:     username, | ||||
| 		Hash:     hash, | ||||
| 		Role:     Role(role), | ||||
| 		Grants:   grants, | ||||
| 		Language: language.String, | ||||
| 	}, nil | ||||
| 	user := &User{ | ||||
| 		Name:   username, | ||||
| 		Hash:   hash, | ||||
| 		Role:   Role(role), | ||||
| 		Grants: grants, | ||||
| 	} | ||||
| 	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) { | ||||
|  |  | |||
|  | @ -323,6 +323,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit | |||
| 		return s.handleUserTokenDelete(w, r, v) | ||||
| 	} else if r.Method == http.MethodGet && r.URL.Path == userAccountPath { | ||||
| 		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 { | ||||
| 		return s.handleMatrixDiscovery(w) | ||||
| 	} 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 | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 	Id   int    `json:"id"` | ||||
| 	Name string `json:"name"` | ||||
| } | ||||
| 
 | ||||
| type userAccountResponse struct { | ||||
| 	Username      string                            `json:"username"` | ||||
| 	Role          string                            `json:"role,omitempty"` | ||||
| 	Language      string                            `json:"language,omitempty"` | ||||
| 	Plan          *userPlanResponse                 `json:"plan,omitempty"` | ||||
| 	Notification  *userNotificationSettingsResponse `json:"notification,omitempty"` | ||||
| 	Subscriptions []*userSubscriptionResponse       `json:"subscriptions,omitempty"` | ||||
| 	Username string            `json:"username"` | ||||
| 	Role     string            `json:"role,omitempty"` | ||||
| 	Plan     *userPlanResponse `json:"plan,omitempty"` | ||||
| 	Settings *auth.UserPrefs   `json:"settings,omitempty"` | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 		response.Username = v.user.Name | ||||
| 		response.Role = string(v.user.Role) | ||||
| 		response.Language = v.user.Language | ||||
| 		response.Notification = &userNotificationSettingsResponse{ | ||||
| 			Sound: "dadum", | ||||
| 		} | ||||
| 		response.Settings = v.user.Prefs | ||||
| 	} else { | ||||
| 		response = &userAccountResponse{ | ||||
| 			Username: auth.Everyone, | ||||
|  | @ -501,6 +487,41 @@ func (s *Server) handleUserAccount(w http.ResponseWriter, r *http.Request, v *vi | |||
| 	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 { | ||||
| 	r.URL.Path = webSiteDir + r.URL.Path | ||||
| 	util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r) | ||||
|  |  | |||
|  | @ -172,6 +172,20 @@ class Api { | |||
|         console.log(`[Api] Account`, 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(); | ||||
|  |  | |||
|  | @ -34,6 +34,8 @@ import DialogActions from "@mui/material/DialogActions"; | |||
| import userManager from "../app/UserManager"; | ||||
| import {playSound, shuffle, sounds, validTopic, validUrl} from "../app/utils"; | ||||
| import {useTranslation} from "react-i18next"; | ||||
| import api from "../app/Api"; | ||||
| import session from "../app/Session"; | ||||
| 
 | ||||
| const Preferences = () => { | ||||
|     return ( | ||||
|  | @ -443,7 +445,9 @@ const Language = () => { | |||
| 
 | ||||
|     const handleChange = async (ev) => { | ||||
|         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.
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue