Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ type User struct {
UpdatedTs int64 `json:"updatedTs"`

// Domain specific fields
Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"`
PasswordHash string `json:"-"`
OpenID string `json:"openId"`
Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"`
PasswordHash string `json:"-"`
OpenID string `json:"openId"`
UserSettingList []*UserSetting `json:"userSettingList"`
}

type UserCreate struct {
Expand Down
34 changes: 34 additions & 0 deletions api/user_setting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package api

type UserSettingKey string

const (
// UserSettingLocaleKey is the key type for user locale
UserSettingLocaleKey UserSettingKey = "locale"
)

// String returns the string format of UserSettingKey type.
func (key UserSettingKey) String() string {
switch key {
case UserSettingLocaleKey:
return "locale"
}
return ""
}

type UserSetting struct {
UserID int
Key UserSettingKey `json:"key"`
// Value is a JSON string with basic value
Value string `json:"value"`
}

type UserSettingUpsert struct {
UserID int
Key UserSettingKey `json:"key"`
Value string `json:"value"`
}

type UserSettingFind struct {
UserID int
}
2 changes: 1 addition & 1 deletion docker-compose.yaml → quickstart/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ services:
- "5230:5230"
volumes:
- ~/.memos/:/var/opt/memos
command: --mode=prod --port=5230
command: --mode=prod --port=5230
69 changes: 53 additions & 16 deletions server/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,29 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil
})

g.GET("/user/:id", func(c echo.Context) error {
// GET /api/user/me is used to check if the user is logged in.
g.GET("/user/me", func(c echo.Context) error {
ctx := c.Request().Context()
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err)
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}

user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &id,
})
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}

if user != nil {
// data desensitize
user.OpenID = ""
userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{
UserID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
}
user.UserSettingList = userSettingList

c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
Expand All @@ -84,22 +89,54 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil
})

// GET /api/user/me is used to check if the user is logged in.
g.GET("/user/me", func(c echo.Context) error {
g.POST("/user/setting", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}

userFind := &api.UserFind{
ID: &userID,
userSettingUpsert := &api.UserSettingUpsert{}
if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err)
}
user, err := s.Store.FindUser(ctx, userFind)

if userSettingUpsert.Key.String() == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user setting key")
}

userSettingUpsert.UserID = userID
userSetting, err := s.Store.UpsertUserSetting(ctx, userSettingUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err)
}

c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userSetting)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user setting response").SetInternal(err)
}
return nil
})

g.GET("/user/:id", func(c echo.Context) error {
ctx := c.Request().Context()
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err)
}

user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &id,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err)
}

if user != nil {
// data desensitize
user.OpenID = ""
}

c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
Expand Down
11 changes: 11 additions & 0 deletions store/db/migration/dev/LATEST__SCHEMA.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ DROP TABLE IF EXISTS `memo_organizer`;
DROP TABLE IF EXISTS `memo`;
DROP TABLE IF EXISTS `shortcut`;
DROP TABLE IF EXISTS `resource`;
DROP TABLE IF EXISTS `user_setting`;
DROP TABLE IF EXISTS `user`;

-- user
Expand Down Expand Up @@ -139,3 +140,13 @@ SET
WHERE
rowid = old.rowid;
END;

-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);

CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
9 changes: 9 additions & 0 deletions store/db/migration/prod/0.4/00__user_setting.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);

CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
122 changes: 122 additions & 0 deletions store/user_setting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package store

import (
"context"
"database/sql"

"github.com/usememos/memos/api"
)

type userSettingRaw struct {
UserID int
Key api.UserSettingKey
Value string
}

func (raw *userSettingRaw) toUserSetting() *api.UserSetting {
return &api.UserSetting{
UserID: raw.UserID,
Key: raw.Key,
Value: raw.Value,
}
}

func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUpsert) (*api.UserSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()

userSettingRaw, err := upsertUserSetting(ctx, tx, upsert)
if err != nil {
return nil, err
}

if err := tx.Commit(); err != nil {
return nil, err
}

userSetting := userSettingRaw.toUserSetting()

return userSetting, nil
}

func (s *Store) FindUserSettingList(ctx context.Context, find *api.UserSettingFind) ([]*api.UserSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()

userSettingRawList, err := findUserSettingList(ctx, tx, find)
if err != nil {
return nil, err
}

list := []*api.UserSetting{}
for _, raw := range userSettingRawList {
list = append(list, raw.toUserSetting())
}

return list, nil
}

func upsertUserSetting(ctx context.Context, tx *sql.Tx, upsert *api.UserSettingUpsert) (*userSettingRaw, error) {
query := `
INSERT INTO user_setting (
user_id, key, value
)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE
SET
value = EXCLUDED.value
RETURNING user_id, key, value
`
var userSettingRaw userSettingRaw
if err := tx.QueryRowContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value).Scan(
&userSettingRaw.UserID,
&userSettingRaw.Key,
&userSettingRaw.Value,
); err != nil {
return nil, FormatError(err)
}

return &userSettingRaw, nil
}

func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingFind) ([]*userSettingRaw, error) {
query := `
SELECT
user_id,
key,
value
FROM user_setting
WHERE user_id = ?
`
rows, err := tx.QueryContext(ctx, query, find.UserID)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()

userSettingRawList := make([]*userSettingRaw, 0)
for rows.Next() {
var userSettingRaw userSettingRaw
if err := rows.Scan(
&userSettingRaw.UserID,
&userSettingRaw.Key,
&userSettingRaw.Value,
); err != nil {
return nil, FormatError(err)
}

userSettingRawList = append(userSettingRawList, &userSettingRaw)
}

if err := rows.Err(); err != nil {
return nil, FormatError(err)
}

return userSettingRawList, nil
}
17 changes: 15 additions & 2 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useEffect, useState } from "react";
import useI18n from "./hooks/useI18n";
import { appRouterSwitch } from "./routers";
import { locationService } from "./services";
import { globalService, locationService } from "./services";
import { useAppSelector } from "./store";
import * as storage from "./helpers/storage";

function App() {
const { setLocale } = useI18n();
const global = useAppSelector((state) => state.global);
const pathname = useAppSelector((state) => state.location.pathname);
const [isLoading, setLoading] = useState(true);

Expand All @@ -12,9 +16,18 @@ function App() {
window.onpopstate = () => {
locationService.updateStateWithLocation();
};
setLoading(false);
globalService.initialState().then(() => {
setLoading(false);
});
}, []);

useEffect(() => {
setLocale(global.locale);
storage.set({
locale: global.locale,
});
}, [global]);

return <>{isLoading ? null : appRouterSwitch(pathname)}</>;
}

Expand Down
8 changes: 2 additions & 6 deletions web/src/components/AboutSiteDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import "../less/about-site-dialog.less";
interface Props extends DialogProps {}

const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
const { t } = useI18n();
const [profile, setProfile] = useState<Profile>();
const { t, setLocale } = useI18n();

useEffect(() => {
try {
Expand All @@ -27,10 +27,6 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
version: "0.0.0",
});
}

setTimeout(() => {
setLocale("zh");
}, 2333);
}, []);

const handleCloseBtnClick = () => {
Expand All @@ -42,7 +38,7 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
<div className="dialog-header-container">
<p className="title-text">
<span className="icon-text">🤠</span>
{t("about")} <b>Memos</b>
{t("common.about")} <b>Memos</b>
</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X />
Expand Down
Loading