Plan stuff WIPWIPWIP

This commit is contained in:
binwiederhier 2022-12-17 15:17:52 -05:00
parent 8752680233
commit ac56fa36ba
8 changed files with 111 additions and 62 deletions

View file

@ -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"`

View file

@ -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
} }

View file

@ -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 {

View file

@ -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")

View file

@ -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"`
} }

View file

@ -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);

View file

@ -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")}>

View file

@ -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) {