diff --git a/auth/auth.go b/auth/auth.go index 56b6d29e..d813a91f 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -64,6 +64,7 @@ type User struct { Role Role Grants []Grant Prefs *UserPrefs + Plan *UserPlan } type UserPrefs struct { @@ -72,6 +73,13 @@ type UserPrefs struct { 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 { ID string `json:"id"` BaseURL string `json:"base_url"` diff --git a/auth/auth_sqlite.go b/auth/auth_sqlite.go index c007dd7d..31d1fa5b 100644 --- a/auth/auth_sqlite.go +++ b/auth/auth_sqlite.go @@ -23,8 +23,10 @@ const ( BEGIN; CREATE TABLE IF NOT EXISTS plan ( id INT NOT NULL, - name TEXT NOT NULL, - limit_messages INT, + name TEXT NOT NULL, + messages_limit INT NOT NULL, + emails_limit INT NOT NULL, + attachment_bytes_limit INT NOT NULL, PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS user ( @@ -55,20 +57,21 @@ const ( id INT PRIMARY KEY, 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; COMMIT; ` selectUserByNameQuery = ` - SELECT user, pass, role, settings - FROM user - WHERE user = ? + SELECT u.user, u.pass, u.role, u.settings, p.name, p.messages_limit, p.emails_limit, p.attachment_bytes_limit + FROM user u + LEFT JOIN plan p on p.id = u.plan_id + WHERE user = ? ` selectUserByTokenQuery = ` - SELECT user, pass, role, settings - FROM user - JOIN user_token on user.id = user_token.user_id - WHERE token = ? + SELECT u.user, u.pass, u.role, u.settings, p.name, p.messages_limit, p.emails_limit, p.attachment_bytes_limit + FROM user u + JOIN user_token t on u.id = t.user_id + LEFT JOIN plan p on p.id = u.plan_id + WHERE t.token = ? ` selectTopicPermsQuery = ` SELECT read, write @@ -321,11 +324,13 @@ func (a *SQLiteAuthManager) userByToken(token string) (*User, error) { func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) { defer rows.Close() 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() { 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 } else if err := rows.Err(); err != nil { return nil, err @@ -346,6 +351,14 @@ func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) { 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 } diff --git a/server/server.go b/server/server.go index 9801edf6..2b31c457 100644 --- a/server/server.go +++ b/server/server.go @@ -333,6 +333,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.handleUserStats(w, r, v) } else if r.Method == http.MethodPost && r.URL.Path == accountPath { 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 { return s.handleAccountDelete(w, r, v) } 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) } else if r.Method == http.MethodDelete && r.URL.Path == accountTokenPath { 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 { return s.handleAccountSettingsChange(w, r, v) } else if r.Method == http.MethodPost && r.URL.Path == accountSubscriptionPath { diff --git a/server/server_account.go b/server/server_account.go index 7a8f7cca..ac3407f5 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -32,6 +32,52 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * 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 { if v.user == nil { return errHTTPUnauthorized @@ -99,36 +145,6 @@ func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request 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 { if v.user == nil { return errors.New("no user") diff --git a/server/types.go b/server/types.go index 710bf05a..679ca662 100644 --- a/server/types.go +++ b/server/types.go @@ -225,8 +225,17 @@ type apiAccountTokenResponse 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 { @@ -236,4 +245,5 @@ type apiAccountSettingsResponse struct { Language string `json:"language,omitempty"` Notification *auth.UserNotificationPrefs `json:"notification,omitempty"` Subscriptions []*auth.UserSubscription `json:"subscriptions,omitempty"` + Usage *apiAccountUsageLimits `json:"usage,omitempty"` } diff --git a/web/src/app/Api.js b/web/src/app/Api.js index 36a81545..3d753b8f 100644 --- a/web/src/app/Api.js +++ b/web/src/app/Api.js @@ -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) { const url = accountUrl(baseUrl); 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) { const url = accountSettingsUrl(baseUrl); const body = JSON.stringify(payload); diff --git a/web/src/components/Account.js b/web/src/components/Account.js index b5ca6a80..ab5edede 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -52,6 +52,8 @@ const Basics = () => { const Stats = () => { const { t } = useTranslation(); const { account } = useOutletContext(); + const admin = account?.role === "admin" + const accountType = account?.plan?.name ?? "Free"; return ( @@ -62,7 +64,7 @@ const Stats = () => {
{account?.role === "admin" ? <>Unlimited 👑 - : "Free"} + : accountType}
diff --git a/web/src/components/App.js b/web/src/components/App.js index 971b9f93..eb2fba2f 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -96,7 +96,7 @@ const Layout = () => { useEffect(() => { (async () => { - const acc = await api.getAccountSettings("http://localhost:2586", session.token()); + const acc = await api.getAccount("http://localhost:2586", session.token()); if (acc) { setAccount(acc); if (acc.language) {