mirror of
https://github.com/usememos/memos.git
synced 2024-12-21 10:11:42 +03:00
Merge pull request #26 from justmemos/refactor/go
🔥🔥🔥Big refactoring🔥🔥🔥
This commit is contained in:
commit
475f258f5b
@ -28,4 +28,4 @@ jobs:
|
|||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ secrets.DOCKER_NEOSMEMO_USERNAME }}/memos:latest
|
tags: ${{ secrets.DOCKER_NEOSMEMO_USERNAME }}/memos:next
|
||||||
|
@ -17,18 +17,13 @@ COPY . .
|
|||||||
|
|
||||||
RUN go build \
|
RUN go build \
|
||||||
-o memos \
|
-o memos \
|
||||||
./server/main.go
|
./bin/server/main.go
|
||||||
|
|
||||||
# Make workspace with above generated files.
|
# Make workspace with above generated files.
|
||||||
FROM alpine:3.14.3 AS monolithic
|
FROM alpine:3.14.3 AS monolithic
|
||||||
WORKDIR /usr/local/memos
|
WORKDIR /usr/local/memos
|
||||||
|
|
||||||
RUN apk add --no-cache tzdata
|
|
||||||
ENV TZ="Asia/Shanghai"
|
|
||||||
|
|
||||||
COPY --from=backend /backend-build/memos /usr/local/memos/
|
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
|
COPY --from=frontend /frontend-build/dist /usr/local/memos/web/dist
|
||||||
|
|
||||||
CMD ["./memos"]
|
CMD ["./memos"]
|
||||||
|
36
README.md
36
README.md
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://memos.onrender.com/">Live Demo</a> •
|
<a href="https://memos.onrender.com/">Live Demo</a> •
|
||||||
<a href="https://github.com/justmemos/memos/discussions">Discussions</a> •
|
<a href="https://github.com/justmemos/memos/discussions">Discussions</a>
|
||||||
<a href="https://t.me/+M-AqruZmJBhkYWQ1">Telegram</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -13,23 +12,30 @@
|
|||||||
<img alt="GitHub license" src="https://img.shields.io/github/license/justmemos/memos" />
|
<img alt="GitHub license" src="https://img.shields.io/github/license/justmemos/memos" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Memos 是一款开源的 [flomo](https://flomoapp.com/) 替代工具,为了快速方便的部署属于自己的碎片化知识管理工具。
|
Memos is an open source, self-hosted alternative to [flomo](https://flomoapp.com/). Built with `Golang` and `React`.
|
||||||
|
|
||||||
## 🎯 产品意图
|
Making sure that you are in charge of your data and more customizations.
|
||||||
|
|
||||||
- 📅 用于记录:每日/周计划、💡 突发奇想、📕 读后感...
|
## 🎯 Intentions
|
||||||
- 🏗️ 代替了微信“文件传输助手”;
|
|
||||||
- 📒 打造一个属于自己的轻量化“卡片”笔记簿;
|
|
||||||
|
|
||||||
## ✨ 特色亮点
|
- ✍️ For noting 📅 daily/weekly plans, 💡 fantastic ideas, 📕 reading thoughts...
|
||||||
|
- 📒 Write down the lightweight card memos easily;
|
||||||
|
- 🏗️ Build your own fragmented knowledge management tools;
|
||||||
|
|
||||||
- 🦄 开源项目;
|
## ✨ Features
|
||||||
- 😋 精美且细节的视觉样式;
|
|
||||||
- 📑 体验优良的交互逻辑;
|
|
||||||
- ⚡️ 快速地私有化部署;
|
|
||||||
|
|
||||||
## 📕 文档
|
- 🦄 Open source project;
|
||||||
|
- 😋 Beautiful and detailed visual styles;
|
||||||
|
- 📑 Experience excellent interaction logic;
|
||||||
|
- ⚡️ Quick privatization deployment;
|
||||||
|
|
||||||
- [使用 Docker 部署](https://github.com/justmemos/memos/tree/main/docs/deploy)
|
<!--
|
||||||
|
WIP
|
||||||
|
## 📕 Docs
|
||||||
|
|
||||||
Enjoy it and welcome your contributions
|
- [Guide to self host with Docker](https://github.com/justmemos/memos/tree/main/docs/deploy)
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Just enjoy it.
|
||||||
|
114
api/auth.go
114
api/auth.go
@ -1,113 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
type Login struct {
|
||||||
"encoding/json"
|
Name string `json:"name"`
|
||||||
"memos/api/e"
|
Password string `json:"password"`
|
||||||
"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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUserSignIn(w http.ResponseWriter, r *http.Request) {
|
type Signup struct {
|
||||||
type UserSigninDataBody struct {
|
Name string `json:"name"`
|
||||||
Username string `json:"username"`
|
Password string `json:"password"`
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
131
api/memo.go
131
api/memo.go
@ -1,115 +1,42 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
type Memo struct {
|
||||||
"encoding/json"
|
Id int `json:"id"`
|
||||||
"memos/api/e"
|
CreatedTs int64 `json:"createdTs"`
|
||||||
"memos/store"
|
UpdatedTs int64 `json:"updatedTs"`
|
||||||
"net/http"
|
RowStatus string `json:"rowStatus"`
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
Content string `json:"content"`
|
||||||
)
|
CreatorId int `json:"creatorId"`
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCreateMemo(w http.ResponseWriter, r *http.Request) {
|
type MemoCreate struct {
|
||||||
userId, _ := GetUserIdInSession(r)
|
Content string `json:"content"`
|
||||||
|
CreatorId int
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateMemo(w http.ResponseWriter, r *http.Request) {
|
type MemoPatch struct {
|
||||||
vars := mux.Vars(r)
|
Id int
|
||||||
memoId := vars["id"]
|
|
||||||
|
|
||||||
memoPatch := store.MemoPatch{}
|
Content *string `json:"content"`
|
||||||
err := json.NewDecoder(r.Body).Decode(&memoPatch)
|
RowStatus *string `json:"rowStatus"`
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(Response{
|
|
||||||
Succeed: true,
|
|
||||||
Message: "",
|
|
||||||
Data: memo,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeleteMemo(w http.ResponseWriter, r *http.Request) {
|
type MemoFind struct {
|
||||||
vars := mux.Vars(r)
|
Id *int `json:"id"`
|
||||||
memoId := vars["id"]
|
CreatorId *int `json:"creatorId"`
|
||||||
|
RowStatus *string `json:"rowStatus"`
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterMemoRoutes(r *mux.Router) {
|
type MemoDelete struct {
|
||||||
memoRouter := r.PathPrefix("/api/memo").Subrouter()
|
Id *int `json:"id"`
|
||||||
|
CreatorId *int
|
||||||
memoRouter.Use(JSONResponseMiddleWare)
|
}
|
||||||
memoRouter.Use(AuthCheckerMiddleWare)
|
|
||||||
|
type MemoService interface {
|
||||||
memoRouter.HandleFunc("/all", handleGetMyMemos).Methods("GET")
|
CreateMemo(create *MemoCreate) (*Memo, error)
|
||||||
memoRouter.HandleFunc("/", handleCreateMemo).Methods("PUT")
|
PatchMemo(patch *MemoPatch) (*Memo, error)
|
||||||
memoRouter.HandleFunc("/{id}", handleUpdateMemo).Methods("PATCH")
|
FindMemoList(find *MemoFind) ([]*Memo, error)
|
||||||
memoRouter.HandleFunc("/{id}", handleDeleteMemo).Methods("DELETE")
|
FindMemo(find *MemoFind) (*Memo, error)
|
||||||
|
DeleteMemo(delete *MemoDelete) error
|
||||||
}
|
}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
113
api/query.go
113
api/query.go
@ -1,113 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
142
api/resource.go
142
api/resource.go
@ -1,130 +1,40 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
type Resource struct {
|
||||||
"encoding/json"
|
Id int `json:"id"`
|
||||||
"io/ioutil"
|
CreatedTs int64 `json:"createdTs"`
|
||||||
"memos/api/e"
|
UpdatedTs int64 `json:"updatedTs"`
|
||||||
"memos/store"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
Filename string `json:"filename"`
|
||||||
)
|
Blob []byte `json:"blob"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
|
||||||
func handleGetMyResources(w http.ResponseWriter, r *http.Request) {
|
CreatorId int `json:"creatorId"`
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUploadResource(w http.ResponseWriter, r *http.Request) {
|
type ResourceCreate struct {
|
||||||
userId, _ := GetUserIdInSession(r)
|
Filename string `json:"filename"`
|
||||||
|
Blob []byte `json:"blob"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
|
||||||
err := r.ParseMultipartForm(5 << 20)
|
CreatorId int
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(Response{
|
|
||||||
Succeed: true,
|
|
||||||
Message: "Upload file succeed",
|
|
||||||
Data: resource,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeleteResource(w http.ResponseWriter, r *http.Request) {
|
type ResourceFind struct {
|
||||||
vars := mux.Vars(r)
|
Id *int `json:"id"`
|
||||||
resourceId := vars["id"]
|
CreatorId *int `json:"creatorId"`
|
||||||
|
Filename *string `json:"filename"`
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetResource(w http.ResponseWriter, r *http.Request) {
|
type ResourceDelete struct {
|
||||||
vars := mux.Vars(r)
|
Id int
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterResourceRoutes(r *mux.Router) {
|
type ResourceService interface {
|
||||||
resourceRouter := r.PathPrefix("/").Subrouter()
|
CreateResource(create *ResourceCreate) (*Resource, error)
|
||||||
|
FindResourceList(find *ResourceFind) ([]*Resource, error)
|
||||||
resourceRouter.Use(AuthCheckerMiddleWare)
|
FindResource(find *ResourceFind) (*Resource, error)
|
||||||
|
DeleteResource(delete *ResourceDelete) error
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"memos/utils"
|
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
)
|
|
||||||
|
|
||||||
var SessionStore = sessions.NewCookieStore([]byte(utils.GenUUID()))
|
|
51
api/shortcut.go
Normal file
51
api/shortcut.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
type Shortcut struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
CreatedTs int64 `json:"createdTs"`
|
||||||
|
UpdatedTs int64 `json:"updatedTs"`
|
||||||
|
|
||||||
|
Title string `json:"title"`
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
RowStatus string `json:"rowStatus"`
|
||||||
|
CreatorId int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortcutCreate struct {
|
||||||
|
// Standard fields
|
||||||
|
CreatorId int
|
||||||
|
|
||||||
|
// Domain specific fields
|
||||||
|
Title string `json:"title"`
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortcutPatch struct {
|
||||||
|
Id int
|
||||||
|
|
||||||
|
Title *string `json:"title"`
|
||||||
|
Payload *string `json:"payload"`
|
||||||
|
RowStatus *string `json:"rowStatus"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortcutFind struct {
|
||||||
|
Id *int
|
||||||
|
|
||||||
|
// Standard fields
|
||||||
|
CreatorId *int
|
||||||
|
|
||||||
|
// Domain specific fields
|
||||||
|
Title *string `json:"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
|
||||||
|
}
|
@ -1,34 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
164
api/user.go
164
api/user.go
@ -1,149 +1,49 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
type User struct {
|
||||||
"encoding/json"
|
Id int `json:"id"`
|
||||||
"memos/api/e"
|
CreatedTs int64 `json:"createdTs"`
|
||||||
"memos/store"
|
UpdatedTs int64 `json:"updatedTs"`
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
OpenId string `json:"openId"`
|
||||||
)
|
Name string `json:"name"`
|
||||||
|
Password string `json:"-"`
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateMyUserInfo(w http.ResponseWriter, r *http.Request) {
|
type UserCreate struct {
|
||||||
userId, _ := GetUserIdInSession(r)
|
OpenId string `json:"openId"`
|
||||||
|
Name string `json:"name"`
|
||||||
updateUserPatch := store.UpdateUserPatch{}
|
Password string `json:"password"`
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleResetUserOpenId(w http.ResponseWriter, r *http.Request) {
|
type UserPatch struct {
|
||||||
userId, _ := GetUserIdInSession(r)
|
Id int
|
||||||
|
|
||||||
openId, err := store.ResetUserOpenId(userId)
|
OpenId *string
|
||||||
|
|
||||||
if err != nil {
|
Name *string `json:"name"`
|
||||||
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
|
Password *string `json:"password"`
|
||||||
return
|
ResetOpenId *bool `json:"resetOpenId"`
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(Response{
|
|
||||||
Succeed: true,
|
|
||||||
Message: "",
|
|
||||||
Data: openId,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCheckUsername(w http.ResponseWriter, r *http.Request) {
|
type UserFind struct {
|
||||||
type CheckUsernameDataBody struct {
|
Id *int `json:"id"`
|
||||||
Username string
|
|
||||||
}
|
|
||||||
|
|
||||||
checkUsername := CheckUsernameDataBody{}
|
Name *string `json:"name"`
|
||||||
err := json.NewDecoder(r.Body).Decode(&checkUsername)
|
Password *string
|
||||||
|
OpenId *string
|
||||||
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 UserRenameCheck struct {
|
||||||
type ValidPasswordDataBody struct {
|
Name string `json:"name"`
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterUserRoutes(r *mux.Router) {
|
type UserPasswordCheck struct {
|
||||||
userRouter := r.PathPrefix("/api/user").Subrouter()
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
userRouter.Use(JSONResponseMiddleWare)
|
|
||||||
userRouter.Use(AuthCheckerMiddleWare)
|
type UserService interface {
|
||||||
|
CreateUser(create *UserCreate) (*User, error)
|
||||||
userRouter.HandleFunc("/me", handleGetMyUserInfo).Methods("GET")
|
PatchUser(patch *UserPatch) (*User, error)
|
||||||
userRouter.HandleFunc("/me", handleUpdateMyUserInfo).Methods("PATCH")
|
FindUser(find *UserFind) (*User, error)
|
||||||
userRouter.HandleFunc("/open_id/new", handleResetUserOpenId).Methods("POST")
|
|
||||||
userRouter.HandleFunc("/checkusername", handleCheckUsername).Methods("POST")
|
|
||||||
userRouter.HandleFunc("/validpassword", handleValidPassword).Methods("POST")
|
|
||||||
}
|
}
|
||||||
|
23
api/utils.go
23
api/utils.go
@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
17
bin/server/cmd/profile_dev.go
Normal file
17
bin/server/cmd/profile_dev.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//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: 8080,
|
||||||
|
dsn: fmt.Sprintf("file:%s/memos_dev.db", dataDir),
|
||||||
|
}
|
||||||
|
}
|
97
bin/server/cmd/root.go
Normal file
97
bin/server/cmd/root.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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(m.profile.port)
|
||||||
|
|
||||||
|
s.ShortcutService = store.NewShortcutService(db)
|
||||||
|
s.MemoService = store.NewMemoService(db)
|
||||||
|
s.UserService = store.NewUserService(db)
|
||||||
|
s.ShortcutService = store.NewShortcutService(db)
|
||||||
|
s.ResourceService = store.NewResourceService(db)
|
||||||
|
|
||||||
|
m.server = s
|
||||||
|
|
||||||
|
if err := s.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
7
bin/server/main.go
Normal file
7
bin/server/main.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "memos/bin/server/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
100
common/error.go
Normal file
100
common/error.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
21
common/util.go
Normal file
21
common/util.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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()
|
||||||
|
}
|
22
go.mod
22
go.mod
@ -2,13 +2,31 @@ module memos
|
|||||||
|
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
require github.com/gorilla/mux v1.8.0
|
|
||||||
|
|
||||||
require github.com/mattn/go-sqlite3 v1.14.9
|
require github.com/mattn/go-sqlite3 v1.14.9
|
||||||
|
|
||||||
require github.com/google/uuid v1.3.0
|
require github.com/google/uuid v1.3.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // 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/gorilla/context v1.1.1 // indirect
|
||||||
|
github.com/labstack/echo/v4 v4.6.3
|
||||||
|
github.com/labstack/gommon v0.3.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
github.com/gorilla/sessions v1.2.1
|
github.com/gorilla/sessions v1.2.1
|
||||||
|
github.com/labstack/echo-contrib v0.12.0
|
||||||
)
|
)
|
||||||
|
605
go.sum
605
go.sum
@ -1,10 +1,613 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||||
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
|
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||||
|
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||||
|
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||||
|
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||||
|
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||||
|
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||||
|
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||||
|
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||||
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
|
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||||
|
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||||
|
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||||
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
|
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||||
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
|
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||||
|
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||||
|
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||||
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
|
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||||
|
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||||
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||||
|
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||||
|
github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fTKCulPVs=
|
||||||
|
github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0=
|
||||||
|
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
|
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/casbin/casbin/v2 v2.40.6/go.mod h1:sEL80qBYTbd+BPeL4iyvwYzFT3qwLaESq5aFKVLbLfA=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
|
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
|
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
|
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||||
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
|
||||||
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/labstack/echo-contrib v0.12.0 h1:NPr1ez+XUa5s/4LujEon+32Bxg5DO6EKSW/va06pmLc=
|
||||||
|
github.com/labstack/echo-contrib v0.12.0/go.mod h1:kR62TbwsBgmpV2HVab5iQRsQtLuhPyGqCBee88XRc4M=
|
||||||
|
github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
|
||||||
|
github.com/labstack/echo/v4 v4.6.3 h1:VhPuIZYxsbPmo4m9KAkMU/el2442eB7EBFFhNTTT9ac=
|
||||||
|
github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A=
|
||||||
|
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||||
|
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
||||||
|
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||||
|
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
||||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||||
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||||
|
github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
|
||||||
|
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
|
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||||
|
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||||
|
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||||
|
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
|
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
|
github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
||||||
|
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
|
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
||||||
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||||
|
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 h1:kETrAMYZq6WVGPa8IIixL0CaEcIUNi+1WX7grUoi3y8=
|
||||||
|
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
|
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
|
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
|
||||||
|
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
|
||||||
|
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
|
||||||
|
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
|
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||||
|
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
|
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||||
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||||
|
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||||
|
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||||
|
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||||
|
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
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]
|
[build]
|
||||||
bin = "./.air/memos"
|
bin = "./.air/memos"
|
||||||
cmd = "go build -o ./.air/memos ./server/main.go"
|
cmd = "go build -o ./.air/memos ./bin/server/main.go"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = [".air", "web"]
|
exclude_dir = [".air", "web"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
|
97
server/auth.go
Normal file
97
server/auth.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"memos/api"
|
||||||
|
"memos/common"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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 := json.NewDecoder(c.Request().Body).Decode(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 password
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setUserSession(c, user)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set login session").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
g.POST("/auth/logout", func(c echo.Context) error {
|
||||||
|
err := removeUserSession(c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set logout session").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Response().WriteHeader(http.StatusOK)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
g.POST("/auth/signup", func(c echo.Context) error {
|
||||||
|
signup := &api.Signup{}
|
||||||
|
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup 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("Existed 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setUserSession(c, user)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signup session").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 created user response").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
101
server/basic_auth.go
Normal file
101
server/basic_auth.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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) error {
|
||||||
|
sess, err := session.Get("session", c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get session, err: %w", err)
|
||||||
|
}
|
||||||
|
sess.Options = &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 1000 * 3600 * 24 * 30,
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
sess.Values[userIdContextKey] = user.Id
|
||||||
|
err = sess.Save(c.Request(), c.Response())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set session, err: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeUserSession(c echo.Context) error {
|
||||||
|
sess, err := session.Get("session", c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get session, err: %w", err)
|
||||||
|
}
|
||||||
|
sess.Options = &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 0,
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
sess.Values[userIdContextKey] = nil
|
||||||
|
err = sess.Save(c.Request(), c.Response())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set session, err: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use session in the initial version
|
||||||
|
func BasicAuthMiddleware(us api.UserService, next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
// Skips auth
|
||||||
|
if common.HasPrefixes(c.Path(), "/api/auth") {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess, err := session.Get("session", c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing session").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdValue := sess.Values[userIdContextKey]
|
||||||
|
if userIdValue == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing userId in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := strconv.Atoi(fmt.Sprintf("%v", userIdValue))
|
||||||
|
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("Failed to find user by ID: %d", userId)).SetInternal(err)
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Not found user ID: %d", userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stores userId into context.
|
||||||
|
c.Set(getUserIdContextKey(), userId)
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
11
server/common.go
Normal file
11
server/common.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
func composeResponse(data interface{}) interface{} {
|
||||||
|
type R struct {
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return R{
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
133
server/memo.go
Normal file
133
server/memo.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"memos/api"
|
||||||
|
"memos/common"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"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 := json.NewDecoder(c.Request().Body).Decode(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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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 := json.NewDecoder(c.Request().Body).Decode(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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
g.GET("/memo", func(c echo.Context) error {
|
||||||
|
userId := c.Get(getUserIdContextKey()).(int)
|
||||||
|
memoFind := &api.MemoFind{
|
||||||
|
CreatorId: &userId,
|
||||||
|
}
|
||||||
|
showHiddenMemo, err := strconv.ParseBool(c.QueryParam("hidden"))
|
||||||
|
if err != nil {
|
||||||
|
showHiddenMemo = false
|
||||||
|
}
|
||||||
|
|
||||||
|
rowStatus := "NORMAL"
|
||||||
|
if showHiddenMemo {
|
||||||
|
rowStatus = "HIDDEN"
|
||||||
|
}
|
||||||
|
memoFind.RowStatus = &rowStatus
|
||||||
|
|
||||||
|
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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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
|
||||||
|
})
|
||||||
|
}
|
94
server/resource.go
Normal file
94
server/resource.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"memos/api"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"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.StatusInternalServerError, "Failed to open file").SetInternal(err)
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
fileBytes, err := ioutil.ReadAll(src)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceCreate := &api.ResourceCreate{
|
||||||
|
Filename: filename,
|
||||||
|
Type: filetype,
|
||||||
|
Size: size,
|
||||||
|
Blob: fileBytes,
|
||||||
|
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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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
|
||||||
|
})
|
||||||
|
}
|
73
server/server.go
Normal file
73
server/server.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"memos/api"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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(port int) *Server {
|
||||||
|
e := echo.New()
|
||||||
|
e.Debug = true
|
||||||
|
e.HideBanner = true
|
||||||
|
e.HidePort = false
|
||||||
|
|
||||||
|
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||||
|
Format: "${method} ${uri} ${status}\n",
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
|
||||||
|
Skipper: middleware.DefaultSkipper,
|
||||||
|
ErrorMessage: "Request timeout",
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||||
|
Skipper: middleware.DefaultSkipper,
|
||||||
|
Root: "web/dist",
|
||||||
|
Browse: false,
|
||||||
|
HTML5: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.Use(session.Middleware(sessions.NewCookieStore([]byte("just_memos"))))
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
e: e,
|
||||||
|
port: port,
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookGroup := e.Group("/h")
|
||||||
|
s.registerWebhookRoutes(webhookGroup)
|
||||||
|
|
||||||
|
apiGroup := e.Group("/api")
|
||||||
|
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return BasicAuthMiddleware(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))
|
||||||
|
}
|
113
server/shortcut.go
Normal file
113
server/shortcut.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"memos/api"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"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 := json.NewDecoder(c.Request().Body).Decode(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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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 := json.NewDecoder(c.Request().Body).Decode(shortcutPatch); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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.FindShortcutList(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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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
|
||||||
|
})
|
||||||
|
}
|
124
server/user.go
Normal file
124
server/user.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"memos/api"
|
||||||
|
"memos/common"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||||
|
g.GET("/user/me", func(c echo.Context) error {
|
||||||
|
// /api/user/me is used to check if the user is logged in,
|
||||||
|
userSessionId := c.Get(getUserIdContextKey())
|
||||||
|
if userSessionId == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing session")
|
||||||
|
}
|
||||||
|
|
||||||
|
userId := userSessionId.(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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
g.POST("/user/rename_check", func(c echo.Context) error {
|
||||||
|
userRenameCheck := &api.UserRenameCheck{}
|
||||||
|
if err := json.NewDecoder(c.Request().Body).Decode(userRenameCheck); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user rename check request").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userRenameCheck.Name == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user rename check request")
|
||||||
|
}
|
||||||
|
|
||||||
|
userFind := &api.UserFind{
|
||||||
|
Name: &userRenameCheck.Name,
|
||||||
|
}
|
||||||
|
user, err := s.UserService.FindUser(userFind)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isUsable := true
|
||||||
|
if user != nil {
|
||||||
|
isUsable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(isUsable)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode rename check response").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
g.POST("/user/password_check", func(c echo.Context) error {
|
||||||
|
userId := c.Get(getUserIdContextKey()).(int)
|
||||||
|
userPasswordCheck := &api.UserPasswordCheck{}
|
||||||
|
if err := json.NewDecoder(c.Request().Body).Decode(userPasswordCheck); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user password check request").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userPasswordCheck.Password == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user password check request")
|
||||||
|
}
|
||||||
|
|
||||||
|
userFind := &api.UserFind{
|
||||||
|
Id: &userId,
|
||||||
|
Password: &userPasswordCheck.Password,
|
||||||
|
}
|
||||||
|
user, err := s.UserService.FindUser(userFind)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid := false
|
||||||
|
if user != nil {
|
||||||
|
isValid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(isValid)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode password check 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 := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userPatch.ResetOpenId != nil && *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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
74
server/webhook.go
Normal file
74
server/webhook.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"memos/api"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"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 := json.NewDecoder(c.Request().Body).Decode(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 := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode 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
|
||||||
|
})
|
||||||
|
}
|
70
store/db.go
70
store/db.go
@ -1,70 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
251
store/memo.go
251
store/memo.go
@ -1,99 +1,210 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"memos/utils"
|
"fmt"
|
||||||
|
"memos/api"
|
||||||
|
"memos/common"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Memo struct {
|
type MemoService struct {
|
||||||
Id string `json:"id"`
|
db *DB
|
||||||
Content string `json:"content"`
|
|
||||||
UserId string `json:"userId"`
|
|
||||||
DeletedAt string `json:"deletedAt"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
UpdatedAt string `json:"updatedAt"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateNewMemo(content string, userId string) (Memo, error) {
|
func NewMemoService(db *DB) *MemoService {
|
||||||
nowDateTimeStr := utils.GetNowDateTimeStr()
|
return &MemoService{db: db}
|
||||||
newMemo := Memo{
|
}
|
||||||
Id: utils.GenUUID(),
|
|
||||||
Content: content,
|
func (s *MemoService) CreateMemo(create *api.MemoCreate) (*api.Memo, error) {
|
||||||
UserId: userId,
|
memo, err := createMemo(s.db, create)
|
||||||
DeletedAt: "",
|
if err != nil {
|
||||||
CreatedAt: nowDateTimeStr,
|
return nil, err
|
||||||
UpdatedAt: nowDateTimeStr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `INSERT INTO memos (id, content, user_id, deleted_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`
|
return memo, nil
|
||||||
_, err := DB.Exec(query, newMemo.Id, newMemo.Content, newMemo.UserId, newMemo.DeletedAt, newMemo.CreatedAt, newMemo.UpdatedAt)
|
|
||||||
|
|
||||||
return newMemo, FormatDBError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MemoPatch struct {
|
func (s *MemoService) PatchMemo(patch *api.MemoPatch) (*api.Memo, error) {
|
||||||
Content *string
|
memo, err := patchMemo(s.db, patch)
|
||||||
DeletedAt *string
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return memo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateMemo(id string, memoPatch *MemoPatch) (Memo, error) {
|
func (s *MemoService) FindMemoList(find *api.MemoFind) ([]*api.Memo, error) {
|
||||||
memo, _ := GetMemoById(id)
|
list, err := findMemoList(s.db, find)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoService) FindMemo(find *api.MemoFind) (*api.Memo, error) {
|
||||||
|
list, err := findMemoList(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 *MemoService) DeleteMemo(delete *api.MemoDelete) error {
|
||||||
|
err := deleteMemo(s.db, delete)
|
||||||
|
if err != nil {
|
||||||
|
return FormatError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 patchMemo(db *DB, patch *api.MemoPatch) (*api.Memo, error) {
|
||||||
set, args := []string{}, []interface{}{}
|
set, args := []string{}, []interface{}{}
|
||||||
|
|
||||||
if v := memoPatch.Content; v != nil {
|
if v := patch.Content; v != nil {
|
||||||
memo.Content = *v
|
set, args = append(set, "content = ?"), append(args, *v)
|
||||||
set, args = append(set, "content=?"), append(args, *v)
|
|
||||||
}
|
}
|
||||||
if v := memoPatch.DeletedAt; v != nil {
|
if v := patch.RowStatus; v != nil {
|
||||||
memo.DeletedAt = *v
|
set, args = append(set, "row_status = ?"), append(args, *v)
|
||||||
set, args = append(set, "deleted_at=?"), append(args, *v)
|
|
||||||
}
|
|
||||||
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 memo, FormatDBError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteMemo(memoId string) error {
|
|
||||||
query := `DELETE FROM memos WHERE id=?`
|
|
||||||
_, err := DB.Exec(query, memoId)
|
|
||||||
return FormatDBError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 GetMemosByUserId(userId string, onlyDeleted bool) ([]Memo, error) {
|
|
||||||
sqlQuery := `SELECT id, content, deleted_at, created_at, updated_at FROM memos WHERE user_id=?`
|
|
||||||
|
|
||||||
if onlyDeleted {
|
|
||||||
sqlQuery = sqlQuery + ` AND deleted_at!=""`
|
|
||||||
} else {
|
|
||||||
sqlQuery = sqlQuery + ` AND deleted_at=""`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, _ := DB.Query(sqlQuery, userId)
|
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 findMemoList(db *DB, find *api.MemoFind) ([]*api.Memo, 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.RowStatus; v != nil {
|
||||||
|
where, args = append(where, "row_status = ?"), append(args, *v)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
defer rows.Close()
|
||||||
|
|
||||||
memos := []Memo{}
|
list := make([]*api.Memo, 0)
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
memo := Memo{}
|
var memo api.Memo
|
||||||
rows.Scan(&memo.Id, &memo.Content, &memo.DeletedAt, &memo.CreatedAt, &memo.UpdatedAt)
|
if err := rows.Scan(
|
||||||
|
&memo.Id,
|
||||||
|
&memo.CreatorId,
|
||||||
|
&memo.CreatedTs,
|
||||||
|
&memo.UpdatedTs,
|
||||||
|
&memo.Content,
|
||||||
|
&memo.RowStatus,
|
||||||
|
); err != nil {
|
||||||
|
return nil, FormatError(err)
|
||||||
|
}
|
||||||
|
|
||||||
memos = append(memos, memo)
|
list = append(list, &memo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
return nil, FormatDBError(err)
|
return nil, FormatError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return memos, nil
|
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 nil
|
||||||
}
|
}
|
||||||
|
100
store/query.go
100
store/query.go
@ -1,100 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,63 +1,168 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import "memos/utils"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"memos/api"
|
||||||
|
"memos/common"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type Resource struct {
|
type ResourceService struct {
|
||||||
Id string `json:"id"`
|
db *DB
|
||||||
UserId string `json:"userId"`
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
Blob []byte `json:"blob"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateResource(userId string, filename string, blob []byte, filetype string, size int64) (Resource, error) {
|
func NewResourceService(db *DB) *ResourceService {
|
||||||
newResource := Resource{
|
return &ResourceService{db: db}
|
||||||
Id: utils.GenUUID(),
|
}
|
||||||
UserId: userId,
|
|
||||||
Filename: filename,
|
func (s *ResourceService) CreateResource(create *api.ResourceCreate) (*api.Resource, error) {
|
||||||
Blob: blob,
|
resource, err := createResource(s.db, create)
|
||||||
Type: filetype,
|
if err != nil {
|
||||||
Size: size,
|
return nil, err
|
||||||
CreatedAt: utils.GetNowDateTimeStr(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `INSERT INTO resources (id, user_id, filename, blob, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
return resource, nil
|
||||||
_, err := DB.Exec(query, newResource.Id, newResource.UserId, newResource.Filename, newResource.Blob, newResource.Type, newResource.Size, newResource.CreatedAt)
|
|
||||||
|
|
||||||
return newResource, FormatDBError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetResourcesByUserId(userId string) ([]Resource, error) {
|
func (s *ResourceService) FindResourceList(find *api.ResourceFind) ([]*api.Resource, error) {
|
||||||
query := `SELECT id, filename, type, size, created_at FROM resources WHERE user_id=?`
|
list, err := findResourceList(s.db, find)
|
||||||
rows, _ := DB.Query(query, userId)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResourceService) FindResource(find *api.ResourceFind) (*api.Resource, error) {
|
||||||
|
list, err := findResourceList(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 *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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
defer rows.Close()
|
||||||
|
|
||||||
resources := []Resource{}
|
list := make([]*api.Resource, 0)
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
resource := Resource{}
|
var resource api.Resource
|
||||||
rows.Scan(&resource.Id, &resource.Filename, &resource.Type, &resource.Size, &resource.CreatedAt)
|
if err := rows.Scan(
|
||||||
resources = append(resources, resource)
|
&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 {
|
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) {
|
func deleteResource(db *DB, delete *api.ResourceDelete) error {
|
||||||
query := `SELECT id, filename, blob, type, size FROM resources WHERE id=? AND filename=?`
|
result, err := db.Db.Exec(`DELETE FROM resource WHERE id = ?`, delete.Id)
|
||||||
resource := Resource{}
|
if err != nil {
|
||||||
err := DB.QueryRow(query, id, filename).Scan(&resource.Id, &resource.Filename, &resource.Blob, &resource.Type, &resource.Size)
|
return FormatError(err)
|
||||||
return resource, FormatDBError(err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteResourceById(id string) error {
|
rows, _ := result.RowsAffected()
|
||||||
query := `DELETE FROM resources WHERE id=?`
|
if rows == 0 {
|
||||||
_, err := DB.Exec(query, id)
|
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource ID not found: %d", delete.Id)}
|
||||||
return FormatDBError(err)
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
4
store/seed/10000_reset.sql
Normal file
4
store/seed/10000_reset.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
DROP TABLE IF EXISTS `memo`;
|
||||||
|
DROP TABLE IF EXISTS `shortcut`;
|
||||||
|
DROP TABLE IF EXISTS `resource`;
|
||||||
|
DROP TABLE IF EXISTS `user`;
|
128
store/seed/10001_schema.sql
Normal file
128
store/seed/10001_schema.sql
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
-- 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', 100);
|
||||||
|
|
||||||
|
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,
|
||||||
|
-- allowed row status are 'NORMAL', 'ARCHIVED'.
|
||||||
|
row_status TEXT NOT NULL DEFAULT 'NORMAL',
|
||||||
|
FOREIGN KEY(creator_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO
|
||||||
|
sqlite_sequence (name, seq)
|
||||||
|
VALUES
|
||||||
|
('shortcut', 100);
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO
|
||||||
|
sqlite_sequence (name, seq)
|
||||||
|
VALUES
|
||||||
|
('resource', 100);
|
||||||
|
|
||||||
|
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'),
|
||||||
|
|
||||||
|
INSERT INTO memo
|
||||||
|
(`content`, `creator_id`)
|
||||||
|
VALUES
|
||||||
|
('👋 Welcome to memos', 1),
|
217
store/shortcut.go
Normal file
217
store/shortcut.go
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
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, row_status
|
||||||
|
`,
|
||||||
|
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.RowStatus,
|
||||||
|
); 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.RowStatus; v != nil {
|
||||||
|
set, args = append(set, "row_status = ?"), 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, 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 shortcut api.Shortcut
|
||||||
|
if err := row.Scan(
|
||||||
|
&shortcut.Id,
|
||||||
|
&shortcut.Title,
|
||||||
|
&shortcut.Payload,
|
||||||
|
&shortcut.CreatedTs,
|
||||||
|
&shortcut.UpdatedTs,
|
||||||
|
&shortcut.RowStatus,
|
||||||
|
); 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,
|
||||||
|
row_status
|
||||||
|
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.RowStatus,
|
||||||
|
); 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
|
||||||
|
}
|
96
store/sqlite.go
Normal file
96
store/sqlite.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
255
store/user.go
255
store/user.go
@ -1,129 +1,184 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"memos/utils"
|
"memos/api"
|
||||||
|
"memos/common"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type UserService struct {
|
||||||
Id string `json:"id"`
|
db *DB
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
OpenId string `json:"openId"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
UpdatedAt string `json:"updatedAt"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateNewUser(username string, password string) (User, error) {
|
func NewUserService(db *DB) *UserService {
|
||||||
nowDateTimeStr := utils.GetNowDateTimeStr()
|
return &UserService{db: db}
|
||||||
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 {
|
func (s *UserService) CreateUser(create *api.UserCreate) (*api.User, error) {
|
||||||
Username *string
|
user, err := createUser(s.db, create)
|
||||||
Password *string
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateUser(id string, updateUserPatch *UpdateUserPatch) (User, error) {
|
|
||||||
user := User{}
|
|
||||||
user, err := GetUserById(id)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, FormatDBError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) PatchUser(patch *api.UserPatch) (*api.User, error) {
|
||||||
|
user, err := patchUser(s.db, patch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) FindUser(find *api.UserFind) (*api.User, error) {
|
||||||
|
list, err := findUserList(s.db, find)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 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)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
`,
|
||||||
|
create.Name,
|
||||||
|
create.Password,
|
||||||
|
create.OpenId,
|
||||||
|
)
|
||||||
|
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 patchUser(db *DB, patch *api.UserPatch) (*api.User, error) {
|
||||||
set, args := []string{}, []interface{}{}
|
set, args := []string{}, []interface{}{}
|
||||||
|
|
||||||
if v := updateUserPatch.Username; v != nil {
|
if v := patch.Name; v != nil {
|
||||||
user.Username = *v
|
set, args = append(set, "name = ?"), append(args, v)
|
||||||
set, args = append(set, "username=?"), append(args, *v)
|
|
||||||
}
|
}
|
||||||
if v := updateUserPatch.Password; v != nil {
|
if v := patch.Password; v != nil {
|
||||||
user.Password = *v
|
set, args = append(set, "password = ?"), append(args, v)
|
||||||
set, args = append(set, "password=?"), append(args, *v)
|
|
||||||
}
|
}
|
||||||
set, args = append(set, "updated_at=?"), append(args, utils.GetNowDateTimeStr())
|
if v := patch.OpenId; v != nil {
|
||||||
args = append(args, id)
|
set, args = append(set, "open_id = ?"), append(args, v)
|
||||||
|
|
||||||
sqlQuery := `UPDATE users SET ` + strings.Join(set, ",") + ` WHERE id=?`
|
|
||||||
_, err = DB.Exec(sqlQuery, args...)
|
|
||||||
|
|
||||||
return user, FormatDBError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 CheckUsernameUsable(username string) (bool, error) {
|
|
||||||
query := `SELECT * FROM users WHERE username=?`
|
|
||||||
query = fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query)
|
|
||||||
|
|
||||||
var count uint
|
|
||||||
err := DB.QueryRow(query, username).Scan(&count)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
return false, FormatDBError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
usable := true
|
args = append(args, patch.Id)
|
||||||
if count > 0 {
|
|
||||||
usable = false
|
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) {
|
func findUserList(db *DB, find *api.UserFind) ([]*api.User, error) {
|
||||||
query := `SELECT * FROM users WHERE id=? AND password=?`
|
where, args := []string{"1 = 1"}, []interface{}{}
|
||||||
query = fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query)
|
|
||||||
|
|
||||||
var count uint
|
if v := find.Id; v != nil {
|
||||||
err := DB.QueryRow(query, id, password).Scan(&count)
|
where, args = append(where, "id = ?"), append(args, *v)
|
||||||
if err != nil && err != sql.ErrNoRows {
|
}
|
||||||
return false, FormatDBError(err)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
if count > 0 {
|
rows, err := db.Db.Query(`
|
||||||
return true, nil
|
SELECT
|
||||||
} else {
|
id,
|
||||||
return false, nil
|
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 {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil, FormatError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, &user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, FormatError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
@ -13,7 +13,7 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">
|
<p className="title-text">
|
||||||
<span className="icon-text">🤠</span>关于 <b>Memos</b>
|
<span className="icon-text">🤠</span>About <b>Memos</b>
|
||||||
</p>
|
</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<img className="icon-img" src="/icons/close.svg" />
|
<img className="icon-img" src="/icons/close.svg" />
|
||||||
@ -21,9 +21,9 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<p>
|
<p>
|
||||||
把玩 <a href="https://flomoapp.com">flomo</a> 后有感而作的开源项目
|
Memos is an open source, self-hosted alternative to <a href="https://flomoapp.com">flomo</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>特点:精美且细节的视觉样式、体验优良的交互逻辑</p>
|
<p>Built with `Golang` and `React`.</p>
|
||||||
<br />
|
<br />
|
||||||
<p>
|
<p>
|
||||||
🏗 This project is working in progress, <br /> and very pleasure to welcome your{" "}
|
🏗 This project is working in progress, <br /> and very pleasure to welcome your{" "}
|
||||||
|
@ -44,19 +44,19 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
|
|
||||||
const handleSaveBtnClick = async () => {
|
const handleSaveBtnClick = async () => {
|
||||||
if (oldPassword === "" || newPassword === "" || newPasswordAgain === "") {
|
if (oldPassword === "" || newPassword === "" || newPasswordAgain === "") {
|
||||||
toastHelper.error("密码不能为空");
|
toastHelper.error("Please fill in all fields.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword !== newPasswordAgain) {
|
if (newPassword !== newPasswordAgain) {
|
||||||
toastHelper.error("新密码两次输入不一致");
|
toastHelper.error("New passwords do not match.");
|
||||||
setNewPasswordAgain("");
|
setNewPasswordAgain("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordValidResult = validate(newPassword, validateConfig);
|
const passwordValidResult = validate(newPassword, validateConfig);
|
||||||
if (!passwordValidResult.result) {
|
if (!passwordValidResult.result) {
|
||||||
toastHelper.error("密码 " + passwordValidResult.reason);
|
toastHelper.error("Password " + passwordValidResult.reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,13 +64,13 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
const isValid = await userService.checkPasswordValid(oldPassword);
|
const isValid = await userService.checkPasswordValid(oldPassword);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
toastHelper.error("旧密码不匹配");
|
toastHelper.error("Old password is invalid.");
|
||||||
setOldPassword("");
|
setOldPassword("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await userService.updatePassword(newPassword);
|
await userService.updatePassword(newPassword);
|
||||||
toastHelper.info("密码修改成功!");
|
toastHelper.info("Password changed.");
|
||||||
handleCloseBtnClick();
|
handleCloseBtnClick();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toastHelper.error(error);
|
toastHelper.error(error);
|
||||||
@ -80,30 +80,30 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">修改密码</p>
|
<p className="title-text">Change Password</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<img className="icon-img" src="/icons/close.svg" />
|
<img className="icon-img" src="/icons/close.svg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<label className="form-label input-form-label">
|
<label className="form-label input-form-label">
|
||||||
<span className={"normal-text " + (oldPassword === "" ? "" : "not-null")}>旧密码</span>
|
<span className={"normal-text " + (oldPassword === "" ? "" : "not-null")}>Old password</span>
|
||||||
<input type="password" value={oldPassword} onChange={handleOldPasswordChanged} />
|
<input type="password" value={oldPassword} onChange={handleOldPasswordChanged} />
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label input-form-label">
|
<label className="form-label input-form-label">
|
||||||
<span className={"normal-text " + (newPassword === "" ? "" : "not-null")}>新密码</span>
|
<span className={"normal-text " + (newPassword === "" ? "" : "not-null")}>New passworld</span>
|
||||||
<input type="password" value={newPassword} onChange={handleNewPasswordChanged} />
|
<input type="password" value={newPassword} onChange={handleNewPasswordChanged} />
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label input-form-label">
|
<label className="form-label input-form-label">
|
||||||
<span className={"normal-text " + (newPasswordAgain === "" ? "" : "not-null")}>再次输入新密码</span>
|
<span className={"normal-text " + (newPasswordAgain === "" ? "" : "not-null")}>New password again</span>
|
||||||
<input type="password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
|
<input type="password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
|
||||||
</label>
|
</label>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||||
取消
|
Cancel
|
||||||
</span>
|
</span>
|
||||||
<span className="btn confirm-btn" onClick={handleSaveBtnClick}>
|
<span className="btn confirm-btn" onClick={handleSaveBtnClick}>
|
||||||
保存
|
Save
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,29 +27,31 @@ const ConfirmResetOpenIdDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
try {
|
try {
|
||||||
await userService.resetOpenId();
|
await userService.resetOpenId();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toastHelper.error("请求重置 Open API 失败");
|
toastHelper.error("Request reset open API failed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toastHelper.success("重置成功!");
|
toastHelper.success("Reset open API succeeded.");
|
||||||
handleCloseBtnClick();
|
handleCloseBtnClick();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">重置 Open API</p>
|
<p className="title-text">Reset Open API</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<img className="icon-img" src="/icons/close.svg" />
|
<img className="icon-img" src="/icons/close.svg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<p className="warn-text">⚠️ 现有 API 将失效,并生成新的 API,确定要重置吗?</p>
|
<p className="warn-text">
|
||||||
|
⚠️ The existing API will be invalidated and a new one will be generated, are you sure you want to reset?
|
||||||
|
</p>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||||
取消
|
Cancel
|
||||||
</span>
|
</span>
|
||||||
<span className={`btn confirm-btn ${resetBtnClickLoadingState.isLoading ? "loading" : ""}`} onClick={handleConfirmBtnClick}>
|
<span className={`btn confirm-btn ${resetBtnClickLoadingState.isLoading ? "loading" : ""}`} onClick={handleConfirmBtnClick}>
|
||||||
确定重置!
|
Reset!
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from "react";
|
import { memo, useCallback, useEffect, useState } from "react";
|
||||||
import { memoService, queryService } from "../services";
|
import { memoService, shortcutService } from "../services";
|
||||||
import { checkShouldShowMemoWithFilters, filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter";
|
import { checkShouldShowMemoWithFilters, filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import { showDialog } from "./Dialog";
|
import { showDialog } from "./Dialog";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
import Selector from "./common/Selector";
|
import Selector from "./common/Selector";
|
||||||
import "../less/create-query-dialog.less";
|
import "../less/create-shortcut-dialog.less";
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
queryId?: string;
|
shortcutId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateQueryDialog: React.FC<Props> = (props: Props) => {
|
const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||||
const { destroy, queryId } = props;
|
const { destroy, shortcutId } = props;
|
||||||
|
|
||||||
const [title, setTitle] = useState<string>("");
|
const [title, setTitle] = useState<string>("");
|
||||||
const [filters, setFilters] = useState<Filter[]>([]);
|
const [filters, setFilters] = useState<Filter[]>([]);
|
||||||
@ -23,15 +23,15 @@ const CreateQueryDialog: React.FC<Props> = (props: Props) => {
|
|||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const queryTemp = queryService.getQueryById(queryId ?? "");
|
const shortcutTemp = shortcutService.getShortcutById(shortcutId ?? "");
|
||||||
if (queryTemp) {
|
if (shortcutTemp) {
|
||||||
setTitle(queryTemp.title);
|
setTitle(shortcutTemp.title);
|
||||||
const temp = JSON.parse(queryTemp.querystring);
|
const temp = JSON.parse(shortcutTemp.payload);
|
||||||
if (Array.isArray(temp)) {
|
if (Array.isArray(temp)) {
|
||||||
setFilters(temp);
|
setFilters(temp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [queryId]);
|
}, [shortcutId]);
|
||||||
|
|
||||||
const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const text = e.target.value as string;
|
const text = e.target.value as string;
|
||||||
@ -40,17 +40,17 @@ const CreateQueryDialog: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
const handleSaveBtnClick = async () => {
|
const handleSaveBtnClick = async () => {
|
||||||
if (!title) {
|
if (!title) {
|
||||||
toastHelper.error("标题不能为空!");
|
toastHelper.error("Title is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (queryId) {
|
if (shortcutId) {
|
||||||
const editedQuery = await queryService.updateQuery(queryId, title, JSON.stringify(filters));
|
const editedShortcut = await shortcutService.updateShortcut(shortcutId, title, JSON.stringify(filters));
|
||||||
queryService.editQuery(editedQuery);
|
shortcutService.editShortcut(shortcutService.convertResponseModelShortcut(editedShortcut));
|
||||||
} else {
|
} else {
|
||||||
const query = await queryService.createQuery(title, JSON.stringify(filters));
|
const shortcut = await shortcutService.createShortcut(title, JSON.stringify(filters));
|
||||||
queryService.pushQuery(query);
|
shortcutService.pushShortcut(shortcutService.convertResponseModelShortcut(shortcut));
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toastHelper.error(error.message);
|
toastHelper.error(error.message);
|
||||||
@ -62,7 +62,7 @@ const CreateQueryDialog: React.FC<Props> = (props: Props) => {
|
|||||||
if (filters.length > 0) {
|
if (filters.length > 0) {
|
||||||
const lastFilter = filters[filters.length - 1];
|
const lastFilter = filters[filters.length - 1];
|
||||||
if (lastFilter.value.value === "") {
|
if (lastFilter.value.value === "") {
|
||||||
toastHelper.info("先完善上一个过滤器吧");
|
toastHelper.info("Please fill in previous filter value");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ const CreateQueryDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">
|
<p className="title-text">
|
||||||
<span className="icon-text">🔖</span>
|
<span className="icon-text">🔖</span>
|
||||||
{queryId ? "编辑检索" : "创建检索"}
|
{shortcutId ? "Edit Shortcut" : "Create Shortcut"}
|
||||||
</p>
|
</p>
|
||||||
<button className="btn close-btn" onClick={destroy}>
|
<button className="btn close-btn" onClick={destroy}>
|
||||||
<img className="icon-img" src="/icons/close.svg" />
|
<img className="icon-img" src="/icons/close.svg" />
|
||||||
@ -98,11 +98,11 @@ const CreateQueryDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<div className="form-item-container input-form-container">
|
<div className="form-item-container input-form-container">
|
||||||
<span className="normal-text">标题</span>
|
<span className="normal-text">Title</span>
|
||||||
<input className="title-input" type="text" value={title} onChange={handleTitleInputChange} />
|
<input className="title-input" type="text" value={title} onChange={handleTitleInputChange} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-item-container filter-form-container">
|
<div className="form-item-container filter-form-container">
|
||||||
<span className="normal-text">过滤器</span>
|
<span className="normal-text">Filter</span>
|
||||||
<div className="filters-wrapper">
|
<div className="filters-wrapper">
|
||||||
{filters.map((f, index) => {
|
{filters.map((f, index) => {
|
||||||
return (
|
return (
|
||||||
@ -116,7 +116,7 @@ const CreateQueryDialog: React.FC<Props> = (props: Props) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="create-filter-btn" onClick={handleAddFilterBenClick}>
|
<div className="create-filter-btn" onClick={handleAddFilterBenClick}>
|
||||||
添加筛选条件
|
New Filter
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -125,10 +125,10 @@ const CreateQueryDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<div></div>
|
<div></div>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<span className={`tip-text ${filters.length === 0 && "hidden"}`}>
|
<span className={`tip-text ${filters.length === 0 && "hidden"}`}>
|
||||||
符合条件的 Memo 有 <strong>{shownMemoLength}</strong> 条
|
<strong>{shownMemoLength}</strong> eligible memo
|
||||||
</span>
|
</span>
|
||||||
<button className={`btn save-btn ${requestState.isLoading ? "requesting" : ""}`} onClick={handleSaveBtnClick}>
|
<button className={`btn save-btn ${requestState.isLoading ? "requesting" : ""}`} onClick={handleSaveBtnClick}>
|
||||||
保存
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -298,12 +298,12 @@ const FilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInpute
|
|||||||
|
|
||||||
const MemoFilterInputer: React.FC<MemoFilterInputerProps> = memo(FilterInputer);
|
const MemoFilterInputer: React.FC<MemoFilterInputerProps> = memo(FilterInputer);
|
||||||
|
|
||||||
export default function showCreateQueryDialog(queryId?: string): void {
|
export default function showCreateShortcutDialog(shortcutId?: string): void {
|
||||||
showDialog(
|
showDialog(
|
||||||
{
|
{
|
||||||
className: "create-query-dialog",
|
className: "create-shortcut-dialog",
|
||||||
},
|
},
|
||||||
CreateQueryDialog,
|
CreateShortcutDialog,
|
||||||
{ queryId }
|
{ shortcutId }
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -106,11 +106,11 @@ const DailyMemoDiaryDialog: React.FC<Props> = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
{loadingState.isLoading ? (
|
{loadingState.isLoading ? (
|
||||||
<div className="tip-container">
|
<div className="tip-container">
|
||||||
<p className="tip-text">努力加载中...</p>
|
<p className="tip-text">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
) : memos.length === 0 ? (
|
) : memos.length === 0 ? (
|
||||||
<div className="tip-container">
|
<div className="tip-container">
|
||||||
<p className="tip-text">空空如也</p>
|
<p className="tip-text">Oops, there is nothing.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="dailymemos-wrapper">
|
<div className="dailymemos-wrapper">
|
||||||
|
@ -18,7 +18,7 @@ const DeletedMemo: React.FC<Props> = (props: Props) => {
|
|||||||
const memo: FormattedMemo = {
|
const memo: FormattedMemo = {
|
||||||
...propsMemo,
|
...propsMemo,
|
||||||
createdAtStr: utils.getDateTimeString(propsMemo.createdAt),
|
createdAtStr: utils.getDateTimeString(propsMemo.createdAt),
|
||||||
deletedAtStr: utils.getDateTimeString(propsMemo.deletedAt ?? Date.now()),
|
deletedAtStr: utils.getDateTimeString(propsMemo.updatedAt ?? Date.now()),
|
||||||
};
|
};
|
||||||
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
|
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
|
||||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []);
|
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []);
|
||||||
@ -40,7 +40,7 @@ const DeletedMemo: React.FC<Props> = (props: Props) => {
|
|||||||
try {
|
try {
|
||||||
await memoService.restoreMemoById(memo.id);
|
await memoService.restoreMemoById(memo.id);
|
||||||
handleDeletedMemoAction(memo.id);
|
handleDeletedMemoAction(memo.id);
|
||||||
toastHelper.info("恢复成功");
|
toastHelper.info("Restored successfully");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toastHelper.error(error.message);
|
toastHelper.error(error.message);
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ const DeletedMemo: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className={`memo-wrapper ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
|
<div className={`memo-wrapper ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
|
||||||
<div className="memo-top-wrapper">
|
<div className="memo-top-wrapper">
|
||||||
<span className="time-text">删除于 {memo.deletedAtStr}</span>
|
<span className="time-text">Deleted at {memo.deletedAtStr}</span>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<span className="btn more-action-btn">
|
<span className="btn more-action-btn">
|
||||||
<img className="icon-img" src="/icons/more.svg" />
|
<img className="icon-img" src="/icons/more.svg" />
|
||||||
@ -63,10 +63,10 @@ const DeletedMemo: React.FC<Props> = (props: Props) => {
|
|||||||
<div className="more-action-btns-wrapper">
|
<div className="more-action-btns-wrapper">
|
||||||
<div className="more-action-btns-container">
|
<div className="more-action-btns-container">
|
||||||
<span className="btn restore-btn" onClick={handleRestoreMemoClick}>
|
<span className="btn restore-btn" onClick={handleRestoreMemoClick}>
|
||||||
恢复
|
Restore
|
||||||
</span>
|
</span>
|
||||||
<span className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`} onClick={handleDeleteMemoClick}>
|
<span className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`} onClick={handleDeleteMemoClick}>
|
||||||
{showConfirmDeleteBtn ? "确定删除!" : "完全删除"}
|
{showConfirmDeleteBtn ? "Delete!" : "Delete"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -176,12 +176,12 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
|
|||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<Only when={showCancelBtn}>
|
<Only when={showCancelBtn}>
|
||||||
<button className="action-btn cancel-btn" onClick={handleCommonCancelBtnClick}>
|
<button className="action-btn cancel-btn" onClick={handleCommonCancelBtnClick}>
|
||||||
撤销修改
|
Cancel editting
|
||||||
</button>
|
</button>
|
||||||
</Only>
|
</Only>
|
||||||
<Only when={showConfirmBtn}>
|
<Only when={showConfirmBtn}>
|
||||||
<button className="action-btn confirm-btn" disabled={!editorRef.current?.value} onClick={handleCommonConfirmBtnClick}>
|
<button className="action-btn confirm-btn" disabled={!editorRef.current?.value} onClick={handleCommonConfirmBtnClick}>
|
||||||
记下<span className="icon-text">✍️</span>
|
Save <span className="icon-text">✍️</span>
|
||||||
</button>
|
</button>
|
||||||
</Only>
|
</Only>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,19 +93,19 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
<div className="more-action-btns-wrapper">
|
<div className="more-action-btns-wrapper">
|
||||||
<div className="more-action-btns-container">
|
<div className="more-action-btns-container">
|
||||||
<span className="btn" onClick={handleShowMemoStoryDialog}>
|
<span className="btn" onClick={handleShowMemoStoryDialog}>
|
||||||
查看详情
|
View Story
|
||||||
</span>
|
</span>
|
||||||
<span className="btn" onClick={handleMarkMemoClick}>
|
<span className="btn" onClick={handleMarkMemoClick}>
|
||||||
Mark
|
Mark
|
||||||
</span>
|
</span>
|
||||||
<span className="btn" onClick={handleGenMemoImageBtnClick}>
|
<span className="btn" onClick={handleGenMemoImageBtnClick}>
|
||||||
分享
|
Share
|
||||||
</span>
|
</span>
|
||||||
<span className="btn" onClick={handleEditMemoClick}>
|
<span className="btn" onClick={handleEditMemoClick}>
|
||||||
编辑
|
Edit
|
||||||
</span>
|
</span>
|
||||||
<span className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`} onClick={handleDeleteMemoClick}>
|
<span className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`} onClick={handleDeleteMemoClick}>
|
||||||
{showConfirmDeleteBtn ? "确定删除!" : "删除"}
|
{showConfirmDeleteBtn ? "Delete!" : "Delete"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -151,7 +151,7 @@ export function formatMemoContent(content: string) {
|
|||||||
.replace(LINK_REG, "<a class='link' target='_blank' rel='noreferrer' href='$1'>$1</a>")
|
.replace(LINK_REG, "<a class='link' target='_blank' rel='noreferrer' href='$1'>$1</a>")
|
||||||
.replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>");
|
.replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>");
|
||||||
|
|
||||||
// 中英文之间加空格
|
// Add space in english and chinese
|
||||||
if (shouldSplitMemoWord) {
|
if (shouldSplitMemoWord) {
|
||||||
content = content
|
content = content
|
||||||
.replace(/([\u4e00-\u9fa5])([A-Za-z0-9?.,;[\]]+)/g, "$1 $2")
|
.replace(/([\u4e00-\u9fa5])([A-Za-z0-9?.,;[\]]+)/g, "$1 $2")
|
||||||
|
@ -148,7 +148,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
{linkMemos.length > 0 ? (
|
{linkMemos.length > 0 ? (
|
||||||
<div className="linked-memos-wrapper">
|
<div className="linked-memos-wrapper">
|
||||||
<p className="normal-text">关联了 {linkMemos.length} 个 MEMO</p>
|
<p className="normal-text">{linkMemos.length} related MEMO</p>
|
||||||
{linkMemos.map((m) => {
|
{linkMemos.map((m) => {
|
||||||
const rawtext = parseHtmlToRawText(formatMemoContent(m.content)).replaceAll("\n", " ");
|
const rawtext = parseHtmlToRawText(formatMemoContent(m.content)).replaceAll("\n", " ");
|
||||||
return (
|
return (
|
||||||
@ -162,7 +162,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
|||||||
) : null}
|
) : null}
|
||||||
{linkedMemos.length > 0 ? (
|
{linkedMemos.length > 0 ? (
|
||||||
<div className="linked-memos-wrapper">
|
<div className="linked-memos-wrapper">
|
||||||
<p className="normal-text">{linkedMemos.length} 个链接至此的 MEMO</p>
|
<p className="normal-text">{linkedMemos.length} linked MEMO</p>
|
||||||
{linkedMemos.map((m) => {
|
{linkedMemos.map((m) => {
|
||||||
const rawtext = parseHtmlToRawText(formatMemoContent(m.content)).replaceAll("\n", " ");
|
const rawtext = parseHtmlToRawText(formatMemoContent(m.content)).replaceAll("\n", " ");
|
||||||
return (
|
return (
|
||||||
|
@ -130,7 +130,7 @@ const MemoEditor: React.FC<Props> = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const image = await resourceService.upload(file);
|
const image = await resourceService.upload(file);
|
||||||
const url = `/r/${image.id}/${image.filename}`;
|
const url = `/h/r/${image.id}/${image.filename}`;
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -140,7 +140,7 @@ const MemoEditor: React.FC<Props> = () => {
|
|||||||
|
|
||||||
const handleSaveBtnClick = useCallback(async (content: string) => {
|
const handleSaveBtnClick = useCallback(async (content: string) => {
|
||||||
if (content === "") {
|
if (content === "") {
|
||||||
toastHelper.error("内容不能为空呀");
|
toastHelper.error("Content can't be empty");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,7 +270,7 @@ const MemoEditor: React.FC<Props> = () => {
|
|||||||
() => ({
|
() => ({
|
||||||
className: "memo-editor",
|
className: "memo-editor",
|
||||||
initialContent: getEditorContentCache(),
|
initialContent: getEditorContentCache(),
|
||||||
placeholder: "现在的想法是...",
|
placeholder: "Any thoughts...",
|
||||||
showConfirmBtn: true,
|
showConfirmBtn: true,
|
||||||
showCancelBtn: showEditStatus,
|
showCancelBtn: showEditStatus,
|
||||||
onConfirmBtnClick: handleSaveBtnClick,
|
onConfirmBtnClick: handleSaveBtnClick,
|
||||||
@ -282,7 +282,7 @@ const MemoEditor: React.FC<Props> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"memo-editor-wrapper " + (showEditStatus ? "edit-ing" : "")}>
|
<div className={"memo-editor-wrapper " + (showEditStatus ? "edit-ing" : "")}>
|
||||||
<p className={"tip-text " + (showEditStatus ? "" : "hidden")}>正在修改中...</p>
|
<p className={"tip-text " + (showEditStatus ? "" : "hidden")}>Editting...</p>
|
||||||
<Editor
|
<Editor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
{...editorConfig}
|
{...editorConfig}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import appContext from "../stores/appContext";
|
import appContext from "../stores/appContext";
|
||||||
import { locationService, queryService } from "../services";
|
import { locationService, shortcutService } from "../services";
|
||||||
import utils from "../helpers/utils";
|
import utils from "../helpers/utils";
|
||||||
import { getTextWithMemoType } from "../helpers/filter";
|
import { getTextWithMemoType } from "../helpers/filter";
|
||||||
import "../less/memo-filter.less";
|
import "../less/memo-filter.less";
|
||||||
@ -12,17 +12,17 @@ const MemoFilter: React.FC<FilterProps> = () => {
|
|||||||
locationState: { query },
|
locationState: { query },
|
||||||
} = useContext(appContext);
|
} = useContext(appContext);
|
||||||
|
|
||||||
const { tag: tagQuery, duration, type: memoType, text: textQuery, filter } = query;
|
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query;
|
||||||
const queryFilter = queryService.getQueryById(filter);
|
const queryFilter = shortcutService.getShortcutById(shortcutId);
|
||||||
const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || queryFilter);
|
const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || queryFilter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`filter-query-container ${showFilter ? "" : "hidden"}`}>
|
<div className={`filter-query-container ${showFilter ? "" : "hidden"}`}>
|
||||||
<span className="tip-text">筛选:</span>
|
<span className="tip-text">Filter:</span>
|
||||||
<div
|
<div
|
||||||
className={"filter-item-container " + (queryFilter ? "" : "hidden")}
|
className={"filter-item-container " + (queryFilter ? "" : "hidden")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
locationService.setMemoFilter("");
|
locationService.setMemoShortcut("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="icon-text">🔖</span> {queryFilter?.title}
|
<span className="icon-text">🔖</span> {queryFilter?.title}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
import appContext from "../stores/appContext";
|
import appContext from "../stores/appContext";
|
||||||
import { locationService, memoService, queryService } from "../services";
|
import { locationService, memoService, shortcutService } from "../services";
|
||||||
import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts";
|
import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts";
|
||||||
import utils from "../helpers/utils";
|
import utils from "../helpers/utils";
|
||||||
import { checkShouldShowMemoWithFilters } from "../helpers/filter";
|
import { checkShouldShowMemoWithFilters } from "../helpers/filter";
|
||||||
@ -18,8 +18,8 @@ const MemoList: React.FC<Props> = () => {
|
|||||||
const [isFetching, setFetchStatus] = useState(true);
|
const [isFetching, setFetchStatus] = useState(true);
|
||||||
const wrapperElement = useRef<HTMLDivElement>(null);
|
const wrapperElement = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { tag: tagQuery, duration, type: memoType, text: textQuery, filter: queryId } = query;
|
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query;
|
||||||
const queryFilter = queryService.getQueryById(queryId);
|
const queryFilter = shortcutService.getShortcutById(shortcutId);
|
||||||
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || queryFilter);
|
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || queryFilter);
|
||||||
|
|
||||||
const shownMemos =
|
const shownMemos =
|
||||||
@ -28,7 +28,7 @@ const MemoList: React.FC<Props> = () => {
|
|||||||
let shouldShow = true;
|
let shouldShow = true;
|
||||||
|
|
||||||
if (queryFilter) {
|
if (queryFilter) {
|
||||||
const filters = JSON.parse(queryFilter.querystring) as Filter[];
|
const filters = JSON.parse(queryFilter.payload) as Filter[];
|
||||||
if (Array.isArray(filters)) {
|
if (Array.isArray(filters)) {
|
||||||
shouldShow = checkShouldShowMemoWithFilters(memo, filters);
|
shouldShow = checkShouldShowMemoWithFilters(memo, filters);
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ const MemoList: React.FC<Props> = () => {
|
|||||||
setFetchStatus(false);
|
setFetchStatus(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toastHelper.error("😭 请求数据失败了");
|
toastHelper.error("😭 Refresh failed, please try again later.");
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -111,7 +111,13 @@ const MemoList: React.FC<Props> = () => {
|
|||||||
))}
|
))}
|
||||||
<div className="status-text-container">
|
<div className="status-text-container">
|
||||||
<p className="status-text">
|
<p className="status-text">
|
||||||
{isFetching ? "努力请求数据中..." : shownMemos.length === 0 ? "空空如也" : showMemoFilter ? "" : "所有数据加载完啦 🎉"}
|
{isFetching
|
||||||
|
? "Fetching data..."
|
||||||
|
: shownMemos.length === 0
|
||||||
|
? "Oops, there is nothing"
|
||||||
|
: showMemoFilter
|
||||||
|
? ""
|
||||||
|
: "Fetching completed 🎉"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import appContext from "../stores/appContext";
|
import appContext from "../stores/appContext";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import { globalStateService, memoService, queryService } from "../services";
|
import { globalStateService, memoService, shortcutService } from "../services";
|
||||||
import Only from "./common/OnlyWhen";
|
import Only from "./common/OnlyWhen";
|
||||||
import "../less/memos-header.less";
|
import "../less/memos-header.less";
|
||||||
|
|
||||||
@ -12,22 +12,22 @@ interface Props {}
|
|||||||
const MemosHeader: React.FC<Props> = () => {
|
const MemosHeader: React.FC<Props> = () => {
|
||||||
const {
|
const {
|
||||||
locationState: {
|
locationState: {
|
||||||
query: { filter },
|
query: { shortcutId },
|
||||||
},
|
},
|
||||||
globalState: { isMobileView },
|
globalState: { isMobileView },
|
||||||
queryState: { queries },
|
shortcutState: { shortcuts },
|
||||||
} = useContext(appContext);
|
} = useContext(appContext);
|
||||||
|
|
||||||
const [titleText, setTitleText] = useState("MEMOS");
|
const [titleText, setTitleText] = useState("MEMOS");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const query = queryService.getQueryById(filter);
|
const query = shortcutService.getShortcutById(shortcutId);
|
||||||
if (query) {
|
if (query) {
|
||||||
setTitleText(query.title);
|
setTitleText(query.title);
|
||||||
} else {
|
} else {
|
||||||
setTitleText("MEMOS");
|
setTitleText("MEMOS");
|
||||||
}
|
}
|
||||||
}, [filter, queries]);
|
}, [shortcutId, shortcuts]);
|
||||||
|
|
||||||
const handleMemoTextClick = useCallback(() => {
|
const handleMemoTextClick = useCallback(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
@ -49,16 +49,16 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className={`menu-btns-popup ${shownStatus ? "" : "hidden"}`} ref={popupElRef}>
|
<div className={`menu-btns-popup ${shownStatus ? "" : "hidden"}`} ref={popupElRef}>
|
||||||
<button className="btn action-btn" onClick={handleMyAccountBtnClick}>
|
<button className="btn action-btn" onClick={handleMyAccountBtnClick}>
|
||||||
<span className="icon">👤</span> 账号与设置
|
<span className="icon">👤</span> Settings
|
||||||
</button>
|
</button>
|
||||||
<button className="btn action-btn" onClick={handleMemosTrashBtnClick}>
|
<button className="btn action-btn" onClick={handleMemosTrashBtnClick}>
|
||||||
<span className="icon">🗑️</span> 回收站
|
<span className="icon">🗑️</span> Recycle Bin
|
||||||
</button>
|
</button>
|
||||||
<button className="btn action-btn" onClick={handleAboutBtnClick}>
|
<button className="btn action-btn" onClick={handleAboutBtnClick}>
|
||||||
<span className="icon">🤠</span> 关于
|
<span className="icon">🤠</span> About
|
||||||
</button>
|
</button>
|
||||||
<button className="btn action-btn" onClick={handleSignOutBtnClick}>
|
<button className="btn action-btn" onClick={handleSignOutBtnClick}>
|
||||||
<span className="icon">👋</span> 退出
|
<span className="icon">👋</span> Sign out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -20,8 +20,8 @@ interface Props {}
|
|||||||
const MyAccountSection: React.FC<Props> = () => {
|
const MyAccountSection: React.FC<Props> = () => {
|
||||||
const { userState } = useContext(appContext);
|
const { userState } = useContext(appContext);
|
||||||
const user = userState.user as Model.User;
|
const user = userState.user as Model.User;
|
||||||
const [username, setUsername] = useState<string>(user.username);
|
const [username, setUsername] = useState<string>(user.name);
|
||||||
const openAPIRoute = `${window.location.origin}/api/whs/memo/${user.openId}`;
|
const openAPIRoute = `${window.location.origin}/h/${user.openId}/memo`;
|
||||||
|
|
||||||
const handleUsernameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleUsernameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const nextUsername = e.target.value as string;
|
const nextUsername = e.target.value as string;
|
||||||
@ -29,18 +29,18 @@ const MyAccountSection: React.FC<Props> = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmEditUsernameBtnClick = async () => {
|
const handleConfirmEditUsernameBtnClick = async () => {
|
||||||
if (user.username === "guest") {
|
if (user.name === "guest") {
|
||||||
toastHelper.info("🈲 不要修改我的用户名");
|
toastHelper.info("Do not change my username");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username === user.username) {
|
if (username === user.name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usernameValidResult = validate(username, validateConfig);
|
const usernameValidResult = validate(username, validateConfig);
|
||||||
if (!usernameValidResult.result) {
|
if (!usernameValidResult.result) {
|
||||||
toastHelper.error("用户名 " + usernameValidResult.reason);
|
toastHelper.error("Username " + usernameValidResult.reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,21 +48,21 @@ const MyAccountSection: React.FC<Props> = () => {
|
|||||||
const isUsable = await userService.checkUsernameUsable(username);
|
const isUsable = await userService.checkUsernameUsable(username);
|
||||||
|
|
||||||
if (!isUsable) {
|
if (!isUsable) {
|
||||||
toastHelper.error("用户名无法使用");
|
toastHelper.error("Username is not available");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await userService.updateUsername(username);
|
await userService.updateUsername(username);
|
||||||
await userService.doSignIn();
|
await userService.doSignIn();
|
||||||
toastHelper.info("修改成功~");
|
toastHelper.info("Username changed");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toastHelper.error(error.message);
|
toastHelper.error(error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangePasswordBtnClick = () => {
|
const handleChangePasswordBtnClick = () => {
|
||||||
if (user.username === "guest") {
|
if (user.name === "guest") {
|
||||||
toastHelper.info("🈲 不要修改我的密码");
|
toastHelper.info("Do not change my password");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,47 +81,47 @@ const MyAccountSection: React.FC<Props> = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="section-container account-section-container">
|
<div className="section-container account-section-container">
|
||||||
<p className="title-text">基本信息</p>
|
<p className="title-text">Account Information</p>
|
||||||
<label className="form-label input-form-label">
|
<label className="form-label input-form-label">
|
||||||
<span className="normal-text">ID:</span>
|
<span className="normal-text">ID:</span>
|
||||||
<span className="normal-text">{user.id}</span>
|
<span className="normal-text">{user.id}</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label input-form-label">
|
<label className="form-label input-form-label">
|
||||||
<span className="normal-text">创建时间:</span>
|
<span className="normal-text">Created at:</span>
|
||||||
<span className="normal-text">{utils.getDateString(user.createdAt)}</span>
|
<span className="normal-text">{utils.getDateString(user.createdAt)}</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label input-form-label username-label">
|
<label className="form-label input-form-label username-label">
|
||||||
<span className="normal-text">账号:</span>
|
<span className="normal-text">Username:</span>
|
||||||
<input type="text" value={username} onChange={handleUsernameChanged} />
|
<input type="text" value={username} onChange={handleUsernameChanged} />
|
||||||
<div className={`btns-container ${username === user.username ? "hidden" : ""}`} onClick={handlePreventDefault}>
|
<div className={`btns-container ${username === user.name ? "hidden" : ""}`} onClick={handlePreventDefault}>
|
||||||
<span className="btn confirm-btn" onClick={handleConfirmEditUsernameBtnClick}>
|
<span className="btn confirm-btn" onClick={handleConfirmEditUsernameBtnClick}>
|
||||||
保存
|
Save
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="btn cancel-btn"
|
className="btn cancel-btn"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUsername(user.username);
|
setUsername(user.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
撤销
|
Cancel
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label password-label">
|
<label className="form-label password-label">
|
||||||
<span className="normal-text">密码:</span>
|
<span className="normal-text">Password:</span>
|
||||||
<span className="btn" onClick={handleChangePasswordBtnClick}>
|
<span className="btn" onClick={handleChangePasswordBtnClick}>
|
||||||
修改密码
|
Change It
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-container openapi-section-container">
|
<div className="section-container openapi-section-container">
|
||||||
<p className="title-text">Open API(实验性功能)</p>
|
<p className="title-text">Open API (Experimental feature)</p>
|
||||||
<p className="value-text">{openAPIRoute}</p>
|
<p className="value-text">{openAPIRoute}</p>
|
||||||
<span className="reset-btn" onClick={handleResetOpenIdBtnClick}>
|
<span className="reset-btn" onClick={handleResetOpenIdBtnClick}>
|
||||||
重置 API
|
Reset API
|
||||||
</span>
|
</span>
|
||||||
<div className="usage-guide-container">
|
<div className="usage-guide-container">
|
||||||
<p className="title-text">使用方法:</p>
|
<p className="title-text">Usage guide:</p>
|
||||||
<pre>{`POST ${openAPIRoute}\nContent-type: application/json\n{\n "content": "Hello, #memos ${window.location.origin}"\n}`}</pre>
|
<pre>{`POST ${openAPIRoute}\nContent-type: application/json\n{\n "content": "Hello, #memos ${window.location.origin}"\n}`}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,7 @@ const PreferencesSection: React.FC<Props> = () => {
|
|||||||
const { globalState } = useContext(appContext);
|
const { globalState } = useContext(appContext);
|
||||||
const { useTinyUndoHistoryCache, shouldHideImageUrl, shouldSplitMemoWord, shouldUseMarkdownParser } = globalState;
|
const { useTinyUndoHistoryCache, shouldHideImageUrl, shouldSplitMemoWord, shouldUseMarkdownParser } = globalState;
|
||||||
|
|
||||||
const demoMemoContent = "👋 你好呀~欢迎使用memos!\n* ✨ **开源项目**;\n* 😋 精美且细节的视觉样式;\n* 📑 体验优良的交互逻辑;";
|
const demoMemoContent = "👋 Hiya, welcome to memos!\n* ✨ **Open source project**;\n* 😋 What do you think;\n* 📑 Tell me something plz;";
|
||||||
|
|
||||||
const handleOpenTinyUndoChanged = () => {
|
const handleOpenTinyUndoChanged = () => {
|
||||||
globalStateService.setAppSetting({
|
globalStateService.setAppSetting({
|
||||||
@ -65,29 +65,29 @@ const PreferencesSection: React.FC<Props> = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="section-container preferences-section-container">
|
<div className="section-container preferences-section-container">
|
||||||
<p className="title-text">Memo 显示相关</p>
|
<p className="title-text">Memo Display</p>
|
||||||
<div
|
<div
|
||||||
className="demo-content-container memo-content-text"
|
className="demo-content-container memo-content-text"
|
||||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(demoMemoContent) }}
|
dangerouslySetInnerHTML={{ __html: formatMemoContent(demoMemoContent) }}
|
||||||
></div>
|
></div>
|
||||||
<label className="form-label checkbox-form-label" onClick={handleSplitWordsValueChanged}>
|
<label className="form-label checkbox-form-label hidden" onClick={handleSplitWordsValueChanged}>
|
||||||
<span className="normal-text">中英文内容自动间隔</span>
|
<span className="normal-text">Auto-space in English and Chinese</span>
|
||||||
<img className="icon-img" src={shouldSplitMemoWord ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
|
<img className="icon-img" src={shouldSplitMemoWord ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label checkbox-form-label" onClick={handleUseMarkdownParserChanged}>
|
<label className="form-label checkbox-form-label" onClick={handleUseMarkdownParserChanged}>
|
||||||
<span className="normal-text">部分 markdown 格式解析</span>
|
<span className="normal-text">Partial markdown format parsing</span>
|
||||||
<img className="icon-img" src={shouldUseMarkdownParser ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
|
<img className="icon-img" src={shouldUseMarkdownParser ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label checkbox-form-label" onClick={handleHideImageUrlValueChanged}>
|
<label className="form-label checkbox-form-label" onClick={handleHideImageUrlValueChanged}>
|
||||||
<span className="normal-text">隐藏图片链接地址</span>
|
<span className="normal-text">Hide image url</span>
|
||||||
<img className="icon-img" src={shouldHideImageUrl ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
|
<img className="icon-img" src={shouldHideImageUrl ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-container preferences-section-container">
|
<div className="section-container preferences-section-container">
|
||||||
<p className="title-text">编辑器</p>
|
<p className="title-text">Editor Extensions</p>
|
||||||
<label className="form-label checkbox-form-label" onClick={handleOpenTinyUndoChanged}>
|
<label className="form-label checkbox-form-label" onClick={handleOpenTinyUndoChanged}>
|
||||||
<span className="normal-text">
|
<span className="normal-text">
|
||||||
启用{" "}
|
Use{" "}
|
||||||
<a target="_blank" href="https://github.com/boojack/tiny-undo" onClick={(e) => e.stopPropagation()} rel="noreferrer">
|
<a target="_blank" href="https://github.com/boojack/tiny-undo" onClick={(e) => e.stopPropagation()} rel="noreferrer">
|
||||||
tiny-undo
|
tiny-undo
|
||||||
</a>
|
</a>
|
||||||
@ -96,13 +96,13 @@ const PreferencesSection: React.FC<Props> = () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-container">
|
<div className="section-container">
|
||||||
<p className="title-text">其他</p>
|
<p className="title-text">Others</p>
|
||||||
<div className="w-full flex flex-row justify-start items-center">
|
<div className="w-full flex flex-row justify-start items-center">
|
||||||
<button className="px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleExportBtnClick}>
|
<button className="px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleExportBtnClick}>
|
||||||
导出数据(JSON)
|
Export data as JSON
|
||||||
</button>
|
</button>
|
||||||
<button className="btn format-btn hidden" onClick={handleFormatMemosBtnClick}>
|
<button className="btn format-btn hidden" onClick={handleFormatMemosBtnClick}>
|
||||||
格式化数据
|
Format Data
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,8 +43,8 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrl }: Props) => {
|
|||||||
|
|
||||||
<div className="img-container">
|
<div className="img-container">
|
||||||
<img className={imgWidth <= 0 ? "hidden" : ""} ref={imgRef} width={imgWidth + "%"} src={imgUrl} />
|
<img className={imgWidth <= 0 ? "hidden" : ""} ref={imgRef} width={imgWidth + "%"} src={imgUrl} />
|
||||||
<span className={"loading-text " + (imgWidth === -1 ? "" : "hidden")}>图片加载中...</span>
|
<span className={"loading-text " + (imgWidth === -1 ? "" : "hidden")}>Loading image...</span>
|
||||||
<span className={"loading-text " + (imgWidth === 0 ? "" : "hidden")}>😟 图片加载失败,可能是无效的链接</span>
|
<span className={"loading-text " + (imgWidth === 0 ? "" : "hidden")}>😟 Failed to load image</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="action-btns-container">
|
<div className="action-btns-container">
|
||||||
|
@ -36,7 +36,7 @@ const SearchBar: React.FC<Props> = () => {
|
|||||||
<div className="quickly-action-container">
|
<div className="quickly-action-container">
|
||||||
<p className="title-text">QUICKLY FILTER</p>
|
<p className="title-text">QUICKLY FILTER</p>
|
||||||
<div className="section-container types-container">
|
<div className="section-container types-container">
|
||||||
<span className="section-text">类型:</span>
|
<span className="section-text">Type:</span>
|
||||||
<div className="values-container">
|
<div className="values-container">
|
||||||
{memoSpecialTypes.map((t, idx) => {
|
{memoSpecialTypes.map((t, idx) => {
|
||||||
return (
|
return (
|
||||||
|
@ -65,7 +65,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">
|
<p className="title-text">
|
||||||
<span className="icon-text">🥰</span>分享 Memo 图片
|
<span className="icon-text">🥰</span>Share Memo
|
||||||
</p>
|
</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<img className="icon-img" src="/icons/close.svg" />
|
<img className="icon-img" src="/icons/close.svg" />
|
||||||
@ -73,7 +73,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<div className={`tip-words-container ${shortcutImgUrl ? "finish" : "loading"}`}>
|
<div className={`tip-words-container ${shortcutImgUrl ? "finish" : "loading"}`}>
|
||||||
<p className="tip-text">{shortcutImgUrl ? "右键或长按即可保存图片 👇" : "图片生成中..."}</p>
|
<p className="tip-text">{shortcutImgUrl ? "Right click or long press to save image 👇" : "Generating the screenshot..."}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="memo-container" ref={memoElRef}>
|
<div className="memo-container" ref={memoElRef}>
|
||||||
<Only when={shortcutImgUrl !== ""}>
|
<Only when={shortcutImgUrl !== ""}>
|
||||||
@ -97,7 +97,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</Only>
|
</Only>
|
||||||
<div className="watermark-container">
|
<div className="watermark-container">
|
||||||
<span className="normal-text">
|
<span className="normal-text">
|
||||||
<span className="icon-text">✍️</span> by <span className="name-text">{userinfo?.username}</span>
|
<span className="icon-text">✍️</span> by <span className="name-text">{userinfo?.name}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,27 +5,27 @@ import useLoading from "../hooks/useLoading";
|
|||||||
import Only from "./common/OnlyWhen";
|
import Only from "./common/OnlyWhen";
|
||||||
import utils from "../helpers/utils";
|
import utils from "../helpers/utils";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
import { locationService, queryService } from "../services";
|
import { locationService, shortcutService } from "../services";
|
||||||
import showCreateQueryDialog from "./CreateQueryDialog";
|
import showCreateQueryDialog from "./CreateShortcutDialog";
|
||||||
import "../less/query-list.less";
|
import "../less/shortcut-list.less";
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
const QueryList: React.FC<Props> = () => {
|
const ShortcutList: React.FC<Props> = () => {
|
||||||
const {
|
const {
|
||||||
queryState: { queries },
|
shortcutState: { shortcuts },
|
||||||
locationState: {
|
locationState: {
|
||||||
query: { filter },
|
query: { shortcutId },
|
||||||
},
|
},
|
||||||
} = useContext(appContext);
|
} = useContext(appContext);
|
||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
const sortedQueries = queries
|
const sortedShortcuts = shortcuts
|
||||||
.sort((a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt))
|
.sort((a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt))
|
||||||
.sort((a, b) => utils.getTimeStampByDate(b.pinnedAt ?? 0) - utils.getTimeStampByDate(a.pinnedAt ?? 0));
|
.sort((a, b) => utils.getTimeStampByDate(b.updatedAt) - utils.getTimeStampByDate(a.updatedAt));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
queryService
|
shortcutService
|
||||||
.getMyAllQueries()
|
.getMyAllShortcuts()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// do nth
|
// do nth
|
||||||
})
|
})
|
||||||
@ -35,47 +35,47 @@ const QueryList: React.FC<Props> = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="queries-wrapper">
|
<div className="shortcuts-wrapper">
|
||||||
<p className="title-text">
|
<p className="title-text">
|
||||||
<span className="normal-text">快速检索</span>
|
<span className="normal-text">Shortcuts</span>
|
||||||
<span className="btn" onClick={() => showCreateQueryDialog()}>
|
<span className="btn" onClick={() => showCreateQueryDialog()}>
|
||||||
+
|
+
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<Only when={loadingState.isSucceed && sortedQueries.length === 0}>
|
<Only when={loadingState.isSucceed && sortedShortcuts.length === 0}>
|
||||||
<div className="create-query-btn-container">
|
<div className="create-shortcut-btn-container">
|
||||||
<span className="btn" onClick={() => showCreateQueryDialog()}>
|
<span className="btn" onClick={() => showCreateQueryDialog()}>
|
||||||
创建检索
|
New shortcut
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Only>
|
</Only>
|
||||||
<div className="queries-container">
|
<div className="shortcuts-container">
|
||||||
{sortedQueries.map((q) => {
|
{sortedShortcuts.map((s) => {
|
||||||
return <QueryItemContainer key={q.id} query={q} isActive={q.id === filter} />;
|
return <ShortcutContainer key={s.id} shortcut={s} isActive={s.id === shortcutId} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface QueryItemContainerProps {
|
interface ShortcutContainerProps {
|
||||||
query: Model.Query;
|
shortcut: Model.Shortcut;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueryItemContainer: React.FC<QueryItemContainerProps> = (props: QueryItemContainerProps) => {
|
const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutContainerProps) => {
|
||||||
const { query, isActive } = props;
|
const { shortcut, isActive } = props;
|
||||||
const [showActionBtns, toggleShowActionBtns] = useToggle(false);
|
const [showActionBtns, toggleShowActionBtns] = useToggle(false);
|
||||||
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
|
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
|
||||||
|
|
||||||
const handleQueryClick = () => {
|
const handleQueryClick = () => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
locationService.setMemoFilter("");
|
locationService.setMemoShortcut("");
|
||||||
} else {
|
} else {
|
||||||
if (!["/", "/recycle"].includes(locationService.getState().pathname)) {
|
if (!["/", "/recycle"].includes(locationService.getState().pathname)) {
|
||||||
locationService.setPathname("/");
|
locationService.setPathname("/");
|
||||||
}
|
}
|
||||||
locationService.setMemoFilter(query.id);
|
locationService.setMemoShortcut(shortcut.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ const QueryItemContainer: React.FC<QueryItemContainerProps> = (props: QueryItemC
|
|||||||
|
|
||||||
if (showConfirmDeleteBtn) {
|
if (showConfirmDeleteBtn) {
|
||||||
try {
|
try {
|
||||||
await queryService.deleteQuery(query.id);
|
await shortcutService.deleteShortcut(shortcut.id);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toastHelper.error(error.message);
|
toastHelper.error(error.message);
|
||||||
}
|
}
|
||||||
@ -104,24 +104,24 @@ const QueryItemContainer: React.FC<QueryItemContainerProps> = (props: QueryItemC
|
|||||||
|
|
||||||
const handleEditQueryBtnClick = (event: React.MouseEvent) => {
|
const handleEditQueryBtnClick = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
showCreateQueryDialog(query.id);
|
showCreateQueryDialog(shortcut.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePinQueryBtnClick = async (event: React.MouseEvent) => {
|
const handlePinQueryBtnClick = async (event: React.MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (query.pinnedAt) {
|
if (shortcut.rowStatus === "ARCHIVED") {
|
||||||
await queryService.unpinQuery(query.id);
|
await shortcutService.unpinShortcut(shortcut.id);
|
||||||
queryService.editQuery({
|
shortcutService.editShortcut({
|
||||||
...query,
|
...shortcut,
|
||||||
pinnedAt: "",
|
rowStatus: "NORMAL",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await queryService.pinQuery(query.id);
|
await shortcutService.pinShortcut(shortcut.id);
|
||||||
queryService.editQuery({
|
shortcutService.editShortcut({
|
||||||
...query,
|
...shortcut,
|
||||||
pinnedAt: utils.getDateTimeString(Date.now()),
|
rowStatus: "NORMAL",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -135,10 +135,10 @@ const QueryItemContainer: React.FC<QueryItemContainerProps> = (props: QueryItemC
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`query-item-container ${isActive ? "active" : ""}`} onClick={handleQueryClick}>
|
<div className={`shortcut-container ${isActive ? "active" : ""}`} onClick={handleQueryClick}>
|
||||||
<div className="query-text-container">
|
<div className="shortcut-text-container">
|
||||||
<span className="icon-text">#</span>
|
<span className="icon-text">#</span>
|
||||||
<span className="query-text">{query.title}</span>
|
<span className="shortcut-text">{shortcut.title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<span className="action-btn toggle-btn" onClick={handleShowActionBtnClick}>
|
<span className="action-btn toggle-btn" onClick={handleShowActionBtnClick}>
|
||||||
@ -147,17 +147,17 @@ const QueryItemContainer: React.FC<QueryItemContainerProps> = (props: QueryItemC
|
|||||||
<div className={`action-btns-wrapper ${showActionBtns ? "" : "hidden"}`} onMouseLeave={handleActionBtnContainerMouseLeave}>
|
<div className={`action-btns-wrapper ${showActionBtns ? "" : "hidden"}`} onMouseLeave={handleActionBtnContainerMouseLeave}>
|
||||||
<div className="action-btns-container">
|
<div className="action-btns-container">
|
||||||
<span className="btn" onClick={handlePinQueryBtnClick}>
|
<span className="btn" onClick={handlePinQueryBtnClick}>
|
||||||
{query.pinnedAt ? "取消置顶" : "置顶"}
|
{shortcut.rowStatus === "ARCHIVED" ? "Unpin" : "Pin"}
|
||||||
</span>
|
</span>
|
||||||
<span className="btn" onClick={handleEditQueryBtnClick}>
|
<span className="btn" onClick={handleEditQueryBtnClick}>
|
||||||
编辑
|
Edit
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`}
|
className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`}
|
||||||
onClick={handleDeleteMemoClick}
|
onClick={handleDeleteMemoClick}
|
||||||
onMouseLeave={handleDeleteBtnMouseLeave}
|
onMouseLeave={handleDeleteBtnMouseLeave}
|
||||||
>
|
>
|
||||||
{showConfirmDeleteBtn ? "确定删除!" : "删除"}
|
{showConfirmDeleteBtn ? "Delete!" : "Delete"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -167,4 +167,4 @@ const QueryItemContainer: React.FC<QueryItemContainerProps> = (props: QueryItemC
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QueryList;
|
export default ShortcutList;
|
@ -3,7 +3,7 @@ import appContext from "../stores/appContext";
|
|||||||
import { SHOW_SIDERBAR_MOBILE_CLASSNAME } from "../helpers/consts";
|
import { SHOW_SIDERBAR_MOBILE_CLASSNAME } from "../helpers/consts";
|
||||||
import { globalStateService } from "../services";
|
import { globalStateService } from "../services";
|
||||||
import UserBanner from "./UserBanner";
|
import UserBanner from "./UserBanner";
|
||||||
import QueryList from "./QueryList";
|
import ShortcutList from "./ShortcutList";
|
||||||
import TagList from "./TagList";
|
import TagList from "./TagList";
|
||||||
import UsageHeatMap from "./UsageHeatMap";
|
import UsageHeatMap from "./UsageHeatMap";
|
||||||
import "../less/siderbar.less";
|
import "../less/siderbar.less";
|
||||||
@ -66,7 +66,7 @@ const Sidebar: React.FC<Props> = () => {
|
|||||||
<aside className="sidebar-wrapper" ref={wrapperElRef}>
|
<aside className="sidebar-wrapper" ref={wrapperElRef}>
|
||||||
<UserBanner />
|
<UserBanner />
|
||||||
<UsageHeatMap />
|
<UsageHeatMap />
|
||||||
<QueryList />
|
<ShortcutList />
|
||||||
<TagList />
|
<TagList />
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
@ -70,14 +70,14 @@ const TagList: React.FC<Props> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tags-wrapper">
|
<div className="tags-wrapper">
|
||||||
<p className="title-text">常用标签</p>
|
<p className="title-text">Tags</p>
|
||||||
<div className="tags-container">
|
<div className="tags-container">
|
||||||
{tags.map((t, idx) => (
|
{tags.map((t, idx) => (
|
||||||
<TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={tagQuery} />
|
<TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={tagQuery} />
|
||||||
))}
|
))}
|
||||||
<Only when={tags.length < 5 && memoService.initialized}>
|
<Only when={tags.length < 5 && memoService.initialized}>
|
||||||
<p className="tag-tip-container">
|
<p className="tag-tip-container">
|
||||||
输入<span className="code-text"># Tag </span>来创建标签吧~
|
Enter <span className="code-text"># Tag </span> to create a tag
|
||||||
</p>
|
</p>
|
||||||
</Only>
|
</Only>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,7 +13,7 @@ const UserBanner: React.FC<Props> = () => {
|
|||||||
memoState: { memos, tags },
|
memoState: { memos, tags },
|
||||||
userState: { user },
|
userState: { user },
|
||||||
} = useContext(appContext);
|
} = useContext(appContext);
|
||||||
const username = user ? user.username : "Memos";
|
const username = user ? user.name : "Memos";
|
||||||
const createdDays = user ? Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdAt)) / 1000 / 3600 / 24) : 0;
|
const createdDays = user ? Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdAt)) / 1000 / 3600 / 24) : 0;
|
||||||
|
|
||||||
const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false);
|
const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false);
|
||||||
|
@ -58,7 +58,7 @@ const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
|
|||||||
<img className="icon-img" src="/icons/arrow-left.svg" />
|
<img className="icon-img" src="/icons/arrow-left.svg" />
|
||||||
</span>
|
</span>
|
||||||
<span className="normal-text">
|
<span className="normal-text">
|
||||||
{firstDate.getFullYear()} 年 {firstDate.getMonth() + 1} 月
|
{firstDate.getFullYear()}/{firstDate.getMonth() + 1}
|
||||||
</span>
|
</span>
|
||||||
<span className="btn-text" onClick={() => handleChangeMonthBtnClick(1)}>
|
<span className="btn-text" onClick={() => handleChangeMonthBtnClick(1)}>
|
||||||
<img className="icon-img" src="/icons/arrow-right.svg" />
|
<img className="icon-img" src="/icons/arrow-right.svg" />
|
||||||
@ -66,13 +66,13 @@ const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="date-picker-day-container">
|
<div className="date-picker-day-container">
|
||||||
<div className="date-picker-day-header">
|
<div className="date-picker-day-header">
|
||||||
<span className="day-item">周一</span>
|
<span className="day-item">Mon</span>
|
||||||
<span className="day-item">周二</span>
|
<span className="day-item">Tue</span>
|
||||||
<span className="day-item">周三</span>
|
<span className="day-item">Web</span>
|
||||||
<span className="day-item">周四</span>
|
<span className="day-item">Thu</span>
|
||||||
<span className="day-item">周五</span>
|
<span className="day-item">Fri</span>
|
||||||
<span className="day-item">周六</span>
|
<span className="day-item">Sat</span>
|
||||||
<span className="day-item">周日</span>
|
<span className="day-item">Sun</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{dayList.map((d) => {
|
{dayList.map((d) => {
|
||||||
|
@ -15,7 +15,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nullItem = {
|
const nullItem = {
|
||||||
text: "请选择",
|
text: "Select",
|
||||||
value: "",
|
value: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import utils from "./utils";
|
type ResponseObject<T> = {
|
||||||
|
|
||||||
type ResponseType<T = unknown> = {
|
|
||||||
succeed: boolean;
|
|
||||||
message: string;
|
|
||||||
data: T;
|
data: T;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RequestConfig = {
|
type RequestConfig = {
|
||||||
@ -13,7 +11,7 @@ type RequestConfig = {
|
|||||||
dataType?: "json" | "file";
|
dataType?: "json" | "file";
|
||||||
};
|
};
|
||||||
|
|
||||||
async function request<T>(config: RequestConfig): Promise<ResponseType<T>> {
|
async function request<T>(config: RequestConfig): Promise<T> {
|
||||||
const { method, url, data, dataType } = config;
|
const { method, url, data, dataType } = config;
|
||||||
const requestConfig: RequestInit = {
|
const requestConfig: RequestInit = {
|
||||||
method,
|
method,
|
||||||
@ -31,13 +29,13 @@ async function request<T>(config: RequestConfig): Promise<ResponseType<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, requestConfig);
|
const response = await fetch(url, requestConfig);
|
||||||
const responseData = (await response.json()) as ResponseType<T>;
|
const responseData = (await response.json()) as ResponseObject<T>;
|
||||||
|
|
||||||
if (!responseData.succeed) {
|
if (responseData.error || responseData.message) {
|
||||||
throw responseData;
|
throw new Error(responseData.error || responseData.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseData;
|
return responseData.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace api {
|
namespace api {
|
||||||
@ -48,47 +46,57 @@ namespace api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signin(username: string, password: string) {
|
export function login(name: string, password: string) {
|
||||||
return request({
|
return request<Model.User>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/api/auth/signin",
|
url: "/api/auth/login",
|
||||||
data: { username, password },
|
data: {
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signup(username: string, password: string) {
|
export function signup(name: string, password: string) {
|
||||||
return request({
|
return request<Model.User>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/api/auth/signup",
|
url: "/api/auth/signup",
|
||||||
data: { username, password },
|
data: {
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signout() {
|
export function signout() {
|
||||||
return request({
|
return request({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/api/auth/signout",
|
url: "/api/auth/logout",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkUsernameUsable(username: string) {
|
export function checkUsernameUsable(name: string) {
|
||||||
return request<boolean>({
|
return request<boolean>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/api/user/checkusername",
|
url: "/api/user/rename_check",
|
||||||
data: { username },
|
data: {
|
||||||
|
name,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkPasswordValid(password: string) {
|
export function checkPasswordValid(password: string) {
|
||||||
return request<boolean>({
|
return request<boolean>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/api/user/validpassword",
|
url: "/api/user/password_check",
|
||||||
data: { password },
|
data: {
|
||||||
|
password,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateUserinfo(userinfo: Partial<{ username: string; password: string }>) {
|
export function updateUserinfo(userinfo: Partial<{ name: string; password: string; resetOpenId: boolean }>) {
|
||||||
return request({
|
return request<Model.User>({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/api/user/me",
|
url: "/api/user/me",
|
||||||
data: userinfo,
|
data: userinfo,
|
||||||
@ -105,22 +113,24 @@ namespace api {
|
|||||||
export function getMyMemos() {
|
export function getMyMemos() {
|
||||||
return request<Model.Memo[]>({
|
return request<Model.Memo[]>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/api/memo/all",
|
url: "/api/memo",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMyDeletedMemos() {
|
export function getMyDeletedMemos() {
|
||||||
return request<Model.Memo[]>({
|
return request<Model.Memo[]>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/api/memo/all?deleted=true",
|
url: "/api/memo?hidden=true",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMemo(content: string) {
|
export function createMemo(content: string) {
|
||||||
return request<Model.Memo>({
|
return request<Model.Memo>({
|
||||||
method: "PUT",
|
method: "POST",
|
||||||
url: "/api/memo/",
|
url: "/api/memo",
|
||||||
data: { content },
|
data: {
|
||||||
|
content,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +138,9 @@ namespace api {
|
|||||||
return request<Model.Memo>({
|
return request<Model.Memo>({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/memo/${memoId}`,
|
url: `/api/memo/${memoId}`,
|
||||||
data: { content },
|
data: {
|
||||||
|
content,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +149,7 @@ namespace api {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/memo/${memoId}`,
|
url: `/api/memo/${memoId}`,
|
||||||
data: {
|
data: {
|
||||||
deletedAt: utils.getDateTimeString(Date.now()),
|
rowStatus: "HIDDEN",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -147,7 +159,7 @@ namespace api {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/memo/${memoId}`,
|
url: `/api/memo/${memoId}`,
|
||||||
data: {
|
data: {
|
||||||
deletedAt: "",
|
rowStatus: "NORMAL",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -159,56 +171,66 @@ namespace api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMyQueries() {
|
export function getMyShortcuts() {
|
||||||
return request<Model.Query[]>({
|
return request<Model.Shortcut[]>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/api/query/all",
|
url: "/api/shortcut",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createQuery(title: string, querystring: string) {
|
export function createShortcut(title: string, payload: string) {
|
||||||
return request<Model.Query>({
|
return request<Model.Shortcut>({
|
||||||
method: "PUT",
|
method: "POST",
|
||||||
url: "/api/query/",
|
url: "/api/shortcut",
|
||||||
data: { title, querystring },
|
data: {
|
||||||
|
title,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateQuery(queryId: string, title: string, querystring: string) {
|
export function updateShortcut(shortcutId: string, title: string, payload: string) {
|
||||||
return request<Model.Query>({
|
return request<Model.Shortcut>({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/query/${queryId}`,
|
url: `/api/shortcut/${shortcutId}`,
|
||||||
data: { title, querystring },
|
data: {
|
||||||
|
title,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteQueryById(queryId: string) {
|
export function deleteShortcutById(shortcutId: string) {
|
||||||
return request({
|
return request({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: `/api/query/${queryId}`,
|
url: `/api/shortcut/${shortcutId}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pinQuery(queryId: string) {
|
export function pinShortcut(shortcutId: string) {
|
||||||
return request({
|
return request({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/query/${queryId}`,
|
url: `/api/shortcut/${shortcutId}`,
|
||||||
data: { pinnedAt: utils.getDateTimeString(Date.now()) },
|
data: {
|
||||||
|
rowStatus: "ARCHIVED",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unpinQuery(queryId: string) {
|
export function unpinShortcut(shortcutId: string) {
|
||||||
return request({
|
return request({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/query/${queryId}`,
|
url: `/api/shortcut/${shortcutId}`,
|
||||||
data: { pinnedAt: "" },
|
data: {
|
||||||
|
rowStatus: "NORMAL",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadFile(formData: FormData) {
|
export function uploadFile(formData: FormData) {
|
||||||
return request<Model.Resource>({
|
return request<Model.Resource>({
|
||||||
method: "PUT",
|
method: "POST",
|
||||||
url: "/api/resource/",
|
url: "/api/resource",
|
||||||
data: formData,
|
data: formData,
|
||||||
dataType: "file",
|
dataType: "file",
|
||||||
});
|
});
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
// 移动端样式适配额外类名
|
// mobile style addtion classname
|
||||||
export const SHOW_SIDERBAR_MOBILE_CLASSNAME = "mobile-show-sidebar";
|
export const SHOW_SIDERBAR_MOBILE_CLASSNAME = "mobile-show-sidebar";
|
||||||
|
|
||||||
// 默认动画持续时长
|
// default animation duration
|
||||||
export const ANIMATION_DURATION = 200;
|
export const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
// toast 动画持续时长
|
// toast animation duration
|
||||||
export const TOAST_ANIMATION_DURATION = 400;
|
export const TOAST_ANIMATION_DURATION = 400;
|
||||||
|
|
||||||
// 一天的毫秒数
|
// millisecond in a day
|
||||||
export const DAILY_TIMESTAMP = 3600 * 24 * 1000;
|
export const DAILY_TIMESTAMP = 3600 * 24 * 1000;
|
||||||
|
|
||||||
// 标签 正则
|
// tag regex
|
||||||
export const TAG_REG = /#\s?(.+?)\s/g;
|
export const TAG_REG = /#\s?(.+?)\s/g;
|
||||||
|
|
||||||
// URL 正则
|
// URL regex
|
||||||
export const LINK_REG = /(https?:\/\/[^\s<\\*>']+)/g;
|
export const LINK_REG = /(https?:\/\/[^\s<\\*>']+)/g;
|
||||||
|
|
||||||
// 图片 正则
|
// image regex
|
||||||
export const IMAGE_URL_REG = /([^\s<\\*>']+\.(jpeg|jpg|gif|png|svg))/g;
|
export const IMAGE_URL_REG = /([^\s<\\*>']+\.(jpeg|jpg|gif|png|svg))/g;
|
||||||
|
|
||||||
// memo 关联正则
|
// linked memo regex
|
||||||
export const MEMO_LINK_REG = /\[@(.+?)\]\((.+?)\)/g;
|
export const MEMO_LINK_REG = /\[@(.+?)\]\((.+?)\)/g;
|
||||||
|
@ -1,68 +1,68 @@
|
|||||||
import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "./consts";
|
import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "./consts";
|
||||||
|
|
||||||
export const relationConsts = [
|
export const relationConsts = [
|
||||||
{ text: "且", value: "AND" },
|
{ text: "And", value: "AND" },
|
||||||
{ text: "或", value: "OR" },
|
{ text: "Or", value: "OR" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const filterConsts = {
|
export const filterConsts = {
|
||||||
TAG: {
|
TAG: {
|
||||||
|
text: "Tag",
|
||||||
value: "TAG",
|
value: "TAG",
|
||||||
text: "标签",
|
|
||||||
operators: [
|
operators: [
|
||||||
{
|
{
|
||||||
text: "包括",
|
text: "Contains",
|
||||||
value: "CONTAIN",
|
value: "CONTAIN",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "排除",
|
text: "Does not contain",
|
||||||
value: "NOT_CONTAIN",
|
value: "NOT_CONTAIN",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
TYPE: {
|
TYPE: {
|
||||||
|
text: "Type",
|
||||||
value: "TYPE",
|
value: "TYPE",
|
||||||
text: "类型",
|
|
||||||
operators: [
|
operators: [
|
||||||
{
|
{
|
||||||
|
text: "Is",
|
||||||
value: "IS",
|
value: "IS",
|
||||||
text: "是",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
text: "Is not",
|
||||||
value: "IS_NOT",
|
value: "IS_NOT",
|
||||||
text: "不是",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
text: "Connected",
|
||||||
value: "CONNECTED",
|
value: "CONNECTED",
|
||||||
text: "有关联",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
text: "No tags",
|
||||||
value: "NOT_TAGGED",
|
value: "NOT_TAGGED",
|
||||||
text: "无标签",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
text: "Has links",
|
||||||
value: "LINKED",
|
value: "LINKED",
|
||||||
text: "有超链接",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
text: "Has images",
|
||||||
value: "IMAGED",
|
value: "IMAGED",
|
||||||
text: "有图片",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
TEXT: {
|
TEXT: {
|
||||||
|
text: "Text",
|
||||||
value: "TEXT",
|
value: "TEXT",
|
||||||
text: "文本",
|
|
||||||
operators: [
|
operators: [
|
||||||
{
|
{
|
||||||
|
text: "Contain",
|
||||||
value: "CONTAIN",
|
value: "CONTAIN",
|
||||||
text: "包括",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
text: "Does not contain",
|
||||||
value: "NOT_CONTAIN",
|
value: "NOT_CONTAIN",
|
||||||
text: "排除",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -4,13 +4,10 @@ import { InputAction } from "tiny-undo";
|
|||||||
* Define storage data type
|
* Define storage data type
|
||||||
*/
|
*/
|
||||||
interface StorageData {
|
interface StorageData {
|
||||||
// 编辑器输入缓存内容
|
// Editor content cache
|
||||||
editorContentCache: string;
|
editorContentCache: string;
|
||||||
// 分词开关
|
|
||||||
shouldSplitMemoWord: boolean;
|
shouldSplitMemoWord: boolean;
|
||||||
// 是否隐藏图片链接地址
|
|
||||||
shouldHideImageUrl: boolean;
|
shouldHideImageUrl: boolean;
|
||||||
// markdown 解析开关
|
|
||||||
shouldUseMarkdownParser: boolean;
|
shouldUseMarkdownParser: boolean;
|
||||||
|
|
||||||
// Editor setting
|
// Editor setting
|
||||||
|
@ -43,6 +43,10 @@ namespace utils {
|
|||||||
return `${year}/${month}/${date}`;
|
return `${year}/${month}/${date}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDataStringWithTs(ts: number): string {
|
||||||
|
return getDateTimeString(ts * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
export function getTimeString(t: Date | number | string): string {
|
export function getTimeString(t: Date | number | string): string {
|
||||||
const d = new Date(getTimeStampByDate(t));
|
const d = new Date(getTimeStampByDate(t));
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
// 验证器
|
// Validator
|
||||||
// * 主要用于验证表单
|
// * use for validating form data
|
||||||
const chineseReg = /[\u3000\u3400-\u4DBF\u4E00-\u9FFF]/;
|
const chineseReg = /[\u3000\u3400-\u4DBF\u4E00-\u9FFF]/;
|
||||||
|
|
||||||
export interface ValidatorConfig {
|
export interface ValidatorConfig {
|
||||||
// 最小长度
|
// min length
|
||||||
minLength: number;
|
minLength: number;
|
||||||
// 最大长度
|
// max length
|
||||||
maxLength: number;
|
maxLength: number;
|
||||||
// 无空格
|
// no space
|
||||||
noSpace: boolean;
|
noSpace: boolean;
|
||||||
// 无中文
|
// no chinese
|
||||||
noChinese: boolean;
|
noChinese: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ export function validate(text: string, config: Partial<ValidatorConfig>): { resu
|
|||||||
if (text.length < config.minLength) {
|
if (text.length < config.minLength) {
|
||||||
return {
|
return {
|
||||||
result: false,
|
result: false,
|
||||||
reason: "长度过短",
|
reason: "Too short",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ export function validate(text: string, config: Partial<ValidatorConfig>): { resu
|
|||||||
if (text.length > config.maxLength) {
|
if (text.length > config.maxLength) {
|
||||||
return {
|
return {
|
||||||
result: false,
|
result: false,
|
||||||
reason: "长度超出",
|
reason: "Too long",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,14 +35,14 @@ export function validate(text: string, config: Partial<ValidatorConfig>): { resu
|
|||||||
if (config.noSpace && text.includes(" ")) {
|
if (config.noSpace && text.includes(" ")) {
|
||||||
return {
|
return {
|
||||||
result: false,
|
result: false,
|
||||||
reason: "不应含有空格",
|
reason: "Don't allow space",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.noChinese && chineseReg.test(text)) {
|
if (config.noChinese && chineseReg.test(text)) {
|
||||||
return {
|
return {
|
||||||
result: false,
|
result: false,
|
||||||
reason: "不应含有中文字符",
|
reason: "Don't allow chinese",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +15,8 @@ export interface Store<S extends State, A extends Action> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 简单实现的 Redux
|
* Toy-Redux
|
||||||
* @param preloadedState 初始 state
|
* @param preloadedState initial state
|
||||||
* @param reducer reducer pure function
|
* @param reducer reducer pure function
|
||||||
* @returns store
|
* @returns store
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@import "./mixin.less";
|
@import "./mixin.less";
|
||||||
|
|
||||||
.create-query-dialog {
|
.create-shortcut-dialog {
|
||||||
> .dialog-container {
|
> .dialog-container {
|
||||||
width: 420px;
|
width: 420px;
|
||||||
|
|
||||||
@ -155,7 +155,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 875px) {
|
@media only screen and (max-width: 875px) {
|
||||||
.dialog-wrapper.create-query-dialog {
|
.dialog-wrapper.create-shortcut-dialog {
|
||||||
padding: 24px 16px;
|
padding: 24px 16px;
|
||||||
padding-top: 64px;
|
padding-top: 64px;
|
||||||
justify-content: unset;
|
justify-content: unset;
|
@ -1,6 +1,6 @@
|
|||||||
@import "./mixin.less";
|
@import "./mixin.less";
|
||||||
|
|
||||||
.queries-wrapper {
|
.shortcuts-wrapper {
|
||||||
.flex(column, flex-start, flex-start);
|
.flex(column, flex-start, flex-start);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .create-query-btn-container {
|
> .create-shortcut-btn-container {
|
||||||
.flex(row, center, center);
|
.flex(row, center, center);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@ -55,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .queries-container {
|
> .shortcuts-container {
|
||||||
.flex(column, flex-start, flex-start);
|
.flex(column, flex-start, flex-start);
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -63,7 +63,7 @@
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
> .query-item-container {
|
> .shortcut-container {
|
||||||
.flex(row, space-between, center);
|
.flex(row, space-between, center);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@ -86,7 +86,7 @@
|
|||||||
&.active {
|
&.active {
|
||||||
background-color: @text-green !important;
|
background-color: @text-green !important;
|
||||||
|
|
||||||
> .query-text-container {
|
> .shortcut-text-container {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
@ -95,7 +95,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .query-text-container {
|
> .shortcut-text-container {
|
||||||
.flex(row, flex-start, center);
|
.flex(row, flex-start, center);
|
||||||
max-width: calc(100% - 24px);
|
max-width: calc(100% - 24px);
|
||||||
color: @text-black;
|
color: @text-black;
|
||||||
@ -110,7 +110,7 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .query-text {
|
> .shortcut-text {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,7 +181,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 875px) {
|
@media only screen and (max-width: 875px) {
|
||||||
.queries-container {
|
.shortcuts-container {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@ -192,7 +192,7 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .query-item-container {
|
> .shortcut-container {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import appContext from "../stores/appContext";
|
import appContext from "../stores/appContext";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import { globalStateService, locationService, memoService, queryService } from "../services";
|
import { globalStateService, locationService, memoService, shortcutService } from "../services";
|
||||||
import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts";
|
import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts";
|
||||||
import utils from "../helpers/utils";
|
import utils from "../helpers/utils";
|
||||||
import { checkShouldShowMemoWithFilters } from "../helpers/filter";
|
import { checkShouldShowMemoWithFilters } from "../helpers/filter";
|
||||||
@ -21,8 +21,8 @@ const MemoTrash: React.FC<Props> = () => {
|
|||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
const [deletedMemos, setDeletedMemos] = useState<Model.Memo[]>([]);
|
const [deletedMemos, setDeletedMemos] = useState<Model.Memo[]>([]);
|
||||||
|
|
||||||
const { tag: tagQuery, duration, type: memoType, text: textQuery, filter: queryId } = query;
|
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query;
|
||||||
const queryFilter = queryService.getQueryById(queryId);
|
const queryFilter = shortcutService.getShortcutById(shortcutId);
|
||||||
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || queryFilter);
|
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || queryFilter);
|
||||||
|
|
||||||
const shownMemos =
|
const shownMemos =
|
||||||
@ -31,7 +31,7 @@ const MemoTrash: React.FC<Props> = () => {
|
|||||||
let shouldShow = true;
|
let shouldShow = true;
|
||||||
|
|
||||||
if (queryFilter) {
|
if (queryFilter) {
|
||||||
const filters = JSON.parse(queryFilter.querystring) as Filter[];
|
const filters = JSON.parse(queryFilter.payload) as Filter[];
|
||||||
if (Array.isArray(filters)) {
|
if (Array.isArray(filters)) {
|
||||||
shouldShow = checkShouldShowMemoWithFilters(memo, filters);
|
shouldShow = checkShouldShowMemoWithFilters(memo, filters);
|
||||||
}
|
}
|
||||||
@ -114,13 +114,13 @@ const MemoTrash: React.FC<Props> = () => {
|
|||||||
<img className="icon-img" src="/icons/menu.svg" alt="menu" />
|
<img className="icon-img" src="/icons/menu.svg" alt="menu" />
|
||||||
</button>
|
</button>
|
||||||
</Only>
|
</Only>
|
||||||
<span className="normal-text">回收站</span>
|
<span className="normal-text">Recycle Bin</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MemoFilter />
|
<MemoFilter />
|
||||||
{loadingState.isLoading ? (
|
{loadingState.isLoading ? (
|
||||||
<div className="tip-text-container">
|
<div className="tip-text-container">
|
||||||
<p className="tip-text">努力请求数据中...</p>
|
<p className="tip-text">fetching data...</p>
|
||||||
</div>
|
</div>
|
||||||
) : deletedMemos.length === 0 ? (
|
) : deletedMemos.length === 0 ? (
|
||||||
<div className="tip-text-container">
|
<div className="tip-text-container">
|
||||||
|
@ -30,7 +30,7 @@ const Setting: React.FC<Props> = () => {
|
|||||||
<img className="icon-img" src="/icons/menu.svg" alt="menu" />
|
<img className="icon-img" src="/icons/menu.svg" alt="menu" />
|
||||||
</button>
|
</button>
|
||||||
</Only>
|
</Only>
|
||||||
<span className="normal-text">账号与设置</span>
|
<span className="normal-text">Settings</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -55,34 +55,29 @@ const Signin: React.FC<Props> = () => {
|
|||||||
|
|
||||||
const usernameValidResult = validate(username, validateConfig);
|
const usernameValidResult = validate(username, validateConfig);
|
||||||
if (!usernameValidResult.result) {
|
if (!usernameValidResult.result) {
|
||||||
toastHelper.error("用户名 " + usernameValidResult.reason);
|
toastHelper.error("Username: " + usernameValidResult.reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordValidResult = validate(password, validateConfig);
|
const passwordValidResult = validate(password, validateConfig);
|
||||||
if (!passwordValidResult.result) {
|
if (!passwordValidResult.result) {
|
||||||
toastHelper.error("密码 " + passwordValidResult.reason);
|
toastHelper.error("Password: " + passwordValidResult.reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
signinBtnsClickLoadingState.setLoading();
|
signinBtnsClickLoadingState.setLoading();
|
||||||
let actionFunc = api.signin;
|
let actionFunc = api.login;
|
||||||
if (action === "signup") {
|
if (action === "signup") {
|
||||||
actionFunc = api.signup;
|
actionFunc = api.signup;
|
||||||
}
|
}
|
||||||
const { succeed, message } = await actionFunc(username, password);
|
await actionFunc(username, password);
|
||||||
|
|
||||||
if (!succeed && message) {
|
|
||||||
toastHelper.error("😟 " + message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await userService.doSignIn();
|
const user = await userService.doSignIn();
|
||||||
if (user) {
|
if (user) {
|
||||||
locationService.replaceHistory("/");
|
locationService.replaceHistory("/");
|
||||||
} else {
|
} else {
|
||||||
toastHelper.error("😟 登录失败");
|
toastHelper.error("😟 Login failed");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -106,18 +101,13 @@ const Signin: React.FC<Props> = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
signinBtnsClickLoadingState.setLoading();
|
signinBtnsClickLoadingState.setLoading();
|
||||||
const { succeed, message } = await api.signin("guest", "123456");
|
await api.login("guest", "123456");
|
||||||
|
|
||||||
if (!succeed && message) {
|
|
||||||
toastHelper.error("😟 " + message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await userService.doSignIn();
|
const user = await userService.doSignIn();
|
||||||
if (user) {
|
if (user) {
|
||||||
locationService.replaceHistory("/");
|
locationService.replaceHistory("/");
|
||||||
} else {
|
} else {
|
||||||
toastHelper.error("😟 登录失败");
|
toastHelper.error("😟 Login failed");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -131,7 +121,7 @@ const Signin: React.FC<Props> = () => {
|
|||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<div className="page-header-container">
|
<div className="page-header-container">
|
||||||
<p className="title-text">
|
<p className="title-text">
|
||||||
登录 Memos <span className="icon-text">✍️</span>
|
Login to Memos <span className="icon-text">✍️</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{showAutoSigninAsGuest ? (
|
{showAutoSigninAsGuest ? (
|
||||||
@ -142,13 +132,13 @@ const Signin: React.FC<Props> = () => {
|
|||||||
className={`btn guest-signin ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
className={`btn guest-signin ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||||
onClick={handleAutoSigninAsGuestBtnClick}
|
onClick={handleAutoSigninAsGuestBtnClick}
|
||||||
>
|
>
|
||||||
👉 快速登录进行体验
|
👉 Login as Guest quickly
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||||
onClick={handleSwitchAccountSigninBtnClick}
|
onClick={handleSwitchAccountSigninBtnClick}
|
||||||
>
|
>
|
||||||
已有账号,我要自己登录
|
I have an account
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -156,11 +146,11 @@ const Signin: React.FC<Props> = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="page-content-container">
|
<div className="page-content-container">
|
||||||
<div className="form-item-container input-form-container">
|
<div className="form-item-container input-form-container">
|
||||||
<span className={"normal-text " + (username === "" ? "" : "not-null")}>账号</span>
|
<span className={"normal-text " + (username === "" ? "" : "not-null")}>Usernmae</span>
|
||||||
<input type="text" autoComplete="off" value={username} onChange={handleUsernameInputChanged} />
|
<input type="text" autoComplete="off" value={username} onChange={handleUsernameInputChanged} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-item-container input-form-container">
|
<div className="form-item-container input-form-container">
|
||||||
<span className={"normal-text " + (password === "" ? "" : "not-null")}>密码</span>
|
<span className={"normal-text " + (password === "" ? "" : "not-null")}>Password</span>
|
||||||
<input type="password" autoComplete="off" value={password} onChange={handlePasswordInputChanged} />
|
<input type="password" autoComplete="off" value={password} onChange={handlePasswordInputChanged} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -171,14 +161,14 @@ const Signin: React.FC<Props> = () => {
|
|||||||
className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||||
onClick={handleAutoSigninAsGuestBtnClick}
|
onClick={handleAutoSigninAsGuestBtnClick}
|
||||||
>
|
>
|
||||||
体验一下
|
Login as Guest
|
||||||
</button>
|
</button>
|
||||||
<span className="split-text">/</span>
|
<span className="split-text">/</span>
|
||||||
<button
|
<button
|
||||||
className={`btn signin-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
className={`btn signin-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||||
onClick={() => handleSigninBtnsClick("signup")}
|
onClick={() => handleSigninBtnsClick("signup")}
|
||||||
>
|
>
|
||||||
注册
|
Sign up
|
||||||
</button>
|
</button>
|
||||||
<span className="split-text">/</span>
|
<span className="split-text">/</span>
|
||||||
<button
|
<button
|
||||||
@ -186,7 +176,7 @@ const Signin: React.FC<Props> = () => {
|
|||||||
className={`btn signin-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
className={`btn signin-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||||
onClick={() => handleSigninBtnsClick("signin")}
|
onClick={() => handleSigninBtnsClick("signin")}
|
||||||
>
|
>
|
||||||
登录
|
Login
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import globalStateService from "./globalStateService";
|
import globalStateService from "./globalStateService";
|
||||||
import locationService from "./locationService";
|
import locationService from "./locationService";
|
||||||
import memoService from "./memoService";
|
import memoService from "./memoService";
|
||||||
import queryService from "./queryService";
|
import shortcutService from "./shortcutService";
|
||||||
import userService from "./userService";
|
import userService from "./userService";
|
||||||
import resourceService from "./resourceService";
|
import resourceService from "./resourceService";
|
||||||
|
|
||||||
export { globalStateService, locationService, memoService, queryService, userService, resourceService };
|
export { globalStateService, locationService, memoService, shortcutService, userService, resourceService };
|
||||||
|
@ -36,13 +36,13 @@ class LocationService {
|
|||||||
duration: null,
|
duration: null,
|
||||||
text: "",
|
text: "",
|
||||||
type: "",
|
type: "",
|
||||||
filter: "",
|
shortcutId: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
state.query.tag = urlParams.get("tag") ?? "";
|
state.query.tag = urlParams.get("tag") ?? "";
|
||||||
state.query.type = (urlParams.get("type") ?? "") as MemoSpecType;
|
state.query.type = (urlParams.get("type") ?? "") as MemoSpecType;
|
||||||
state.query.text = urlParams.get("text") ?? "";
|
state.query.text = urlParams.get("text") ?? "";
|
||||||
state.query.filter = urlParams.get("filter") ?? "";
|
state.query.shortcutId = urlParams.get("filter") ?? "";
|
||||||
const from = parseInt(urlParams.get("from") ?? "0");
|
const from = parseInt(urlParams.get("from") ?? "0");
|
||||||
const to = parseInt(urlParams.get("to") ?? "0");
|
const to = parseInt(urlParams.get("to") ?? "0");
|
||||||
if (to > from && to !== 0) {
|
if (to > from && to !== 0) {
|
||||||
@ -71,7 +71,7 @@ class LocationService {
|
|||||||
duration: null,
|
duration: null,
|
||||||
text: "",
|
text: "",
|
||||||
type: "",
|
type: "",
|
||||||
filter: "",
|
shortcutId: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -142,10 +142,10 @@ class LocationService {
|
|||||||
updateLocationUrl();
|
updateLocationUrl();
|
||||||
};
|
};
|
||||||
|
|
||||||
public setMemoFilter = (filterId: string) => {
|
public setMemoShortcut = (shortcutId: string) => {
|
||||||
appStore.dispatch({
|
appStore.dispatch({
|
||||||
type: "SET_QUERY_FILTER",
|
type: "SET_SHORTCUT_ID",
|
||||||
payload: filterId,
|
payload: shortcutId,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateLocationUrl();
|
updateLocationUrl();
|
||||||
|
@ -16,11 +16,10 @@ class MemoService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await api.getMyMemos();
|
const data = await api.getMyMemos();
|
||||||
const memos = [];
|
const memos: Model.Memo[] = data.map((m) => {
|
||||||
for (const m of data) {
|
return this.convertResponseModelMemo(m);
|
||||||
memos.push(m);
|
});
|
||||||
}
|
|
||||||
appStore.dispatch({
|
appStore.dispatch({
|
||||||
type: "SET_MEMOS",
|
type: "SET_MEMOS",
|
||||||
payload: {
|
payload: {
|
||||||
@ -40,9 +39,11 @@ class MemoService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await api.getMyDeletedMemos();
|
const data = await api.getMyDeletedMemos();
|
||||||
data.sort((a, b) => utils.getTimeStampByDate(b.deletedAt) - utils.getTimeStampByDate(a.deletedAt));
|
const deletedMemos: Model.Memo[] = data.map((m) => {
|
||||||
return data;
|
return this.convertResponseModelMemo(m);
|
||||||
|
});
|
||||||
|
return deletedMemos;
|
||||||
}
|
}
|
||||||
|
|
||||||
public pushMemo(memo: Model.Memo) {
|
public pushMemo(memo: Model.Memo) {
|
||||||
@ -125,13 +126,21 @@ class MemoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createMemo(text: string): Promise<Model.Memo> {
|
public async createMemo(text: string): Promise<Model.Memo> {
|
||||||
const { data: memo } = await api.createMemo(text);
|
const memo = await api.createMemo(text);
|
||||||
return memo;
|
return this.convertResponseModelMemo(memo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateMemo(memoId: string, text: string): Promise<Model.Memo> {
|
public async updateMemo(memoId: string, text: string): Promise<Model.Memo> {
|
||||||
const { data: memo } = await api.updateMemo(memoId, text);
|
const memo = await api.updateMemo(memoId, text);
|
||||||
return memo;
|
return this.convertResponseModelMemo(memo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertResponseModelMemo(memo: Model.Memo): Model.Memo {
|
||||||
|
return {
|
||||||
|
...memo,
|
||||||
|
createdAt: utils.getDataStringWithTs(memo.createdTs),
|
||||||
|
updatedAt: utils.getDataStringWithTs(memo.updatedTs),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
import userService from "./userService";
|
|
||||||
import api from "../helpers/api";
|
|
||||||
import appStore from "../stores/appStore";
|
|
||||||
|
|
||||||
class QueryService {
|
|
||||||
public getState() {
|
|
||||||
return appStore.getState().queryState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getMyAllQueries() {
|
|
||||||
if (!userService.getState().user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await api.getMyQueries();
|
|
||||||
appStore.dispatch({
|
|
||||||
type: "SET_QUERIES",
|
|
||||||
payload: {
|
|
||||||
queries: data,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getQueryById(id: string) {
|
|
||||||
for (const q of this.getState().queries) {
|
|
||||||
if (q.id === id) {
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public pushQuery(query: Model.Query) {
|
|
||||||
appStore.dispatch({
|
|
||||||
type: "INSERT_QUERY",
|
|
||||||
payload: {
|
|
||||||
query: {
|
|
||||||
...query,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public editQuery(query: Model.Query) {
|
|
||||||
appStore.dispatch({
|
|
||||||
type: "UPDATE_QUERY",
|
|
||||||
payload: query,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteQuery(queryId: string) {
|
|
||||||
await api.deleteQueryById(queryId);
|
|
||||||
appStore.dispatch({
|
|
||||||
type: "DELETE_QUERY_BY_ID",
|
|
||||||
payload: {
|
|
||||||
id: queryId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createQuery(title: string, querystring: string) {
|
|
||||||
const { data } = await api.createQuery(title, querystring);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateQuery(queryId: string, title: string, querystring: string) {
|
|
||||||
const { data } = await api.updateQuery(queryId, title, querystring);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async pinQuery(queryId: string) {
|
|
||||||
await api.pinQuery(queryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unpinQuery(queryId: string) {
|
|
||||||
await api.unpinQuery(queryId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryService = new QueryService();
|
|
||||||
|
|
||||||
export default queryService;
|
|
@ -10,14 +10,14 @@ class ResourceService {
|
|||||||
const { name: filename, size } = file;
|
const { name: filename, size } = file;
|
||||||
|
|
||||||
if (size > 5 << 20) {
|
if (size > 5 << 20) {
|
||||||
return Promise.reject("超过最大文件大小 5Mb");
|
return Promise.reject("overload max size: 5Mb");
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append("file", file, filename);
|
formData.append("file", file, filename);
|
||||||
|
|
||||||
const { data } = await api.uploadFile(formData);
|
const data = await api.uploadFile(formData);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
93
web/src/services/shortcutService.ts
Normal file
93
web/src/services/shortcutService.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import userService from "./userService";
|
||||||
|
import api from "../helpers/api";
|
||||||
|
import appStore from "../stores/appStore";
|
||||||
|
import utils from "../helpers/utils";
|
||||||
|
|
||||||
|
class ShortcutService {
|
||||||
|
public getState() {
|
||||||
|
return appStore.getState().shortcutState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getMyAllShortcuts() {
|
||||||
|
if (!userService.getState().user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await api.getMyShortcuts();
|
||||||
|
appStore.dispatch({
|
||||||
|
type: "SET_SHORTCUTS",
|
||||||
|
payload: {
|
||||||
|
shortcuts: data.map((s) => this.convertResponseModelShortcut(s)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getShortcutById(id: string) {
|
||||||
|
for (const q of this.getState().shortcuts) {
|
||||||
|
if (q.id === id) {
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushShortcut(shortcut: Model.Shortcut) {
|
||||||
|
appStore.dispatch({
|
||||||
|
type: "INSERT_SHORTCUT",
|
||||||
|
payload: {
|
||||||
|
shortcut: {
|
||||||
|
...shortcut,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public editShortcut(shortcut: Model.Shortcut) {
|
||||||
|
appStore.dispatch({
|
||||||
|
type: "UPDATE_SHORTCUT",
|
||||||
|
payload: shortcut,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteShortcut(shortcutId: string) {
|
||||||
|
await api.deleteShortcutById(shortcutId);
|
||||||
|
appStore.dispatch({
|
||||||
|
type: "DELETE_SHORTCUT_BY_ID",
|
||||||
|
payload: {
|
||||||
|
id: shortcutId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createShortcut(title: string, shortcutstring: string) {
|
||||||
|
const data = await api.createShortcut(title, shortcutstring);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateShortcut(shortcutId: string, title: string, shortcutstring: string) {
|
||||||
|
const data = await api.updateShortcut(shortcutId, title, shortcutstring);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pinShortcut(shortcutId: string) {
|
||||||
|
await api.pinShortcut(shortcutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unpinShortcut(shortcutId: string) {
|
||||||
|
await api.unpinShortcut(shortcutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public convertResponseModelShortcut(shortcut: Model.Shortcut): Model.Shortcut {
|
||||||
|
return {
|
||||||
|
...shortcut,
|
||||||
|
createdAt: utils.getDataStringWithTs(shortcut.createdTs),
|
||||||
|
updatedAt: utils.getDataStringWithTs(shortcut.updatedTs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcutService = new ShortcutService();
|
||||||
|
|
||||||
|
export default shortcutService;
|
@ -1,4 +1,5 @@
|
|||||||
import api from "../helpers/api";
|
import api from "../helpers/api";
|
||||||
|
import utils from "../helpers/utils";
|
||||||
import appStore from "../stores/appStore";
|
import appStore from "../stores/appStore";
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
@ -7,11 +8,13 @@ class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async doSignIn() {
|
public async doSignIn() {
|
||||||
const { data: user } = await api.getUserInfo();
|
const user = await api.getUserInfo();
|
||||||
if (user) {
|
if (user) {
|
||||||
appStore.dispatch({
|
appStore.dispatch({
|
||||||
type: "SIGN_IN",
|
type: "LOGIN",
|
||||||
payload: { user },
|
payload: {
|
||||||
|
user: this.convertResponseModelUser(user),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
userService.doSignOut();
|
userService.doSignOut();
|
||||||
@ -30,18 +33,18 @@ class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async checkUsernameUsable(username: string): Promise<boolean> {
|
public async checkUsernameUsable(username: string): Promise<boolean> {
|
||||||
const { data: isUsable } = await api.checkUsernameUsable(username);
|
const isUsable = await api.checkUsernameUsable(username);
|
||||||
return isUsable;
|
return isUsable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateUsername(username: string): Promise<void> {
|
public async updateUsername(name: string): Promise<void> {
|
||||||
await api.updateUserinfo({
|
await api.updateUserinfo({
|
||||||
username,
|
name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkPasswordValid(password: string): Promise<boolean> {
|
public async checkPasswordValid(password: string): Promise<boolean> {
|
||||||
const { data: isValid } = await api.checkPasswordValid(password);
|
const isValid = await api.checkPasswordValid(password);
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,12 +55,22 @@ class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async resetOpenId(): Promise<string> {
|
public async resetOpenId(): Promise<string> {
|
||||||
const { data: openId } = await api.resetOpenId();
|
const user = await api.updateUserinfo({
|
||||||
|
resetOpenId: true,
|
||||||
|
});
|
||||||
appStore.dispatch({
|
appStore.dispatch({
|
||||||
type: "RESET_OPENID",
|
type: "RESET_OPENID",
|
||||||
payload: openId,
|
payload: user.openId,
|
||||||
});
|
});
|
||||||
return openId;
|
return user.openId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertResponseModelUser(user: Model.User): Model.User {
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
createdAt: utils.getDataStringWithTs(user.createdTs),
|
||||||
|
updatedAt: utils.getDataStringWithTs(user.updatedTs),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,17 +4,17 @@ import * as globalStore from "./globalStateStore";
|
|||||||
import * as locationStore from "./locationStore";
|
import * as locationStore from "./locationStore";
|
||||||
import * as memoStore from "./memoStore";
|
import * as memoStore from "./memoStore";
|
||||||
import * as userStore from "./userStore";
|
import * as userStore from "./userStore";
|
||||||
import * as queryStore from "./queryStore";
|
import * as shortcutStore from "./shortcutStore";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
globalState: globalStore.State;
|
globalState: globalStore.State;
|
||||||
locationState: locationStore.State;
|
locationState: locationStore.State;
|
||||||
memoState: memoStore.State;
|
memoState: memoStore.State;
|
||||||
userState: userStore.State;
|
userState: userStore.State;
|
||||||
queryState: queryStore.State;
|
shortcutState: shortcutStore.State;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppStateActions = globalStore.Actions | locationStore.Actions | memoStore.Actions | userStore.Actions | queryStore.Actions;
|
type AppStateActions = globalStore.Actions | locationStore.Actions | memoStore.Actions | userStore.Actions | shortcutStore.Actions;
|
||||||
|
|
||||||
const appStore = createStore<AppState, AppStateActions>(
|
const appStore = createStore<AppState, AppStateActions>(
|
||||||
{
|
{
|
||||||
@ -22,14 +22,14 @@ const appStore = createStore<AppState, AppStateActions>(
|
|||||||
locationState: locationStore.defaultState,
|
locationState: locationStore.defaultState,
|
||||||
memoState: memoStore.defaultState,
|
memoState: memoStore.defaultState,
|
||||||
userState: userStore.defaultState,
|
userState: userStore.defaultState,
|
||||||
queryState: queryStore.defaultState,
|
shortcutState: shortcutStore.defaultState,
|
||||||
},
|
},
|
||||||
combineReducers<AppState, AppStateActions>({
|
combineReducers<AppState, AppStateActions>({
|
||||||
globalState: globalStore.reducer,
|
globalState: globalStore.reducer,
|
||||||
locationState: locationStore.reducer,
|
locationState: locationStore.reducer,
|
||||||
memoState: memoStore.reducer,
|
memoState: memoStore.reducer,
|
||||||
userState: userStore.reducer,
|
userState: userStore.reducer,
|
||||||
queryState: queryStore.reducer,
|
shortcutState: shortcutStore.reducer,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -6,10 +6,10 @@ export interface AppSetting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface State extends AppSetting {
|
export interface State extends AppSetting {
|
||||||
markMemoId: string;
|
|
||||||
editMemoId: string;
|
|
||||||
isMobileView: boolean;
|
isMobileView: boolean;
|
||||||
showSiderbarInMobileView: boolean;
|
showSiderbarInMobileView: boolean;
|
||||||
|
markMemoId: string;
|
||||||
|
editMemoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SetMarkMemoIdAction {
|
interface SetMarkMemoIdAction {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export type State = AppLocation;
|
export type State = AppLocation;
|
||||||
|
|
||||||
interface SetLocation {
|
interface SetLocationAction {
|
||||||
type: "SET_LOCATION";
|
type: "SET_LOCATION";
|
||||||
payload: State;
|
payload: State;
|
||||||
}
|
}
|
||||||
@ -12,13 +12,13 @@ interface SetPathnameAction {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SetQuery {
|
interface SetQueryAction {
|
||||||
type: "SET_QUERY";
|
type: "SET_QUERY";
|
||||||
payload: Query;
|
payload: Query;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SetQueryFilterAction {
|
interface SetShortcutIdAction {
|
||||||
type: "SET_QUERY_FILTER";
|
type: "SET_SHORTCUT_ID";
|
||||||
payload: string;
|
payload: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,14 +58,14 @@ interface SetHashAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Actions =
|
export type Actions =
|
||||||
| SetLocation
|
| SetLocationAction
|
||||||
| SetPathnameAction
|
| SetPathnameAction
|
||||||
| SetQuery
|
| SetQueryAction
|
||||||
| SetTagQueryAction
|
| SetTagQueryAction
|
||||||
| SetFromAndToQueryAction
|
| SetFromAndToQueryAction
|
||||||
| SetTypeAction
|
| SetTypeAction
|
||||||
| SetTextAction
|
| SetTextAction
|
||||||
| SetQueryFilterAction
|
| SetShortcutIdAction
|
||||||
| SetHashAction;
|
| SetHashAction;
|
||||||
|
|
||||||
export function reducer(state: State, action: Actions) {
|
export function reducer(state: State, action: Actions) {
|
||||||
@ -156,8 +156,8 @@ export function reducer(state: State, action: Actions) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "SET_QUERY_FILTER": {
|
case "SET_SHORTCUT_ID": {
|
||||||
if (action.payload === state.query.filter) {
|
if (action.payload === state.query.shortcutId) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +183,6 @@ export const defaultState: State = {
|
|||||||
duration: null,
|
duration: null,
|
||||||
type: "",
|
type: "",
|
||||||
text: "",
|
text: "",
|
||||||
filter: "",
|
shortcutId: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
import utils from "../helpers/utils";
|
|
||||||
|
|
||||||
export interface State {
|
|
||||||
queries: Model.Query[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetQueries {
|
|
||||||
type: "SET_QUERIES";
|
|
||||||
payload: {
|
|
||||||
queries: Model.Query[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InsertQueryAction {
|
|
||||||
type: "INSERT_QUERY";
|
|
||||||
payload: {
|
|
||||||
query: Model.Query;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteQueryByIdAction {
|
|
||||||
type: "DELETE_QUERY_BY_ID";
|
|
||||||
payload: {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateQueryAction {
|
|
||||||
type: "UPDATE_QUERY";
|
|
||||||
payload: Model.Query;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Actions = SetQueries | InsertQueryAction | DeleteQueryByIdAction | UpdateQueryAction;
|
|
||||||
|
|
||||||
export function reducer(state: State, action: Actions): State {
|
|
||||||
switch (action.type) {
|
|
||||||
case "SET_QUERIES": {
|
|
||||||
const queries = utils.dedupeObjectWithId(
|
|
||||||
action.payload.queries
|
|
||||||
.sort((a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt))
|
|
||||||
.sort((a, b) => utils.getTimeStampByDate(b.pinnedAt ?? 0) - utils.getTimeStampByDate(a.pinnedAt ?? 0))
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
queries,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "INSERT_QUERY": {
|
|
||||||
const queries = utils.dedupeObjectWithId(
|
|
||||||
[action.payload.query, ...state.queries].sort(
|
|
||||||
(a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
queries,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "DELETE_QUERY_BY_ID": {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
queries: [...state.queries].filter((query) => query.id !== action.payload.id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "UPDATE_QUERY": {
|
|
||||||
const queries = state.queries.map((m) => {
|
|
||||||
if (m.id === action.payload.id) {
|
|
||||||
return {
|
|
||||||
...m,
|
|
||||||
...action.payload,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
queries,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultState: State = {
|
|
||||||
queries: [],
|
|
||||||
};
|
|
92
web/src/stores/shortcutStore.ts
Normal file
92
web/src/stores/shortcutStore.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import utils from "../helpers/utils";
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
shortcuts: Model.Shortcut[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SetShortcutsAction {
|
||||||
|
type: "SET_SHORTCUTS";
|
||||||
|
payload: {
|
||||||
|
shortcuts: Model.Shortcut[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InsertShortcutAction {
|
||||||
|
type: "INSERT_SHORTCUT";
|
||||||
|
payload: {
|
||||||
|
shortcut: Model.Shortcut;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteShortcutByIdAction {
|
||||||
|
type: "DELETE_SHORTCUT_BY_ID";
|
||||||
|
payload: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateShortcutAction {
|
||||||
|
type: "UPDATE_SHORTCUT";
|
||||||
|
payload: Model.Shortcut;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Actions = SetShortcutsAction | InsertShortcutAction | DeleteShortcutByIdAction | UpdateShortcutAction;
|
||||||
|
|
||||||
|
export function reducer(state: State, action: Actions): State {
|
||||||
|
switch (action.type) {
|
||||||
|
case "SET_SHORTCUTS": {
|
||||||
|
const shortcuts = utils.dedupeObjectWithId(
|
||||||
|
action.payload.shortcuts
|
||||||
|
.sort((a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt))
|
||||||
|
.sort((a, b) => utils.getTimeStampByDate(b.updatedAt) - utils.getTimeStampByDate(a.updatedAt))
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
shortcuts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "INSERT_SHORTCUT": {
|
||||||
|
const shortcuts = utils.dedupeObjectWithId(
|
||||||
|
[action.payload.shortcut, ...state.shortcuts].sort(
|
||||||
|
(a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
shortcuts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "DELETE_SHORTCUT_BY_ID": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
shortcuts: [...state.shortcuts].filter((shortcut) => shortcut.id !== action.payload.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "UPDATE_SHORTCUT": {
|
||||||
|
const shortcuts = state.shortcuts.map((m) => {
|
||||||
|
if (m.id === action.payload.id) {
|
||||||
|
return {
|
||||||
|
...m,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
shortcuts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultState: State = {
|
||||||
|
shortcuts: [],
|
||||||
|
};
|
@ -3,7 +3,7 @@ export interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SignInAction {
|
interface SignInAction {
|
||||||
type: "SIGN_IN";
|
type: "LOGIN";
|
||||||
payload: State;
|
payload: State;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ export type Actions = SignInAction | SignOutAction | ResetOpenIdAction;
|
|||||||
|
|
||||||
export function reducer(state: State, action: Actions): State {
|
export function reducer(state: State, action: Actions): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "SIGN_IN": {
|
case "LOGIN": {
|
||||||
return {
|
return {
|
||||||
user: action.payload.user,
|
user: action.payload.user,
|
||||||
};
|
};
|
||||||
|
7
web/src/types/api.d.ts
vendored
7
web/src/types/api.d.ts
vendored
@ -1,6 +1 @@
|
|||||||
declare namespace Api {
|
declare namespace Api {}
|
||||||
interface MemosStat {
|
|
||||||
timestamp: string;
|
|
||||||
amount: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
2
web/src/types/basic.d.ts
vendored
2
web/src/types/basic.d.ts
vendored
@ -1,9 +1,7 @@
|
|||||||
type BasicType = undefined | null | boolean | number | string | Record<string, unknown> | Array<BasicType>;
|
type BasicType = undefined | null | boolean | number | string | Record<string, unknown> | Array<BasicType>;
|
||||||
|
|
||||||
// 日期戳
|
|
||||||
type DateStamp = number;
|
type DateStamp = number;
|
||||||
|
|
||||||
// 时间戳
|
|
||||||
type TimeStamp = number;
|
type TimeStamp = number;
|
||||||
|
|
||||||
type FunctionType = (...args: unknown[]) => unknown;
|
type FunctionType = (...args: unknown[]) => unknown;
|
||||||
|
2
web/src/types/location.d.ts
vendored
2
web/src/types/location.d.ts
vendored
@ -8,7 +8,7 @@ interface Query {
|
|||||||
duration: Duration | null;
|
duration: Duration | null;
|
||||||
type: MemoSpecType | "";
|
type: MemoSpecType | "";
|
||||||
text: string;
|
text: string;
|
||||||
filter: string;
|
shortcutId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppRouter = "/" | "/signin" | "/recycle" | "/setting";
|
type AppRouter = "/" | "/signin" | "/recycle" | "/setting";
|
||||||
|
16
web/src/types/models.d.ts
vendored
16
web/src/types/models.d.ts
vendored
@ -1,28 +1,30 @@
|
|||||||
declare namespace Model {
|
declare namespace Model {
|
||||||
interface BaseModel {
|
interface BaseModel {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdTs: number;
|
||||||
|
updatedTs: number;
|
||||||
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User extends BaseModel {
|
interface User extends BaseModel {
|
||||||
username: string;
|
name: string;
|
||||||
openId: string;
|
openId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Memo extends BaseModel {
|
interface Memo extends BaseModel {
|
||||||
content: string;
|
content: string;
|
||||||
deletedAt: string;
|
rowStatus: "NORMAL" | "HIDDEN";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Query extends BaseModel {
|
interface Shortcut extends BaseModel {
|
||||||
title: string;
|
title: string;
|
||||||
querystring: string;
|
payload: string;
|
||||||
pinnedAt: string;
|
rowStatus: "NORMAL" | "ARCHIVED";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Resource {
|
interface Resource extends BaseModel {
|
||||||
id: string;
|
|
||||||
filename: string;
|
filename: string;
|
||||||
type: string;
|
type: string;
|
||||||
size: string;
|
size: string;
|
||||||
|
@ -8,11 +8,11 @@ export default defineConfig({
|
|||||||
cors: true,
|
cors: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
// target: "http://localhost:8080/",
|
target: "http://localhost:8080/",
|
||||||
target: "https://memos.justsven.top/",
|
// target: "https://memos.justsven.top/",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
"/r/": {
|
"/h/": {
|
||||||
target: "https://memos.justsven.top/",
|
target: "https://memos.justsven.top/",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user