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…
Reference in a new issue