Commit d661134b authored by email's avatar email

refactor: backend

parent 3d8997a4
......@@ -17,18 +17,16 @@ COPY . .
RUN go build \
-o memos \
./server/main.go
./bin/server/main.go
# Make workspace with above generated files.
FROM alpine:3.14.3 AS monolithic
WORKDIR /usr/local/memos
RUN apk add --no-cache tzdata
ENV TZ="Asia/Shanghai"
# RUN apk add --no-cache tzdata
# ENV TZ="Asia/Shanghai"
COPY --from=backend /backend-build/memos /usr/local/memos/
# Copy default resources, like db file.
COPY --from=backend /backend-build/resources /usr/local/memos/resources
COPY --from=frontend /frontend-build/dist /usr/local/memos/web/dist
CMD ["./memos"]
......
package api
import (
"encoding/json"
"memos/api/e"
"memos/store"
"net/http"
"github.com/gorilla/mux"
)
func handleUserSignUp(w http.ResponseWriter, r *http.Request) {
type UserSignUpDataBody struct {
Username string `json:"username"`
Password string `json:"password"`
}
userSignup := UserSignUpDataBody{}
err := json.NewDecoder(r.Body).Decode(&userSignup)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
usernameUsable, _ := store.CheckUsernameUsable(userSignup.Username)
if !usernameUsable {
json.NewEncoder(w).Encode(Response{
Succeed: false,
Message: "Username is existed",
Data: nil,
})
return
}
user, err := store.CreateNewUser(userSignup.Username, userSignup.Password)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
session, _ := SessionStore.Get(r, "session")
session.Values["user_id"] = user.Id
session.Save(r, w)
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: user,
})
type Login struct {
Name string
Password string
}
func handleUserSignIn(w http.ResponseWriter, r *http.Request) {
type UserSigninDataBody struct {
Username string `json:"username"`
Password string `json:"password"`
}
userSignin := UserSigninDataBody{}
err := json.NewDecoder(r.Body).Decode(&userSignin)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
user, err := store.GetUserByUsernameAndPassword(userSignin.Username, userSignin.Password)
if err != nil {
json.NewEncoder(w).Encode(Response{
Succeed: false,
Message: "Username and password not allowed",
Data: nil,
})
return
}
session, _ := SessionStore.Get(r, "session")
session.Values["user_id"] = user.Id
session.Save(r, w)
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: user,
})
}
func handleUserSignOut(w http.ResponseWriter, r *http.Request) {
session, _ := SessionStore.Get(r, "session")
session.Values["user_id"] = ""
session.Save(r, w)
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: nil,
})
}
func RegisterAuthRoutes(r *mux.Router) {
authRouter := r.PathPrefix("/api/auth").Subrouter()
authRouter.Use(JSONResponseMiddleWare)
authRouter.HandleFunc("/signup", handleUserSignUp).Methods("POST")
authRouter.HandleFunc("/signin", handleUserSignIn).Methods("POST")
authRouter.HandleFunc("/signout", handleUserSignOut).Methods("POST")
type Signup struct {
Name string
Password string
}
package e
var Codes = map[string]int{
"NOT_AUTH": 20001,
"REQUEST_BODY_ERROR": 40001,
"UPLOAD_FILE_ERROR": 40002,
"OVERLOAD_MAX_SIZE": 40003,
"NOT_FOUND": 40400,
"USER_NOT_FOUND": 40401,
"RESOURCE_NOT_FOUND": 40402,
"DATABASE_ERROR": 50001,
}
package e
import (
"encoding/json"
"net/http"
)
type ServerError struct {
Code int
Message string
}
type ErrorResponse struct {
Succeed bool `json:"succeed"`
Message string `json:"message"`
StatusCode int `json:"statusCode"`
Data interface{} `json:"data"`
}
func getServerError(err string) ServerError {
code, exists := Codes[err]
println(err)
if !exists {
err = "BAD_REQUEST"
code = 40000
}
return ServerError{
Code: code,
Message: err,
}
}
func ErrorHandler(w http.ResponseWriter, err string, message string) {
serverError := getServerError(err)
res := ErrorResponse{
Succeed: false,
Message: message,
StatusCode: serverError.Code,
Data: nil,
}
statusCode := int(serverError.Code / 100)
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(res)
}
package api
import (
"encoding/json"
"memos/api/e"
"memos/store"
"net/http"
"github.com/gorilla/mux"
)
func handleGetMyMemos(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r)
urlParams := r.URL.Query()
deleted := urlParams.Get("deleted")
onlyDeletedFlag := deleted == "true"
memos, err := store.GetMemosByUserId(userId, onlyDeletedFlag)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: memos,
})
type Memo struct {
Id int `jsonapi:"primary,memo"`
CreatedTs int64 `jsonapi:"attr,createdTs"`
UpdatedTs int64 `jsonapi:"attr,updatedTs"`
RowStatus string `jsonapi:"attr,rowStatus"`
Content string `jsonapi:"attr,content"`
CreatorId int `jsonapi:"attr,creatorId"`
}
func handleCreateMemo(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r)
type CreateMemoDataBody struct {
Content string `json:"content"`
}
createMemo := CreateMemoDataBody{}
err := json.NewDecoder(r.Body).Decode(&createMemo)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
memo, err := store.CreateNewMemo(createMemo.Content, userId)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: memo,
})
type MemoCreate struct {
Content string `jsonapi:"attr,content"`
CreatorId int
}
func handleUpdateMemo(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
memoId := vars["id"]
memoPatch := store.MemoPatch{}
err := json.NewDecoder(r.Body).Decode(&memoPatch)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
memo, err := store.UpdateMemo(memoId, &memoPatch)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
type MemoPatch struct {
Id int
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: memo,
})
Content *string
RowStatus *string
}
func handleDeleteMemo(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
memoId := vars["id"]
err := store.DeleteMemo(memoId)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: nil,
})
type MemoFind struct {
Id *int
CreatorId *int
}
func RegisterMemoRoutes(r *mux.Router) {
memoRouter := r.PathPrefix("/api/memo").Subrouter()
memoRouter.Use(JSONResponseMiddleWare)
memoRouter.Use(AuthCheckerMiddleWare)
type MemoDelete struct {
Id *int `jsonapi:"primary,memo"`
CreatorId *int
}
memoRouter.HandleFunc("/all", handleGetMyMemos).Methods("GET")
memoRouter.HandleFunc("/", handleCreateMemo).Methods("PUT")
memoRouter.HandleFunc("/{id}", handleUpdateMemo).Methods("PATCH")
memoRouter.HandleFunc("/{id}", handleDeleteMemo).Methods("DELETE")
type MemoService interface {
CreateMemo(create *MemoCreate) (*Memo, error)
PatchMemo(patch *MemoPatch) (*Memo, error)
FindMemoList(find *MemoFind) ([]*Memo, error)
FindMemo(find *MemoFind) (*Memo, error)
DeleteMemo(delete *MemoDelete) error
}
package api
import (
"memos/api/e"
"net/http"
)
func AuthCheckerMiddleWare(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, _ := SessionStore.Get(r, "session")
if userId, ok := session.Values["user_id"].(string); !ok || userId == "" {
e.ErrorHandler(w, "NOT_AUTH", "Need authorize")
return
}
next.ServeHTTP(w, r)
})
}
func JSONResponseMiddleWare(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
func CorsMiddleWare(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
next.ServeHTTP(w, r)
})
}
package api
import (
"encoding/json"
"memos/api/e"
"memos/store"
"net/http"
"github.com/gorilla/mux"
)
func handleGetMyQueries(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r)
queries, err := store.GetQueriesByUserId(userId)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: queries,
})
}
func handleCreateQuery(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r)
type CreateQueryDataBody struct {
Title string `json:"title"`
Querystring string `json:"querystring"`
}
queryData := CreateQueryDataBody{}
err := json.NewDecoder(r.Body).Decode(&queryData)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
query, err := store.CreateNewQuery(queryData.Title, queryData.Querystring, userId)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: query,
})
}
func handleUpdateQuery(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
queryId := vars["id"]
queryPatch := store.QueryPatch{}
err := json.NewDecoder(r.Body).Decode(&queryPatch)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
query, err := store.UpdateQuery(queryId, &queryPatch)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: query,
})
}
func handleDeleteQuery(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
queryId := vars["id"]
err := store.DeleteQuery(queryId)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: nil,
})
}
func RegisterQueryRoutes(r *mux.Router) {
queryRouter := r.PathPrefix("/api/query").Subrouter()
queryRouter.Use(JSONResponseMiddleWare)
queryRouter.Use(AuthCheckerMiddleWare)
queryRouter.HandleFunc("/all", handleGetMyQueries).Methods("GET")
queryRouter.HandleFunc("/", handleCreateQuery).Methods("PUT")
queryRouter.HandleFunc("/{id}", handleUpdateQuery).Methods("PATCH")
queryRouter.HandleFunc("/{id}", handleDeleteQuery).Methods("DELETE")
}
package api
import (
"encoding/json"
"io/ioutil"
"memos/api/e"
"memos/store"
"net/http"
"strings"
type Resource struct {
Id int `jsonapi:"primary,resource"`
CreatedTs int64 `jsonapi:"attr,createdTs"`
UpdatedTs int64 `jsonapi:"attr,updatedTs"`
"github.com/gorilla/mux"
)
Filename string `jsonapi:"attr,filename"`
Blob []byte `jsonapi:"attr,blob"`
Type string `jsonapi:"attr,type"`
Size int64 `jsonapi:"attr,size"`
func handleGetMyResources(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r)
resources, err := store.GetResourcesByUserId(userId)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: resources,
})
CreatorId int `jsonapi:"attr,creatorId"`
}
func handleUploadResource(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r)
err := r.ParseMultipartForm(5 << 20)
if err != nil {
e.ErrorHandler(w, "OVERLOAD_MAX_SIZE", "The max size of resource is 5Mb.")
return
}
file, handler, err := r.FormFile("file")
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
defer file.Close()
filename := handler.Filename
filetype := handler.Header.Get("Content-Type")
size := handler.Size
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
e.ErrorHandler(w, "UPLOAD_FILE_ERROR", "Read file error")
return
}
resource, err := store.CreateResource(userId, filename, fileBytes, filetype, size)
type ResourceCreate struct {
Filename string `jsonapi:"attr,filename"`
Blob []byte `jsonapi:"attr,blob"`
Type string `jsonapi:"attr,type"`
Size int64 `jsonapi:"attr,size"`
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "Upload file succeed",
Data: resource,
})
CreatorId int `jsonapi:"attr,creatorId"`
}
func handleDeleteResource(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
resourceId := vars["id"]
err := store.DeleteResourceById(resourceId)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: nil,
})
type ResourceFind struct {
Id *int
CreatorId *int
Filename *string
}
func handleGetResource(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
resourceId := vars["id"]
filename := vars["filename"]
etag := `"` + resourceId + "/" + filename + `"`
w.Header().Set("Etag", etag)
w.Header().Set("Cache-Control", "max-age=2592000")
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
w.WriteHeader(http.StatusNotModified)
return
}
}
resource, err := store.GetResourceByIdAndFilename(resourceId, filename)
if err != nil {
e.ErrorHandler(w, "RESOURCE_NOT_FOUND", err.Error())
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(resource.Blob)
type ResourceDelete struct {
Id int
}
func RegisterResourceRoutes(r *mux.Router) {
resourceRouter := r.PathPrefix("/").Subrouter()
resourceRouter.Use(AuthCheckerMiddleWare)
resourceRouter.HandleFunc("/api/resource/all", handleGetMyResources).Methods("GET")
resourceRouter.HandleFunc("/api/resource/", handleUploadResource).Methods("PUT")
resourceRouter.HandleFunc("/api/resource/{id}", handleDeleteResource).Methods("DELETE")
resourceRouter.HandleFunc("/r/{id}/{filename}", handleGetResource).Methods("GET")
type ResourceService interface {
CreateResource(create *ResourceCreate) (*Resource, error)
FindResourceList(find *ResourceFind) ([]*Resource, error)
FindResource(find *ResourceFind) (*Resource, error)
DeleteResource(delete *ResourceDelete) error
}
package api
import (
"memos/utils"
"github.com/gorilla/sessions"
)
var SessionStore = sessions.NewCookieStore([]byte(utils.GenUUID()))
package api
type Shortcut struct {
Id int `jsonapi:"primary,shortcut"`
CreatedTs int64 `jsonapi:"attr,createdTs"`
UpdatedTs int64 `jsonapi:"attr,updatedTs"`
Title string `jsonapi:"attr,title"`
Payload string `jsonapi:"attr,payload"`
PinnedTs int64 `jsonapi:"attr,pinnedTs"`
CreatorId int
}
type ShortcutCreate struct {
// Standard fields
CreatorId int
// Domain specific fields
Title string `jsonapi:"attr,title"`
Payload string `jsonapi:"attr,payload"`
}
type ShortcutPatch struct {
Id int
Title *string `jsonapi:"attr,title"`
Payload *string `jsonapi:"attr,payload"`
PinnedTs *int64
Pinned *bool `jsonapi:"attr,pinned"`
}
type ShortcutFind struct {
Id *int
// Standard fields
CreatorId *int
// Domain specific fields
Title *string `jsonapi:"attr,title"`
}
type ShortcutDelete struct {
Id int
}
type ShortcutService interface {
CreateShortcut(create *ShortcutCreate) (*Shortcut, error)
PatchShortcut(patch *ShortcutPatch) (*Shortcut, error)
FindShortcutList(find *ShortcutFind) ([]*Shortcut, error)
FindShortcut(find *ShortcutFind) (*Shortcut, error)
DeleteShortcut(delete *ShortcutDelete) error
}
package api
import (
"net/http"
"os"
"path/filepath"
)
type SPAHandler struct {
StaticPath string
IndexPath string
}
func (h SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path, err := filepath.Abs(r.URL.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
path = filepath.Join(h.StaticPath, path)
_, err = os.Stat(path)
if os.IsNotExist(err) {
// file does not exist, serve index.html
http.ServeFile(w, r, filepath.Join(h.StaticPath, h.IndexPath))
return
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.FileServer(http.Dir(h.StaticPath)).ServeHTTP(w, r)
}
package api
import (
"encoding/json"
"memos/api/e"
"memos/store"
"net/http"
"github.com/gorilla/mux"
)
func handleGetMyUserInfo(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r)
user, err := store.GetUserById(userId)
if err != nil {
e.ErrorHandler(w, "USER_NOT_FOUND", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: user,
})
type User struct {
Id int `jsonapi:"primary,user"`
CreatedTs int64 `jsonapi:"attr,createdTs"`
UpdatedTs int64 `jsonapi:"attr,updatedTs"`
Name string `jsonapi:"attr,name"`
Password string
OpenId string `jsonapi:"attr,openId"`
}
func handleUpdateMyUserInfo(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r)
updateUserPatch := store.UpdateUserPatch{}
err := json.NewDecoder(r.Body).Decode(&updateUserPatch)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
if updateUserPatch.Username != nil {
usernameUsable, _ := store.CheckUsernameUsable(*updateUserPatch.Username)
if !usernameUsable {
json.NewEncoder(w).Encode(Response{
Succeed: false,
Message: "Username is existed",
Data: nil,
})
return
}
}
user, err := store.UpdateUser(userId, &updateUserPatch)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: user,
})
type UserCreate struct {
Name string `jsonapi:"attr,name"`
Password string `jsonapi:"attr,password"`
OpenId string `jsonapi:"attr,openId"`
}
func handleResetUserOpenId(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r)
openId, err := store.ResetUserOpenId(userId)
type UserPatch struct {
Id int
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
Name *string `jsonapi:"attr,name"`
Password *string `jsonapi:"attr,password"`
OpenId *string
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: openId,
})
ResetOpenId *bool `jsonapi:"attr,resetOpenId"`
}
func handleCheckUsername(w http.ResponseWriter, r *http.Request) {
type CheckUsernameDataBody struct {
Username string
}
checkUsername := CheckUsernameDataBody{}
err := json.NewDecoder(r.Body).Decode(&checkUsername)
type UserFind struct {
Id *int `jsonapi:"attr,id"`
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
usable, err := store.CheckUsernameUsable(checkUsername.Username)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: usable,
})
}
func handleValidPassword(w http.ResponseWriter, r *http.Request) {
type ValidPasswordDataBody struct {
Password string
}
userId, _ := GetUserIdInSession(r)
validPassword := ValidPasswordDataBody{}
err := json.NewDecoder(r.Body).Decode(&validPassword)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
valid, err := store.CheckPasswordValid(userId, validPassword.Password)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: valid,
})
Name *string `jsonapi:"attr,name"`
OpenId *string
}
func RegisterUserRoutes(r *mux.Router) {
userRouter := r.PathPrefix("/api/user").Subrouter()
userRouter.Use(JSONResponseMiddleWare)
userRouter.Use(AuthCheckerMiddleWare)
userRouter.HandleFunc("/me", handleGetMyUserInfo).Methods("GET")
userRouter.HandleFunc("/me", handleUpdateMyUserInfo).Methods("PATCH")
userRouter.HandleFunc("/open_id/new", handleResetUserOpenId).Methods("POST")
userRouter.HandleFunc("/checkusername", handleCheckUsername).Methods("POST")
userRouter.HandleFunc("/validpassword", handleValidPassword).Methods("POST")
type UserService interface {
CreateUser(create *UserCreate) (*User, error)
PatchUser(patch *UserPatch) (*User, error)
FindUser(find *UserFind) (*User, error)
}
package api
import (
"net/http"
)
type Response struct {
Succeed bool `json:"succeed"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
func GetUserIdInSession(r *http.Request) (string, error) {
session, _ := SessionStore.Get(r, "session")
userId, ok := session.Values["user_id"].(string)
if !ok {
return "", http.ErrNoCookie
}
return userId, nil
}
package api
import (
"encoding/json"
"memos/api/e"
"memos/store"
"net/http"
"github.com/gorilla/mux"
)
func handleCreateMemoByWH(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
openId := vars["openId"]
type CreateMemoDataBody struct {
Content string `json:"content"`
}
createMemo := CreateMemoDataBody{}
err := json.NewDecoder(r.Body).Decode(&createMemo)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
user, err := store.GetUserByOpenId(openId)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
memo, err := store.CreateNewMemo(createMemo.Content, user.Id)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: memo,
})
}
func RegisterWebHooksRoutes(r *mux.Router) {
memoRouter := r.PathPrefix("/api/whs").Subrouter()
memoRouter.Use(JSONResponseMiddleWare)
memoRouter.HandleFunc("/memo/{openId}", handleCreateMemoByWH).Methods("POST")
}
//go:build !release
// +build !release
package cmd
import (
"fmt"
)
// GetDevProfile will return a profile for dev.
func GetDevProfile(dataDir string) Profile {
return Profile{
mode: "8080",
port: 1234,
dsn: fmt.Sprintf("file:%s/memos_dev.db", dataDir),
}
}
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"memos/server"
"memos/store"
)
var (
dataDir string
)
type Profile struct {
// mode can be "release" or "dev"
mode string
// port is the binding port for server.
port int
// dsn points to where Memos stores its own data
dsn string
}
func checkDataDir() error {
// Convert to absolute path if relative path is supplied.
if !filepath.IsAbs(dataDir) {
absDir, err := filepath.Abs(filepath.Dir(os.Args[0]) + "/" + dataDir)
if err != nil {
return err
}
dataDir = absDir
}
// Trim trailing / in case user supplies
dataDir = strings.TrimRight(dataDir, "/")
if _, err := os.Stat(dataDir); err != nil {
error := fmt.Errorf("unable to access --data %s, %w", dataDir, err)
return error
}
return nil
}
type Main struct {
profile *Profile
server *server.Server
db *store.DB
}
func Execute() {
err := checkDataDir()
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}
m := Main{}
profile := GetDevProfile(dataDir)
m.profile = &profile
err = m.Run()
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}
}
func (m *Main) Run() error {
db := store.NewDB(m.profile.dsn)
if err := db.Open(); err != nil {
return fmt.Errorf("cannot open db: %w", err)
}
m.db = db
s := server.NewServer()
s.ShortcutService = store.NewShortcutService(db)
s.MemoService = store.NewMemoService(db)
s.UserService = store.NewUserService(db)
m.server = s
if err := s.Run(); err != nil {
return err
}
return nil
}
package main
import "memos/bin/server/cmd"
func main() {
cmd.Execute()
}
package common
import (
"errors"
)
// Code is the error code.
type Code int
// Application error codes.
const (
// 0 ~ 99 general error
Ok Code = 0
Internal Code = 1
NotAuthorized Code = 2
Invalid Code = 3
NotFound Code = 4
Conflict Code = 5
NotImplemented Code = 6
// 101 ~ 199 db error
DbConnectionFailure Code = 101
DbStatementSyntaxError Code = 102
DbExecutionError Code = 103
// 201 db migration error
// Db migration is a core feature, so we separate it from the db error
MigrationSchemaMissing Code = 201
MigrationAlreadyApplied Code = 202
MigrationOutOfOrder Code = 203
MigrationBaselineMissing Code = 204
// 301 task error
TaskTimingNotAllowed Code = 301
// 10001 advisor error code
CompatibilityDropDatabase Code = 10001
CompatibilityRenameTable Code = 10002
CompatibilityDropTable Code = 10003
CompatibilityRenameColumn Code = 10004
CompatibilityDropColumn Code = 10005
CompatibilityAddPrimaryKey Code = 10006
CompatibilityAddUniqueKey Code = 10007
CompatibilityAddForeignKey Code = 10008
CompatibilityAddCheck Code = 10009
CompatibilityAlterCheck Code = 10010
CompatibilityAlterColumn Code = 10011
)
// Error represents an application-specific error. Application errors can be
// unwrapped by the caller to extract out the code & message.
//
// Any non-application error (such as a disk error) should be reported as an
// Internal error and the human user should only see "Internal error" as the
// message. These low-level internal error details should only be logged and
// reported to the operator of the application (not the end user).
type Error struct {
// Machine-readable error code.
Code Code
// Embedded error.
Err error
}
// Error implements the error interface. Not used by the application otherwise.
func (e *Error) Error() string {
return e.Err.Error()
}
// ErrorCode unwraps an application error and returns its code.
// Non-application errors always return EINTERNAL.
func ErrorCode(err error) Code {
var e *Error
if err == nil {
return Ok
} else if errors.As(err, &e) {
return e.Code
}
return Internal
}
// ErrorMessage unwraps an application error and returns its message.
// Non-application errors always return "Internal error".
func ErrorMessage(err error) string {
var e *Error
if err == nil {
return ""
} else if errors.As(err, &e) {
return e.Err.Error()
}
return "Internal error."
}
// Errorf is a helper function to return an Error with a given code and error.
func Errorf(code Code, err error) *Error {
return &Error{
Code: code,
Err: err,
}
}
package common
import (
"strings"
"github.com/google/uuid"
)
// HasPrefixes returns true if the string s has any of the given prefixes.
func HasPrefixes(src string, prefixes ...string) bool {
for _, prefix := range prefixes {
if strings.HasPrefix(src, prefix) {
return true
}
}
return false
}
func GenUUID() string {
return uuid.New().String()
}
......@@ -2,13 +2,29 @@ module memos
go 1.17
require github.com/gorilla/mux v1.8.0
require github.com/mattn/go-sqlite3 v1.14.9
require github.com/google/uuid v1.3.0
require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/labstack/echo/v4 v4.6.3
github.com/labstack/gommon v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 // indirect
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
)
require (
github.com/google/jsonapi v1.0.0
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1
github.com/labstack/echo-contrib v0.12.0
)
This diff is collapsed.
DROP TABLE IF EXISTS `memos`;
DROP TABLE IF EXISTS `queries`;
DROP TABLE IF EXISTS `resources`;
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` TEXT NOT NULL PRIMARY KEY,
`username` TEXT NOT NULL,
`password` TEXT NOT NULL,
`open_id` TEXT NOT NULL DEFAULT '',
`created_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
`updated_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
UNIQUE(`username`, `open_id`)
);
CREATE TABLE `queries` (
`id` TEXT NOT NULL PRIMARY KEY,
`user_id` TEXT NOT NULL,
`title` TEXT NOT NULL,
`querystring` TEXT NOT NULL,
`created_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
`updated_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
`pinned_at` TEXT NOT NULL DEFAULT '',
FOREIGN KEY(`user_id`) REFERENCES `users`(`id`)
);
CREATE TABLE `memos` (
`id` TEXT NOT NULL PRIMARY KEY,
`content` TEXT NOT NULL,
`user_id` TEXT NOT NULL,
`created_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
`updated_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
`deleted_at` TEXT NOT NULL DEFAULT '',
FOREIGN KEY(`user_id`) REFERENCES `users`(`id`)
);
CREATE TABLE `resources` (
`id` TEXT NOT NULL PRIMARY KEY,
`user_id` TEXT NOT NULL,
`filename` TEXT NOT NULL,
`blob` BLOB NOT NULL,
`type` TEXT NOT NULL,
`size` INTEGER NOT NULL DEFAULT 0,
`created_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
FOREIGN KEY(`user_id`) REFERENCES `users`(`id`)
);
INSERT INTO `users`
(`id`, `username`, `password`, `open_id`)
VALUES
('1', 'guest', '123456', 'guest_open_id'),
('2', 'mine', '123456', 'mine_open_id');
INSERT INTO `memos`
(`id`, `content`, `user_id`)
VALUES
('1', '👋 Welcome to memos', '1'),
('2', '👋 Welcome to memos', '2');
......@@ -3,7 +3,7 @@ tmp_dir = ".air"
[build]
bin = "./.air/memos"
cmd = "go build -o ./.air/memos ./server/main.go"
cmd = "go build -o ./.air/memos ./bin/server/main.go"
delay = 1000
exclude_dir = [".air", "web"]
exclude_file = []
......
package server
import (
"fmt"
"memos/api"
"memos/common"
"net/http"
"github.com/google/jsonapi"
"github.com/labstack/echo/v4"
)
func (s *Server) registerAuthRoutes(g *echo.Group) {
g.POST("/auth/login", func(c echo.Context) error {
login := &api.Login{}
if err := jsonapi.UnmarshalPayload(c.Request().Body, login); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted login request").SetInternal(err)
}
userFind := &api.UserFind{
Name: &login.Name,
}
user, err := s.UserService.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to authenticate user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found: %s", login.Name))
}
// Compare the stored hashed password, with the hashed version of the password that was received.
if login.Password != user.Password {
// If the two passwords don't match, return a 401 status.
return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect password").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal create user response").SetInternal(err)
}
setUserSession(c, user)
return nil
})
g.POST("/auth/logout", func(c echo.Context) error {
removeUserSession(c)
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
c.Response().WriteHeader(http.StatusOK)
return nil
})
g.POST("/auth/signup", func(c echo.Context) error {
signup := &api.Signup{}
if err := jsonapi.UnmarshalPayload(c.Request().Body, signup); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted login request").SetInternal(err)
}
userFind := &api.UserFind{
Name: &signup.Name,
}
user, err := s.UserService.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to authenticate user").SetInternal(err)
}
if user != nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Exist user found: %s", signup.Name))
}
userCreate := &api.UserCreate{
Name: signup.Name,
Password: signup.Password,
OpenId: common.GenUUID(),
}
user, err = s.UserService.CreateUser(userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal create user response").SetInternal(err)
}
setUserSession(c, user)
return nil
})
}
package server
import (
"fmt"
"memos/api"
"memos/common"
"net/http"
"strconv"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
var (
userIdContextKey = "user-id"
)
func getUserIdContextKey() string {
return userIdContextKey
}
// Purpose of this cookie is to store the user's id.
func setUserSession(c echo.Context, user *api.User) {
sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 1000 * 3600 * 24 * 30,
HttpOnly: true,
}
sess.Values[userIdContextKey] = strconv.Itoa(user.Id)
sess.Save(c.Request(), c.Response())
}
func removeUserSession(c echo.Context) {
sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 0,
HttpOnly: true,
}
sess.Values[userIdContextKey] = nil
sess.Save(c.Request(), c.Response())
}
// Use session instead of jwt in the initial version
func JWTMiddleware(us api.UserService, next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Skips auth, test
if common.HasPrefixes(c.Path(), "/api/auth", "/api/test") {
return next(c)
}
sess, err := session.Get("session", c)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing session")
}
userId, err := strconv.Atoi(fmt.Sprintf("%v", sess.Values[userIdContextKey]))
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Failed to malformatted user id in the session.")
}
// Even if there is no error, we still need to make sure the user still exists.
principalFind := &api.UserFind{
Id: &userId,
}
user, err := us.FindUser(principalFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to find user ID: %d", userId)).SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Failed to find user ID: %d", userId))
}
// Stores principalID into context.
c.Set(getUserIdContextKey(), userId)
return next(c)
}
}
package main
import (
"memos/api"
"memos/store"
"net/http"
"github.com/gorilla/mux"
)
func main() {
store.InitDBConn()
r := mux.NewRouter().StrictSlash(true)
api.RegisterAuthRoutes(r)
api.RegisterUserRoutes(r)
api.RegisterMemoRoutes(r)
api.RegisterQueryRoutes(r)
api.RegisterResourceRoutes(r)
api.RegisterWebHooksRoutes(r)
webServe := api.SPAHandler{
StaticPath: "./web/dist",
IndexPath: "index.html",
}
r.PathPrefix("/").Handler(webServe)
http.ListenAndServe(":8080", r)
}
package server
import (
"fmt"
"memos/api"
"memos/common"
"net/http"
"strconv"
"github.com/google/jsonapi"
"github.com/labstack/echo/v4"
)
func (s *Server) registerMemoRoutes(g *echo.Group) {
g.POST("/memo", func(c echo.Context) error {
userId := c.Get(getUserIdContextKey()).(int)
memoCreate := &api.MemoCreate{
CreatorId: userId,
}
if err := jsonapi.UnmarshalPayload(c.Request().Body, memoCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
}
memo, err := s.MemoService.CreateMemo(memoCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, memo); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal memo response").SetInternal(err)
}
return nil
})
g.PATCH("/memo/:memoId", func(c echo.Context) error {
memoId, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoPatch := &api.MemoPatch{
Id: memoId,
}
if err := jsonapi.UnmarshalPayload(c.Request().Body, memoPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
}
memo, err := s.MemoService.PatchMemo(memoPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, memo); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal memo response").SetInternal(err)
}
return nil
})
g.GET("/memo", func(c echo.Context) error {
userId := c.Get(getUserIdContextKey()).(int)
memoFind := &api.MemoFind{
CreatorId: &userId,
}
list, err := s.MemoService.FindMemoList(memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, list); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal memo list response").SetInternal(err)
}
return nil
})
g.GET("/memo/:memoId", func(c echo.Context) error {
memoId, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoFind := &api.MemoFind{
Id: &memoId,
}
memo, err := s.MemoService.FindMemo(memoFind)
if err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoId))
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoId)).SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, memo); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal memo response").SetInternal(err)
}
return nil
})
g.DELETE("/memo/:memoId", func(c echo.Context) error {
memoId, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoDelete := &api.MemoDelete{
Id: &memoId,
}
err = s.MemoService.DeleteMemo(memoDelete)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoId)).SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
c.Response().WriteHeader(http.StatusOK)
return nil
})
}
package server
import (
"fmt"
"io/ioutil"
"memos/api"
"net/http"
"strconv"
"github.com/google/jsonapi"
"github.com/labstack/echo/v4"
)
func (s *Server) registerResourceRoutes(g *echo.Group) {
g.POST("/resource", func(c echo.Context) error {
userId := c.Get(getUserIdContextKey()).(int)
err := c.Request().ParseMultipartForm(5 << 20)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
}
file, err := c.FormFile("file")
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
}
filename := file.Filename
filetype := file.Header.Get("Content-Type")
size := file.Size
src, err := file.Open()
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to open file").SetInternal(err)
}
defer src.Close()
blob, err := ioutil.ReadAll(src)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to read file").SetInternal(err)
}
resourceCreate := &api.ResourceCreate{
Filename: filename,
Type: filetype,
Size: size,
Blob: blob,
CreatorId: userId,
}
resource, err := s.ResourceService.CreateResource(resourceCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, resource); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal shortcut response").SetInternal(err)
}
return nil
})
g.GET("/resource", func(c echo.Context) error {
userId := c.Get(getUserIdContextKey()).(int)
resourceFind := &api.ResourceFind{
CreatorId: &userId,
}
list, err := s.ResourceService.FindResourceList(resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, list); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal resource list response").SetInternal(err)
}
return nil
})
g.DELETE("/resource/:resourceId", func(c echo.Context) error {
resourceId, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resourceDelete := &api.ResourceDelete{
Id: resourceId,
}
if err := s.ResourceService.DeleteResource(resourceDelete); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
}
return nil
})
}
package server
import (
"fmt"
"memos/api"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Server struct {
e *echo.Echo
UserService api.UserService
MemoService api.MemoService
ShortcutService api.ShortcutService
ResourceService api.ResourceService
port int
}
func NewServer() *Server {
e := echo.New()
e.Debug = true
e.HideBanner = true
e.HidePort = false
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "web/dist",
Browse: false,
HTML5: true,
}))
e.Use(session.Middleware(sessions.NewCookieStore([]byte("secret"))))
s := &Server{
e: e,
port: 8080,
}
webhookGroup := e.Group("/h")
s.registerWebhookRoutes(webhookGroup)
apiGroup := e.Group("/api")
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return JWTMiddleware(s.UserService, next)
})
s.registerAuthRoutes(apiGroup)
s.registerUserRoutes(apiGroup)
s.registerMemoRoutes(apiGroup)
s.registerShortcutRoutes(apiGroup)
s.registerResourceRoutes(apiGroup)
return s
}
func (server *Server) Run() error {
return server.e.Start(fmt.Sprintf(":%d", server.port))
}
package server
import (
"fmt"
"memos/api"
"net/http"
"strconv"
"time"
"github.com/google/jsonapi"
"github.com/labstack/echo/v4"
)
func (s *Server) registerShortcutRoutes(g *echo.Group) {
g.POST("/shortcut", func(c echo.Context) error {
userId := c.Get(getUserIdContextKey()).(int)
shortcutCreate := &api.ShortcutCreate{
CreatorId: userId,
}
if err := jsonapi.UnmarshalPayload(c.Request().Body, shortcutCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err)
}
shortcut, err := s.ShortcutService.CreateShortcut(shortcutCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, shortcut); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal shortcut response").SetInternal(err)
}
return nil
})
g.PATCH("/shortcut/:shortcutId", func(c echo.Context) error {
shortcutId, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
}
shortcutPatch := &api.ShortcutPatch{
Id: shortcutId,
}
if err := jsonapi.UnmarshalPayload(c.Request().Body, shortcutPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err)
}
if shortcutPatch.Pinned != nil {
pinnedTs := int64(0)
if *shortcutPatch.Pinned {
pinnedTs = time.Now().Unix()
}
shortcutPatch.PinnedTs = &pinnedTs
}
shortcut, err := s.ShortcutService.PatchShortcut(shortcutPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, shortcut); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal shortcut response").SetInternal(err)
}
return nil
})
g.GET("/shortcut", func(c echo.Context) error {
userId := c.Get(getUserIdContextKey()).(int)
shortcutFind := &api.ShortcutFind{
CreatorId: &userId,
}
list, err := s.ShortcutService.FindShortcut(shortcutFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, list); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal shortcut list response").SetInternal(err)
}
return nil
})
g.GET("/shortcut/:shortcutId", func(c echo.Context) error {
shortcutId, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
}
shortcutFind := &api.ShortcutFind{
Id: &shortcutId,
}
shortcut, err := s.ShortcutService.FindShortcut(shortcutFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, shortcut); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal shortcut response").SetInternal(err)
}
return nil
})
g.DELETE("/shortcut/:shortcutId", func(c echo.Context) error {
shortcutId, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
}
shortcutDelete := &api.ShortcutDelete{
Id: shortcutId,
}
if err := s.ShortcutService.DeleteShortcut(shortcutDelete); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete shortcut").SetInternal(err)
}
return nil
})
}
package server
import (
"memos/api"
"memos/common"
"net/http"
"github.com/google/jsonapi"
"github.com/labstack/echo/v4"
)
func (s *Server) registerUserRoutes(g *echo.Group) {
g.GET("/user/me", func(c echo.Context) error {
userId := c.Get(getUserIdContextKey()).(int)
userFind := &api.UserFind{
Id: &userId,
}
user, err := s.UserService.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal user response").SetInternal(err)
}
return nil
})
g.PATCH("user/me", func(c echo.Context) error {
userId := c.Get(getUserIdContextKey()).(int)
userPatch := &api.UserPatch{
Id: userId,
}
if err := jsonapi.UnmarshalPayload(c.Request().Body, userPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
if *userPatch.ResetOpenId {
openId := common.GenUUID()
userPatch.OpenId = &openId
}
user, err := s.UserService.PatchUser(userPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal user response").SetInternal(err)
}
return nil
})
}
package server
import (
"fmt"
"memos/api"
"net/http"
"strconv"
"github.com/google/jsonapi"
"github.com/labstack/echo/v4"
)
func (s *Server) registerWebhookRoutes(g *echo.Group) {
g.GET("/test", func(c echo.Context) error {
return c.HTML(http.StatusOK, "<strong>Hello, World!</strong>")
})
g.POST("/:openId/memo", func(c echo.Context) error {
openId := c.Param("openId")
userFind := &api.UserFind{
OpenId: &openId,
}
user, err := s.UserService.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("User openId not found: %s", openId))
}
memoCreate := &api.MemoCreate{
CreatorId: user.Id,
}
if err := jsonapi.UnmarshalPayload(c.Request().Body, memoCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request by open api").SetInternal(err)
}
memo, err := s.MemoService.CreateMemo(memoCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := jsonapi.MarshalPayload(c.Response().Writer, memo); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal memo response").SetInternal(err)
}
return nil
})
g.GET("r/:resourceId/:filename", func(c echo.Context) error {
resourceId, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
filename := c.Param("filename")
resourceFind := &api.ResourceFind{
Id: &resourceId,
Filename: &filename,
}
resource, err := s.ResourceService.FindResource(resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceId)).SetInternal(err)
}
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", "application/octet-stream")
c.Response().Writer.Write(resource.Blob)
return nil
})
}
package store
import (
"database/sql"
"errors"
"io/ioutil"
"os"
"path/filepath"
_ "github.com/mattn/go-sqlite3"
)
/*
* Use a global variable to save the db connection: Quick and easy to setup.
* Reference: https://techinscribed.com/different-approaches-to-pass-database-connection-into-controllers-in-golang/
*/
var DB *sql.DB
func InitDBConn() {
// mounting point in docker is "/usr/local/memos/data"
dbFilePath := "./data/memos.db"
if _, err := os.Stat(dbFilePath); err != nil {
dbFilePath = "./resources/memos.db"
println("use the default database")
} else {
println("use the custom database")
}
db, err := sql.Open("sqlite3", dbFilePath)
if err != nil {
panic("db connect failed")
} else {
DB = db
println("connect to sqlite succeed")
}
if dbFilePath == "./resources/memos.db" {
resetDataInDefaultDatabase()
}
}
func FormatDBError(err error) error {
if err == nil {
return nil
}
switch err {
case sql.ErrNoRows:
return errors.New("data not found")
default:
return err
}
}
func resetDataInDefaultDatabase() {
initialSQLFilePath := filepath.Join("resources", "initial_db.sql")
c, err := ioutil.ReadFile(initialSQLFilePath)
if err != nil {
// do nth
return
}
sql := string(c)
DB.Exec(sql)
println("Initial data succeed")
}
package store
import (
"memos/utils"
"fmt"
"memos/api"
"memos/common"
"strings"
)
type Memo struct {
Id string `json:"id"`
Content string `json:"content"`
UserId string `json:"userId"`
DeletedAt string `json:"deletedAt"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
type MemoService struct {
db *DB
}
func CreateNewMemo(content string, userId string) (Memo, error) {
nowDateTimeStr := utils.GetNowDateTimeStr()
newMemo := Memo{
Id: utils.GenUUID(),
Content: content,
UserId: userId,
DeletedAt: "",
CreatedAt: nowDateTimeStr,
UpdatedAt: nowDateTimeStr,
}
func NewMemoService(db *DB) *MemoService {
return &MemoService{db: db}
}
query := `INSERT INTO memos (id, content, user_id, deleted_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`
_, err := DB.Exec(query, newMemo.Id, newMemo.Content, newMemo.UserId, newMemo.DeletedAt, newMemo.CreatedAt, newMemo.UpdatedAt)
func (s *MemoService) CreateMemo(create *api.MemoCreate) (*api.Memo, error) {
memo, err := createMemo(s.db, create)
if err != nil {
return nil, err
}
return newMemo, FormatDBError(err)
return memo, nil
}
type MemoPatch struct {
Content *string
DeletedAt *string
func (s *MemoService) PatchMemo(patch *api.MemoPatch) (*api.Memo, error) {
memo, err := patchMemo(s.db, patch)
if err != nil {
return nil, err
}
return memo, nil
}
func UpdateMemo(id string, memoPatch *MemoPatch) (Memo, error) {
memo, _ := GetMemoById(id)
set, args := []string{}, []interface{}{}
func (s *MemoService) FindMemoList(find *api.MemoFind) ([]*api.Memo, error) {
list, err := findMemoList(s.db, find)
if err != nil {
return nil, err
}
return list, nil
}
if v := memoPatch.Content; v != nil {
memo.Content = *v
set, args = append(set, "content=?"), append(args, *v)
func (s *MemoService) FindMemo(find *api.MemoFind) (*api.Memo, error) {
list, err := findMemoList(s.db, find)
if err != nil {
return nil, err
}
if v := memoPatch.DeletedAt; v != nil {
memo.DeletedAt = *v
set, args = append(set, "deleted_at=?"), append(args, *v)
if len(list) == 0 {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
set, args = append(set, "updated_at=?"), append(args, utils.GetNowDateTimeStr())
args = append(args, id)
sqlQuery := `UPDATE memos SET ` + strings.Join(set, ",") + ` WHERE id=?`
_, err := DB.Exec(sqlQuery, args...)
return list[0], nil
}
return memo, FormatDBError(err)
func (s *MemoService) DeleteMemo(delete *api.MemoDelete) error {
err := deleteMemo(s.db, delete)
if err != nil {
return FormatError(err)
}
return nil
}
func DeleteMemo(memoId string) error {
query := `DELETE FROM memos WHERE id=?`
_, err := DB.Exec(query, memoId)
return FormatDBError(err)
func createMemo(db *DB, create *api.MemoCreate) (*api.Memo, error) {
row, err := db.Db.Query(`
INSERT INTO memo (
creator_id,
content
)
VALUES (?, ?)
RETURNING id, creator_id, created_ts, updated_ts, content, row_status
`,
create.CreatorId,
create.Content,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
if !row.Next() {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var memo api.Memo
if err := row.Scan(
&memo.Id,
&memo.CreatorId,
&memo.CreatedTs,
&memo.UpdatedTs,
&memo.Content,
&memo.RowStatus,
); err != nil {
return nil, FormatError(err)
}
return &memo, nil
}
func GetMemoById(id string) (Memo, error) {
query := `SELECT id, content, deleted_at, created_at, updated_at FROM memos WHERE id=?`
memo := Memo{}
err := DB.QueryRow(query, id).Scan(&memo.Id, &memo.Content, &memo.DeletedAt, &memo.CreatedAt, &memo.UpdatedAt)
return memo, FormatDBError(err)
func patchMemo(db *DB, patch *api.MemoPatch) (*api.Memo, error) {
set, args := []string{}, []interface{}{}
if v := patch.Content; v != nil {
set, args = append(set, "content = ?"), append(args, v)
}
if v := patch.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, v)
}
args = append(args, patch.Id)
row, err := db.Db.Query(`
UPDATE memo
SET `+strings.Join(set, ", ")+`
WHERE id = ?
RETURNING id, created_ts, updated_ts, content, row_status
`, args...)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
if !row.Next() {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var memo api.Memo
if err := row.Scan(
&memo.Id,
&memo.CreatedTs,
&memo.UpdatedTs,
&memo.Content,
&memo.RowStatus,
); err != nil {
return nil, FormatError(err)
}
return &memo, nil
}
func GetMemosByUserId(userId string, onlyDeleted bool) ([]Memo, error) {
sqlQuery := `SELECT id, content, deleted_at, created_at, updated_at FROM memos WHERE user_id=?`
func findMemoList(db *DB, find *api.MemoFind) ([]*api.Memo, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if onlyDeleted {
sqlQuery = sqlQuery + ` AND deleted_at!=""`
} else {
sqlQuery = sqlQuery + ` AND deleted_at=""`
if v := find.Id; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := find.CreatorId; v != nil {
where, args = append(where, "creator_id = ?"), append(args, *v)
}
rows, _ := DB.Query(sqlQuery, userId)
rows, err := db.Db.Query(`
SELECT
id,
creator_id,
created_ts,
updated_ts,
content,
row_status
FROM memo
WHERE `+strings.Join(where, " AND "),
args...,
)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
memos := []Memo{}
list := make([]*api.Memo, 0)
for rows.Next() {
memo := Memo{}
rows.Scan(&memo.Id, &memo.Content, &memo.DeletedAt, &memo.CreatedAt, &memo.UpdatedAt)
memos = append(memos, memo)
var memo api.Memo
if err := rows.Scan(
&memo.Id,
&memo.CreatorId,
&memo.CreatedTs,
&memo.UpdatedTs,
&memo.Content,
&memo.RowStatus,
); err != nil {
return nil, FormatError(err)
}
list = append(list, &memo)
}
if err := rows.Err(); err != nil {
return nil, FormatDBError(err)
return nil, FormatError(err)
}
return list, nil
}
func deleteMemo(db *DB, delete *api.MemoDelete) error {
result, err := db.Db.Exec(`DELETE FROM memo WHERE id = ?`, delete.Id)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo ID not found: %d", delete.Id)}
}
return memos, nil
return nil
}
package store
import (
"memos/utils"
"strings"
)
type Query struct {
Id string `json:"id"`
UserId string `json:"userId"`
Title string `json:"title"`
Querystring string `json:"querystring"`
PinnedAt string `json:"pinnedAt"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
func CreateNewQuery(title string, querystring string, userId string) (Query, error) {
nowDateTimeStr := utils.GetNowDateTimeStr()
newQuery := Query{
Id: utils.GenUUID(),
Title: title,
Querystring: querystring,
UserId: userId,
PinnedAt: "",
CreatedAt: nowDateTimeStr,
UpdatedAt: nowDateTimeStr,
}
sqlQuery := `INSERT INTO queries (id, title, querystring, user_id, pinned_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`
_, err := DB.Exec(sqlQuery, newQuery.Id, newQuery.Title, newQuery.Querystring, newQuery.UserId, newQuery.PinnedAt, newQuery.CreatedAt, newQuery.UpdatedAt)
return newQuery, FormatDBError(err)
}
type QueryPatch struct {
Title *string
Querystring *string
PinnedAt *string
}
func UpdateQuery(id string, queryPatch *QueryPatch) (Query, error) {
query, _ := GetQueryById(id)
set, args := []string{}, []interface{}{}
if v := queryPatch.Title; v != nil {
query.Title = *v
set, args = append(set, "title=?"), append(args, *v)
}
if v := queryPatch.Querystring; v != nil {
query.Querystring = *v
set, args = append(set, "querystring=?"), append(args, *v)
}
if v := queryPatch.PinnedAt; v != nil {
query.PinnedAt = *v
set, args = append(set, "pinned_at=?"), append(args, *v)
}
set, args = append(set, "updated_at=?"), append(args, utils.GetNowDateTimeStr())
args = append(args, id)
sqlQuery := `UPDATE queries SET ` + strings.Join(set, ",") + ` WHERE id=?`
_, err := DB.Exec(sqlQuery, args...)
return query, FormatDBError(err)
}
func DeleteQuery(queryId string) error {
query := `DELETE FROM queries WHERE id=?`
_, err := DB.Exec(query, queryId)
return FormatDBError(err)
}
func GetQueryById(queryId string) (Query, error) {
sqlQuery := `SELECT id, title, querystring, pinned_at, created_at, updated_at FROM queries WHERE id=?`
query := Query{}
err := DB.QueryRow(sqlQuery, queryId).Scan(&query.Id, &query.Title, &query.Querystring, &query.PinnedAt, &query.CreatedAt, &query.UpdatedAt)
return query, FormatDBError(err)
}
func GetQueriesByUserId(userId string) ([]Query, error) {
query := `SELECT id, title, querystring, pinned_at, created_at, updated_at FROM queries WHERE user_id=?`
rows, _ := DB.Query(query, userId)
defer rows.Close()
queries := []Query{}
for rows.Next() {
query := Query{}
rows.Scan(&query.Id, &query.Title, &query.Querystring, &query.PinnedAt, &query.CreatedAt, &query.UpdatedAt)
queries = append(queries, query)
}
if err := rows.Err(); err != nil {
return nil, FormatDBError(err)
}
return queries, nil
}
package store
import "memos/utils"
import (
"fmt"
"memos/api"
"memos/common"
"strings"
)
type Resource struct {
Id string `json:"id"`
UserId string `json:"userId"`
Filename string `json:"filename"`
Blob []byte `json:"blob"`
Type string `json:"type"`
Size int64 `json:"size"`
CreatedAt string `json:"createdAt"`
type ResourceService struct {
db *DB
}
func CreateResource(userId string, filename string, blob []byte, filetype string, size int64) (Resource, error) {
newResource := Resource{
Id: utils.GenUUID(),
UserId: userId,
Filename: filename,
Blob: blob,
Type: filetype,
Size: size,
CreatedAt: utils.GetNowDateTimeStr(),
func NewResourceService(db *DB) *ResourceService {
return &ResourceService{db: db}
}
func (s *ResourceService) CreateResource(create *api.ResourceCreate) (*api.Resource, error) {
resource, err := createResource(s.db, create)
if err != nil {
return nil, err
}
query := `INSERT INTO resources (id, user_id, filename, blob, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`
_, err := DB.Exec(query, newResource.Id, newResource.UserId, newResource.Filename, newResource.Blob, newResource.Type, newResource.Size, newResource.CreatedAt)
return resource, nil
}
return newResource, FormatDBError(err)
func (s *ResourceService) FindResouceList(find *api.ResourceFind) ([]*api.Resource, error) {
list, err := findResourceList(s.db, find)
if err != nil {
return nil, err
}
return list, nil
}
func GetResourcesByUserId(userId string) ([]Resource, error) {
query := `SELECT id, filename, type, size, created_at FROM resources WHERE user_id=?`
rows, _ := DB.Query(query, userId)
defer rows.Close()
func (s *ResourceService) DeleteResource(delete *api.ResourceDelete) error {
err := deleteResource(s.db, delete)
if err != nil {
return err
}
return nil
}
func createResource(db *DB, create *api.ResourceCreate) (*api.Resource, error) {
row, err := db.Db.Query(`
INSERT INTO resource (
filename,
blob,
type,
size,
creator_id
)
VALUES (?, ?, ?, ?, ?)
RETURNING id, filename, blob, type, size, created_ts, updated_ts
`,
create.Filename,
create.Blob,
create.Type,
create.Size,
create.CreatorId,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
if !row.Next() {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var resource api.Resource
if err := row.Scan(
&resource.Id,
&resource.Filename,
&resource.Blob,
&resource.Type,
&resource.Size,
&resource.CreatedTs,
&resource.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &resource, nil
}
func findResourceList(db *DB, find *api.ResourceFind) ([]*api.Resource, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.Id; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := find.CreatorId; v != nil {
where, args = append(where, "creator_id = ?"), append(args, *v)
}
if v := find.Filename; v != nil {
where, args = append(where, "filename = ?"), append(args, *v)
}
resources := []Resource{}
rows, err := db.Db.Query(`
SELECT
id,
filename,
blob,
type,
size,
created_ts,
updated_ts
FROM resource
WHERE `+strings.Join(where, " AND "),
args...,
)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
list := make([]*api.Resource, 0)
for rows.Next() {
resource := Resource{}
rows.Scan(&resource.Id, &resource.Filename, &resource.Type, &resource.Size, &resource.CreatedAt)
resources = append(resources, resource)
var resource api.Resource
if err := rows.Scan(
&resource.Id,
&resource.Filename,
&resource.Blob,
&resource.Type,
&resource.Size,
&resource.CreatedTs,
&resource.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
list = append(list, &resource)
}
if err := rows.Err(); err != nil {
return nil, FormatDBError(err)
return nil, FormatError(err)
}
return resources, nil
return list, nil
}
func GetResourceByIdAndFilename(id string, filename string) (Resource, error) {
query := `SELECT id, filename, blob, type, size FROM resources WHERE id=? AND filename=?`
resource := Resource{}
err := DB.QueryRow(query, id, filename).Scan(&resource.Id, &resource.Filename, &resource.Blob, &resource.Type, &resource.Size)
return resource, FormatDBError(err)
}
func deleteResource(db *DB, delete *api.ResourceDelete) error {
result, err := db.Db.Exec(`DELETE FROM resource WHERE id = ?`, delete.Id)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource ID not found: %d", delete.Id)}
}
func DeleteResourceById(id string) error {
query := `DELETE FROM resources WHERE id=?`
_, err := DB.Exec(query, id)
return FormatDBError(err)
return nil
}
DROP TABLE IF EXISTS `memo`;
DROP TABLE IF EXISTS `shortcut`;
DROP TABLE IF EXISTS `resource`;
DROP TABLE IF EXISTS `user`;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
password TEXT NOT NULL,
open_id TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(`name`, `open_id`)
);
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('user', 0);
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
AFTER
UPDATE
ON `user` FOR EACH ROW BEGIN
UPDATE
`user`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
-- allowed row status are 'NORMAL', 'HIDDEN'.
row_status TEXT NOT NULL DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
creator_id INTEGER NOT NULL,
FOREIGN KEY(creator_id) REFERENCES users(id)
);
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('memo', 100);
CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`
AFTER
UPDATE
ON `memo` FOR EACH ROW BEGIN
UPDATE
`memo`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '',
creator_id INTEGER NOT NULL,
pinned_ts BIGINT NOT NULL DEFAULT 0,
FOREIGN KEY(creator_id) REFERENCES users(id)
);
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('shortcut', 0);
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
AFTER
UPDATE
ON `shortcut` FOR EACH ROW BEGIN
UPDATE
`shortcut`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL DEFAULT '',
blob BLOB NOT NULL,
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
creator_id INTEGER NOT NULL,
FOREIGN KEY(creator_id) REFERENCES users(id)
);
CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`
AFTER
UPDATE
ON `resource` FOR EACH ROW BEGIN
UPDATE
`resource`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
INSERT INTO user
(`id`, `name`, `password`, `open_id`)
VALUES
(1, 'guest', '123456', 'guest_open_id'),
(2, 'mine', '123456', 'mine_open_id');
INSERT INTO memo
(`content`, `creator_id`)
VALUES
('👋 Welcome to memos', 1),
('👋 Welcome to memos', 2);
package store
import (
"fmt"
"memos/api"
"memos/common"
"strings"
)
type ShortcutService struct {
db *DB
}
func NewShortcutService(db *DB) *ShortcutService {
return &ShortcutService{db: db}
}
func (s *ShortcutService) CreateShortcut(create *api.ShortcutCreate) (*api.Shortcut, error) {
shortcut, err := createShortcut(s.db, create)
if err != nil {
return nil, err
}
return shortcut, nil
}
func (s *ShortcutService) PatchShortcut(patch *api.ShortcutPatch) (*api.Shortcut, error) {
shortcut, err := patchShortcut(s.db, patch)
if err != nil {
return nil, err
}
return shortcut, nil
}
func (s *ShortcutService) FindShortcutList(find *api.ShortcutFind) ([]*api.Shortcut, error) {
list, err := findShortcutList(s.db, find)
if err != nil {
return nil, err
}
return list, nil
}
func (s *ShortcutService) FindShortcut(find *api.ShortcutFind) (*api.Shortcut, error) {
list, err := findShortcutList(s.db, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
return list[0], nil
}
func (s *ShortcutService) DeleteShortcut(delete *api.ShortcutDelete) error {
err := deleteShortcut(s.db, delete)
if err != nil {
return FormatError(err)
}
return nil
}
func createShortcut(db *DB, create *api.ShortcutCreate) (*api.Shortcut, error) {
row, err := db.Db.Query(`
INSERT INTO shortcut (
title,
payload,
creator_id,
)
VALUES (?, ?, ?)
RETURNING id, title, payload, creator_id, created_ts, updated_ts, pinned_ts
`,
create.Title,
create.Payload,
create.CreatorId,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
var shortcut api.Shortcut
if err := row.Scan(
&shortcut.Id,
&shortcut.Title,
&shortcut.Payload,
&shortcut.CreatorId,
&shortcut.CreatedTs,
&shortcut.UpdatedTs,
&shortcut.PinnedTs,
); err != nil {
return nil, FormatError(err)
}
return &shortcut, nil
}
func patchShortcut(db *DB, patch *api.ShortcutPatch) (*api.Shortcut, error) {
set, args := []string{}, []interface{}{}
if v := patch.Title; v != nil {
set, args = append(set, "title = ?"), append(args, *v)
}
if v := patch.Payload; v != nil {
set, args = append(set, "payload = ?"), append(args, *v)
}
if v := patch.PinnedTs; v != nil {
set, args = append(set, "pinned_ts = ?"), append(args, *v)
}
args = append(args, patch.Id)
row, err := db.Db.Query(`
UPDATE shortcut
SET `+strings.Join(set, ", ")+`
WHERE id = ?
RETURNING id, title, payload, created_ts, updated_ts, pinned_ts
`, args...)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
if !row.Next() {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var shortcut api.Shortcut
if err := row.Scan(
&shortcut.Id,
&shortcut.Title,
&shortcut.Payload,
&shortcut.CreatedTs,
&shortcut.UpdatedTs,
&shortcut.PinnedTs,
); err != nil {
return nil, FormatError(err)
}
return &shortcut, nil
}
func findShortcutList(db *DB, find *api.ShortcutFind) ([]*api.Shortcut, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.Id; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := find.CreatorId; v != nil {
where, args = append(where, "creator_id = ?"), append(args, *v)
}
if v := find.Title; v != nil {
where, args = append(where, "title = ?"), append(args, *v)
}
rows, err := db.Db.Query(`
SELECT
id,
title,
payload,
creator_id,
created_ts,
updated_ts,
pinned_ts
FROM shortcut
WHERE `+strings.Join(where, " AND "),
args...,
)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
list := make([]*api.Shortcut, 0)
for rows.Next() {
var shortcut api.Shortcut
if err := rows.Scan(
&shortcut.Id,
&shortcut.Title,
&shortcut.Payload,
&shortcut.CreatorId,
&shortcut.CreatedTs,
&shortcut.UpdatedTs,
&shortcut.PinnedTs,
); err != nil {
return nil, FormatError(err)
}
list = append(list, &shortcut)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return list, nil
}
func deleteShortcut(db *DB, delete *api.ShortcutDelete) error {
result, err := db.Db.Exec(`DELETE FROM saved_query WHERE id = ?`, delete.Id)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo ID not found: %d", delete.Id)}
}
return nil
}
package store
import (
"database/sql"
"embed"
"errors"
"fmt"
"io/fs"
"sort"
_ "github.com/mattn/go-sqlite3"
)
//go:embed seed
var seedFS embed.FS
type DB struct {
Db *sql.DB
// Datasource name.
DSN string
}
// NewDB returns a new instance of DB associated with the given datasource name.
func NewDB(dsn string) *DB {
db := &DB{
DSN: dsn,
}
return db
}
func (db *DB) Open() (err error) {
// Ensure a DSN is set before attempting to open the database.
if db.DSN == "" {
return fmt.Errorf("dsn required")
}
// Connect to the database.
if db.Db, err = sql.Open("sqlite3", db.DSN); err != nil {
return err
}
if err := db.seed(); err != nil {
return fmt.Errorf("failed to seed: %w", err)
}
return err
}
func (db *DB) seed() error {
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
if err != nil {
return err
}
sort.Strings(filenames)
// Loop over all seed files and execute them in order.
for _, filename := range filenames {
if err := db.seedFile(filename); err != nil {
return fmt.Errorf("seed error: name=%q err=%w", filename, err)
}
}
return nil
}
// seedFile runs a single seed file within a transaction.
func (db *DB) seedFile(name string) error {
tx, err := db.Db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Read and execute migration file.
if buf, err := fs.ReadFile(seedFS, name); err != nil {
return err
} else if _, err := tx.Exec(string(buf)); err != nil {
return err
}
return tx.Commit()
}
func FormatError(err error) error {
if err == nil {
return nil
}
switch err {
case sql.ErrNoRows:
return errors.New("data not found")
default:
return err
}
}
package store
import (
"database/sql"
"fmt"
"memos/utils"
"memos/api"
"memos/common"
"strings"
)
type User struct {
Id string `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
OpenId string `json:"openId"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
type UserService struct {
db *DB
}
func CreateNewUser(username string, password string) (User, error) {
nowDateTimeStr := utils.GetNowDateTimeStr()
newUser := User{
Id: utils.GenUUID(),
Username: username,
Password: password,
OpenId: utils.GenUUID(),
CreatedAt: nowDateTimeStr,
UpdatedAt: nowDateTimeStr,
}
query := `INSERT INTO users (id, username, password, open_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`
_, err := DB.Exec(query, newUser.Id, newUser.Username, newUser.Password, newUser.OpenId, newUser.CreatedAt, newUser.UpdatedAt)
return newUser, FormatDBError(err)
}
type UpdateUserPatch struct {
Username *string
Password *string
func NewUserService(db *DB) *UserService {
return &UserService{db: db}
}
func UpdateUser(id string, updateUserPatch *UpdateUserPatch) (User, error) {
user := User{}
user, err := GetUserById(id)
func (s *UserService) CreateUser(create *api.UserCreate) (*api.User, error) {
user, err := createUser(s.db, create)
if err != nil {
return user, FormatDBError(err)
return nil, err
}
set, args := []string{}, []interface{}{}
return user, nil
}
if v := updateUserPatch.Username; v != nil {
user.Username = *v
set, args = append(set, "username=?"), append(args, *v)
}
if v := updateUserPatch.Password; v != nil {
user.Password = *v
set, args = append(set, "password=?"), append(args, *v)
func (s *UserService) PatchUser(patch *api.UserPatch) (*api.User, error) {
user, err := patchUser(s.db, patch)
if err != nil {
return nil, err
}
set, args = append(set, "updated_at=?"), append(args, utils.GetNowDateTimeStr())
args = append(args, id)
sqlQuery := `UPDATE users SET ` + strings.Join(set, ",") + ` WHERE id=?`
_, err = DB.Exec(sqlQuery, args...)
return user, FormatDBError(err)
return user, nil
}
func ResetUserOpenId(userId string) (string, error) {
openId := utils.GenUUID()
query := `UPDATE users SET open_id=? WHERE id=?`
_, err := DB.Exec(query, openId, userId)
return openId, FormatDBError(err)
}
func (s *UserService) FindUser(find *api.UserFind) (*api.User, error) {
list, err := findUserList(s.db, find)
if err != nil {
return nil, err
}
func GetUserById(id string) (User, error) {
query := `SELECT id, username, password, open_id, created_at, updated_at FROM users WHERE id=?`
user := User{}
err := DB.QueryRow(query, id).Scan(&user.Id, &user.Username, &user.Password, &user.OpenId, &user.CreatedAt, &user.UpdatedAt)
return user, FormatDBError(err)
}
if len(list) == 0 {
return nil, nil
} else if len(list) > 1 {
return nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d users with filter %+v, expect 1. ", len(list), find)}
}
func GetUserByOpenId(openId string) (User, error) {
query := `SELECT id, username, password, open_id, created_at, updated_at FROM users WHERE open_id=?`
user := User{}
err := DB.QueryRow(query, openId).Scan(&user.Id, &user.Username, &user.Password, &user.OpenId, &user.CreatedAt, &user.UpdatedAt)
return user, FormatDBError(err)
return list[0], nil
}
func GetUserByUsernameAndPassword(username string, password string) (User, error) {
query := `SELECT id, username, password, open_id, created_at, updated_at FROM users WHERE username=? AND password=?`
user := User{}
err := DB.QueryRow(query, username, password).Scan(&user.Id, &user.Username, &user.Password, &user.OpenId, &user.CreatedAt, &user.UpdatedAt)
return user, FormatDBError(err)
func createUser(db *DB, create *api.UserCreate) (*api.User, error) {
row, err := db.Db.Query(`
INSERT INTO user (
name,
password,
open_id
)
VALUES (?, ?, ?)
RETURNING id, name, password, open_id, created_ts, updated_ts
`)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
var user api.User
if err := row.Scan(
&user.Id,
&user.Name,
&user.Password,
&user.OpenId,
&user.CreatedTs,
&user.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &user, nil
}
func CheckUsernameUsable(username string) (bool, error) {
query := `SELECT * FROM users WHERE username=?`
query = fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query)
func patchUser(db *DB, patch *api.UserPatch) (*api.User, error) {
set, args := []string{}, []interface{}{}
var count uint
err := DB.QueryRow(query, username).Scan(&count)
if err != nil && err != sql.ErrNoRows {
return false, FormatDBError(err)
if v := patch.Name; v != nil {
set, args = append(set, "name = ?"), append(args, *v)
}
usable := true
if count > 0 {
usable = false
if v := patch.Password; v != nil {
set, args = append(set, "password = ?"), append(args, *v)
}
if v := patch.OpenId; v != nil {
set, args = append(set, "open_id = ?"), append(args, *v)
}
args = append(args, patch.Id)
row, err := db.Db.Query(`
UPDATE user
SET `+strings.Join(set, ", ")+`
WHERE id = ?
RETURNING id, name, password, open_id, created_ts, updated_ts
`, args...)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
if row.Next() {
var user api.User
if err := row.Scan(
&user.Id,
&user.Name,
&user.Password,
&user.OpenId,
&user.CreatedTs,
&user.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &user, nil
}
return usable, nil
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", patch.Id)}
}
func CheckPasswordValid(id string, password string) (bool, error) {
query := `SELECT * FROM users WHERE id=? AND password=?`
query = fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query)
func findUserList(db *DB, find *api.UserFind) ([]*api.User, error) {
where, args := []string{}, []interface{}{}
if v := find.Id; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := find.Name; v != nil {
where, args = append(where, "name = ?"), append(args, *v)
}
if v := find.OpenId; v != nil {
where, args = append(where, "open_id = ?"), append(args, *v)
}
var count uint
err := DB.QueryRow(query, id, password).Scan(&count)
if err != nil && err != sql.ErrNoRows {
return false, FormatDBError(err)
rows, err := db.Db.Query(`
SELECT
id,
name,
password,
open_id,
created_ts
updated_ts
FROM user
WHERE `+strings.Join(where, " AND "),
args...,
)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
list := make([]*api.User, 0)
for rows.Next() {
var user api.User
if err := rows.Scan(
&user.Id,
&user.Name,
&user.Password,
&user.OpenId,
&user.CreatedTs,
&user.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
list = append(list, &user)
}
if count > 0 {
return true, nil
} else {
return false, nil
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return list, nil
}
package utils
import (
"time"
"github.com/google/uuid"
)
func GenUUID() string {
return uuid.New().String()
}
func GetNowDateTimeStr() string {
return time.Now().Local().Format("2006/01/02 15:04:05")
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment