Plan stuff WIPWIPWIP
This commit is contained in:
		
							parent
							
								
									8752680233
								
							
						
					
					
						commit
						ac56fa36ba
					
				
					 8 changed files with 111 additions and 62 deletions
				
			
		|  | @ -64,6 +64,7 @@ type User struct { | ||||||
| 	Role   Role | 	Role   Role | ||||||
| 	Grants []Grant | 	Grants []Grant | ||||||
| 	Prefs  *UserPrefs | 	Prefs  *UserPrefs | ||||||
|  | 	Plan   *UserPlan | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type UserPrefs struct { | type UserPrefs struct { | ||||||
|  | @ -72,6 +73,13 @@ type UserPrefs struct { | ||||||
| 	Subscriptions []*UserSubscription    `json:"subscriptions,omitempty"` | 	Subscriptions []*UserSubscription    `json:"subscriptions,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type UserPlan struct { | ||||||
|  | 	Name                 string `json:"name"` | ||||||
|  | 	MessagesLimit        int    `json:"messages_limit"` | ||||||
|  | 	EmailsLimit          int    `json:"emails_limit"` | ||||||
|  | 	AttachmentBytesLimit int64  `json:"attachment_bytes_limit"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type UserSubscription struct { | type UserSubscription struct { | ||||||
| 	ID      string `json:"id"` | 	ID      string `json:"id"` | ||||||
| 	BaseURL string `json:"base_url"` | 	BaseURL string `json:"base_url"` | ||||||
|  |  | ||||||
|  | @ -24,7 +24,9 @@ const ( | ||||||
| 		CREATE TABLE IF NOT EXISTS plan ( | 		CREATE TABLE IF NOT EXISTS plan ( | ||||||
| 			id INT NOT NULL,		 | 			id INT NOT NULL,		 | ||||||
| 			name TEXT NOT NULL, | 			name TEXT NOT NULL, | ||||||
| 			limit_messages INT, | 			messages_limit INT NOT NULL, | ||||||
|  | 			emails_limit INT NOT NULL, | ||||||
|  | 			attachment_bytes_limit INT NOT NULL, | ||||||
| 			PRIMARY KEY (id) | 			PRIMARY KEY (id) | ||||||
| 		); | 		); | ||||||
| 		CREATE TABLE IF NOT EXISTS user ( | 		CREATE TABLE IF NOT EXISTS user ( | ||||||
|  | @ -55,20 +57,21 @@ const ( | ||||||
| 			id INT PRIMARY KEY, | 			id INT PRIMARY KEY, | ||||||
| 			version INT NOT NULL | 			version INT NOT NULL | ||||||
| 		); | 		); | ||||||
| 		INSERT INTO plan (id, name) VALUES (1, 'Admin') ON CONFLICT (id) DO NOTHING; |  | ||||||
| 		INSERT INTO user (id, user, pass, role) VALUES (1, '*', '', 'anonymous') ON CONFLICT (id) DO NOTHING; | 		INSERT INTO user (id, user, pass, role) VALUES (1, '*', '', 'anonymous') ON CONFLICT (id) DO NOTHING; | ||||||
| 		COMMIT; | 		COMMIT; | ||||||
| 	` | 	` | ||||||
| 	selectUserByNameQuery = ` | 	selectUserByNameQuery = ` | ||||||
| 		SELECT user, pass, role, settings  | 		SELECT u.user, u.pass, u.role, u.settings, p.name, p.messages_limit, p.emails_limit, p.attachment_bytes_limit | ||||||
| 		FROM user  | 		FROM user u | ||||||
|  | 		LEFT JOIN plan p on p.id = u.plan_id | ||||||
| 		WHERE user = ?		 | 		WHERE user = ?		 | ||||||
| 	` | 	` | ||||||
| 	selectUserByTokenQuery = ` | 	selectUserByTokenQuery = ` | ||||||
| 		SELECT user, pass, role, settings  | 		SELECT u.user, u.pass, u.role, u.settings, p.name, p.messages_limit, p.emails_limit, p.attachment_bytes_limit | ||||||
| 		FROM user | 		FROM user u | ||||||
| 		JOIN user_token on user.id = user_token.user_id | 		JOIN user_token t on u.id = t.user_id | ||||||
| 		WHERE token = ? | 		LEFT JOIN plan p on p.id = u.plan_id | ||||||
|  | 		WHERE t.token = ? | ||||||
| 	` | 	` | ||||||
| 	selectTopicPermsQuery = ` | 	selectTopicPermsQuery = ` | ||||||
| 		SELECT read, write  | 		SELECT read, write  | ||||||
|  | @ -321,11 +324,13 @@ func (a *SQLiteAuthManager) userByToken(token string) (*User, error) { | ||||||
| func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) { | func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) { | ||||||
| 	defer rows.Close() | 	defer rows.Close() | ||||||
| 	var username, hash, role string | 	var username, hash, role string | ||||||
| 	var prefs sql.NullString | 	var prefs, planName sql.NullString | ||||||
|  | 	var messagesLimit, emailsLimit sql.NullInt32 | ||||||
|  | 	var attachmentBytesLimit sql.NullInt64 | ||||||
| 	if !rows.Next() { | 	if !rows.Next() { | ||||||
| 		return nil, ErrNotFound | 		return nil, ErrNotFound | ||||||
| 	} | 	} | ||||||
| 	if err := rows.Scan(&username, &hash, &role, &prefs); err != nil { | 	if err := rows.Scan(&username, &hash, &role, &prefs, &planName, &messagesLimit, &emailsLimit, &attachmentBytesLimit); 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 | ||||||
|  | @ -346,6 +351,14 @@ func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	if planName.Valid { | ||||||
|  | 		user.Plan = &UserPlan{ | ||||||
|  | 			Name:                 planName.String, | ||||||
|  | 			MessagesLimit:        int(messagesLimit.Int32), | ||||||
|  | 			EmailsLimit:          int(emailsLimit.Int32), | ||||||
|  | 			AttachmentBytesLimit: attachmentBytesLimit.Int64, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return user, nil | 	return user, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -333,6 +333,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit | ||||||
| 		return s.handleUserStats(w, r, v) | 		return s.handleUserStats(w, r, v) | ||||||
| 	} else if r.Method == http.MethodPost && r.URL.Path == accountPath { | 	} else if r.Method == http.MethodPost && r.URL.Path == accountPath { | ||||||
| 		return s.handleAccountCreate(w, r, v) | 		return s.handleAccountCreate(w, r, v) | ||||||
|  | 	} else if r.Method == http.MethodGet && r.URL.Path == accountPath { | ||||||
|  | 		return s.handleAccountGet(w, r, v) | ||||||
| 	} else if r.Method == http.MethodDelete && r.URL.Path == accountPath { | 	} else if r.Method == http.MethodDelete && r.URL.Path == accountPath { | ||||||
| 		return s.handleAccountDelete(w, r, v) | 		return s.handleAccountDelete(w, r, v) | ||||||
| 	} else if r.Method == http.MethodPost && r.URL.Path == accountPasswordPath { | 	} else if r.Method == http.MethodPost && r.URL.Path == accountPasswordPath { | ||||||
|  | @ -341,8 +343,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit | ||||||
| 		return s.handleAccountTokenGet(w, r, v) | 		return s.handleAccountTokenGet(w, r, v) | ||||||
| 	} else if r.Method == http.MethodDelete && r.URL.Path == accountTokenPath { | 	} else if r.Method == http.MethodDelete && r.URL.Path == accountTokenPath { | ||||||
| 		return s.handleAccountTokenDelete(w, r, v) | 		return s.handleAccountTokenDelete(w, r, v) | ||||||
| 	} else if r.Method == http.MethodGet && r.URL.Path == accountSettingsPath { |  | ||||||
| 		return s.handleAccountSettingsGet(w, r, v) |  | ||||||
| 	} else if r.Method == http.MethodPost && r.URL.Path == accountSettingsPath { | 	} else if r.Method == http.MethodPost && r.URL.Path == accountSettingsPath { | ||||||
| 		return s.handleAccountSettingsChange(w, r, v) | 		return s.handleAccountSettingsChange(w, r, v) | ||||||
| 	} else if r.Method == http.MethodPost && r.URL.Path == accountSubscriptionPath { | 	} else if r.Method == http.MethodPost && r.URL.Path == accountSubscriptionPath { | ||||||
|  |  | ||||||
|  | @ -32,6 +32,52 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||||
|  | 	w.Header().Set("Content-Type", "application/json") | ||||||
|  | 	w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this | ||||||
|  | 	stats, err := v.Stats() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	response := &apiAccountSettingsResponse{ | ||||||
|  | 		Usage: &apiAccountUsageLimits{ | ||||||
|  | 			Basis: "ip", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	if v.user != nil { | ||||||
|  | 		response.Username = v.user.Name | ||||||
|  | 		response.Role = string(v.user.Role) | ||||||
|  | 		if v.user.Prefs != nil { | ||||||
|  | 			if v.user.Prefs.Language != "" { | ||||||
|  | 				response.Language = v.user.Prefs.Language | ||||||
|  | 			} | ||||||
|  | 			if v.user.Prefs.Notification != nil { | ||||||
|  | 				response.Notification = v.user.Prefs.Notification | ||||||
|  | 			} | ||||||
|  | 			if v.user.Prefs.Subscriptions != nil { | ||||||
|  | 				response.Subscriptions = v.user.Prefs.Subscriptions | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if v.user.Plan != nil { | ||||||
|  | 			response.Usage.Basis = "account" | ||||||
|  | 			response.Plan = &apiAccountSettingsPlan{ | ||||||
|  | 				Name:                  v.user.Plan.Name, | ||||||
|  | 				MessagesLimit:         v.user.Plan.MessagesLimit, | ||||||
|  | 				EmailsLimit:           v.user.Plan.EmailsLimit, | ||||||
|  | 				AttachmentsBytesLimit: v.user.Plan.AttachmentBytesLimit, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		response.Username = auth.Everyone | ||||||
|  | 		response.Role = string(auth.RoleAnonymous) | ||||||
|  | 	} | ||||||
|  | 	response.Usage.AttachmentsBytes = stats.VisitorAttachmentBytesUsed | ||||||
|  | 	if err := json.NewEncoder(w).Encode(response); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { | func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||||
| 	if v.user == nil { | 	if v.user == nil { | ||||||
| 		return errHTTPUnauthorized | 		return errHTTPUnauthorized | ||||||
|  | @ -99,36 +145,6 @@ func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *Server) handleAccountSettingsGet(w http.ResponseWriter, r *http.Request, v *visitor) error { |  | ||||||
| 	w.Header().Set("Content-Type", "application/json") |  | ||||||
| 	w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this |  | ||||||
| 	response := &apiAccountSettingsResponse{} |  | ||||||
| 	if v.user != nil { |  | ||||||
| 		response.Username = v.user.Name |  | ||||||
| 		response.Role = string(v.user.Role) |  | ||||||
| 		if v.user.Prefs != nil { |  | ||||||
| 			if v.user.Prefs.Language != "" { |  | ||||||
| 				response.Language = v.user.Prefs.Language |  | ||||||
| 			} |  | ||||||
| 			if v.user.Prefs.Notification != nil { |  | ||||||
| 				response.Notification = v.user.Prefs.Notification |  | ||||||
| 			} |  | ||||||
| 			if v.user.Prefs.Subscriptions != nil { |  | ||||||
| 				response.Subscriptions = v.user.Prefs.Subscriptions |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		response = &apiAccountSettingsResponse{ |  | ||||||
| 			Username: auth.Everyone, |  | ||||||
| 			Role:     string(auth.RoleAnonymous), |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if err := json.NewEncoder(w).Encode(response); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Request, v *visitor) error { | func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||||
| 	if v.user == nil { | 	if v.user == nil { | ||||||
| 		return errors.New("no user") | 		return errors.New("no user") | ||||||
|  |  | ||||||
|  | @ -225,8 +225,17 @@ type apiAccountTokenResponse struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type apiAccountSettingsPlan struct { | type apiAccountSettingsPlan struct { | ||||||
| 	Id   int    `json:"id"` |  | ||||||
| 	Name                  string `json:"name"` | 	Name                  string `json:"name"` | ||||||
|  | 	MessagesLimit         int    `json:"messages_limit"` | ||||||
|  | 	EmailsLimit           int    `json:"emails_limit"` | ||||||
|  | 	AttachmentsBytesLimit int64  `json:"attachments_bytes_limit"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type apiAccountUsageLimits struct { | ||||||
|  | 	Basis            string `json:"basis"` // "ip" or "account" | ||||||
|  | 	Messages         int    `json:"messages"` | ||||||
|  | 	Emails           int    `json:"emails"` | ||||||
|  | 	AttachmentsBytes int64  `json:"attachments_bytes"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type apiAccountSettingsResponse struct { | type apiAccountSettingsResponse struct { | ||||||
|  | @ -236,4 +245,5 @@ type apiAccountSettingsResponse struct { | ||||||
| 	Language      string                      `json:"language,omitempty"` | 	Language      string                      `json:"language,omitempty"` | ||||||
| 	Notification  *auth.UserNotificationPrefs `json:"notification,omitempty"` | 	Notification  *auth.UserNotificationPrefs `json:"notification,omitempty"` | ||||||
| 	Subscriptions []*auth.UserSubscription    `json:"subscriptions,omitempty"` | 	Subscriptions []*auth.UserSubscription    `json:"subscriptions,omitempty"` | ||||||
|  | 	Usage         *apiAccountUsageLimits      `json:"usage,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -175,6 +175,20 @@ class Api { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async getAccount(baseUrl, token) { | ||||||
|  |         const url = accountUrl(baseUrl); | ||||||
|  |         console.log(`[Api] Fetching user account ${url}`); | ||||||
|  |         const response = await fetch(url, { | ||||||
|  |             headers: maybeWithBearerAuth({}, token) | ||||||
|  |         }); | ||||||
|  |         if (response.status !== 200) { | ||||||
|  |             throw new Error(`Unexpected server response ${response.status}`); | ||||||
|  |         } | ||||||
|  |         const account = await response.json(); | ||||||
|  |         console.log(`[Api] Account`, account); | ||||||
|  |         return account; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async deleteAccount(baseUrl, token) { |     async deleteAccount(baseUrl, token) { | ||||||
|         const url = accountUrl(baseUrl); |         const url = accountUrl(baseUrl); | ||||||
|         console.log(`[Api] Deleting user account ${url}`); |         console.log(`[Api] Deleting user account ${url}`); | ||||||
|  | @ -202,20 +216,6 @@ class Api { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getAccountSettings(baseUrl, token) { |  | ||||||
|         const url = accountSettingsUrl(baseUrl); |  | ||||||
|         console.log(`[Api] Fetching user account ${url}`); |  | ||||||
|         const response = await fetch(url, { |  | ||||||
|             headers: maybeWithBearerAuth({}, token) |  | ||||||
|         }); |  | ||||||
|         if (response.status !== 200) { |  | ||||||
|             throw new Error(`Unexpected server response ${response.status}`); |  | ||||||
|         } |  | ||||||
|         const account = await response.json(); |  | ||||||
|         console.log(`[Api] Account`, account); |  | ||||||
|         return account; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async updateAccountSettings(baseUrl, token, payload) { |     async updateAccountSettings(baseUrl, token, payload) { | ||||||
|         const url = accountSettingsUrl(baseUrl); |         const url = accountSettingsUrl(baseUrl); | ||||||
|         const body = JSON.stringify(payload); |         const body = JSON.stringify(payload); | ||||||
|  |  | ||||||
|  | @ -52,6 +52,8 @@ const Basics = () => { | ||||||
| const Stats = () => { | const Stats = () => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const { account } = useOutletContext(); |     const { account } = useOutletContext(); | ||||||
|  |     const admin = account?.role === "admin" | ||||||
|  |     const accountType = account?.plan?.name ?? "Free"; | ||||||
|     return ( |     return ( | ||||||
|         <Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}> |         <Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}> | ||||||
|             <Typography variant="h5" sx={{marginBottom: 2}}> |             <Typography variant="h5" sx={{marginBottom: 2}}> | ||||||
|  | @ -62,7 +64,7 @@ const Stats = () => { | ||||||
|                     <div> |                     <div> | ||||||
|                         {account?.role === "admin" |                         {account?.role === "admin" | ||||||
|                             ? <>Unlimited <Tooltip title={"You are Admin"}><span style={{cursor: "default"}}>👑</span></Tooltip></> |                             ? <>Unlimited <Tooltip title={"You are Admin"}><span style={{cursor: "default"}}>👑</span></Tooltip></> | ||||||
|                             : "Free"} |                             : accountType} | ||||||
|                     </div> |                     </div> | ||||||
|                 </Pref> |                 </Pref> | ||||||
|                 <Pref labelId={"dailyMessages"} title={t("Daily messages")}> |                 <Pref labelId={"dailyMessages"} title={t("Daily messages")}> | ||||||
|  |  | ||||||
|  | @ -96,7 +96,7 @@ const Layout = () => { | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         (async () => { |         (async () => { | ||||||
|             const acc = await api.getAccountSettings("http://localhost:2586", session.token()); |             const acc = await api.getAccount("http://localhost:2586", session.token()); | ||||||
|             if (acc) { |             if (acc) { | ||||||
|                 setAccount(acc); |                 setAccount(acc); | ||||||
|                 if (acc.language) { |                 if (acc.language) { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue