Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8a7df21f2 | |||
| d1a4348048 | |||
| 020b211660 | |||
| 4657e58b52 | |||
| 22971c3a93 | |||
| 5fa9aa3c22 | |||
| fbce43870f | |||
| b1e6956441 | |||
| ad462cec29 | |||
| a0a42285d0 | |||
| b0b2776d03 | |||
| e9ac6affef | |||
| 5eea1339c9 | |||
| f303dc21a0 | |||
| d68891d91d | |||
| 987bb80770 | |||
| b884327a53 | |||
| 4743818fe7 | |||
| 43575e6f54 | |||
| 89f9dc5640 | |||
| f7aca99d52 | |||
| 12a48ae2db | |||
| bcb684d1cc | |||
| d3b26f7126 | |||
| 422e190c96 | |||
| 3e13fa1ce6 | |||
| dc9f531447 | |||
| e330159f55 | |||
| b88846fff5 | |||
| 93b6a910ae | |||
| 8d161b4526 | |||
| b6acf62aab | |||
| 2a11aed881 | |||
| e7b287902b | |||
| c012ce0481 | |||
| c97399a8ea | |||
| 75d622f4a2 | |||
| 5bdf7654fc | |||
| 62657f7f4e | |||
| 64332c3e6a | |||
| 57f51d1c58 | |||
| 9f3f730723 | |||
| e9d303326f | |||
| 20d7112a05 | |||
| 922cc21abc | |||
| cdbd934c4e | |||
| ca1170583e | |||
| f5629c8227 | |||
| 2f58032ad8 | |||
| 466bfe4b49 | |||
| 7d0407013e | |||
| 0e4e2e4bc5 | |||
| a8a3cf31b4 | |||
| f784516470 | |||
| 2aed7c70aa | |||
| b1062f5387 | |||
| 29c835d27a | |||
| 0698c9c853 | |||
| e54ff5ec9e | |||
| f0a23f4620 | |||
| c60bb12424 | |||
| 3b1bb4a95d | |||
| 05a5c59a7e | |||
| 734d5f3aed | |||
| 2cf07753e7 | |||
| 2935057ca7 | |||
| 68b30063a9 | |||
| f06a3d171b | |||
| a98e64cf0a | |||
| dd04bc9e1d | |||
| 2f33eceada | |||
| d5b88775d9 | |||
| a7a01df79a | |||
| e3fac742c5 | |||
| ce390f3f79 | |||
| 43a8b7d0e1 | |||
| b596d04939 | |||
| 84a3548232 | |||
| 63388a7d97 | |||
| 35f980d2b8 | |||
| 90b881502d | |||
| dfac877957 | |||
| 87f5ac8b71 | |||
| 3c1a416afc | |||
| 972a49d6aa | |||
| cea16fac88 | |||
| 7e994c8f11 | |||
| 646a41e931 | |||
| 735938395b | |||
| d8e10ba399 | |||
| 8c28721839 | |||
| da333b0b1e | |||
| 44d07ac401 | |||
| d08ba56c28 | |||
| c991a48df6 | |||
| fd44255668 | |||
| 84564891be | |||
| 8c8bb9e59f | |||
| 99df4acfe9 | |||
| 47ffd99c3b | |||
| c703f281d9 | |||
| e179c65e52 | |||
| 29da70be56 | |||
| a9dce26099 | |||
| 2c27f5d425 | |||
| df7b4d54c6 | |||
| 9994b1fabc | |||
| 2d093d5be0 | |||
| 12b373701b | |||
| 2b8078a19b | |||
| 5617118fa8 | |||
| fa93d0fd6e | |||
| dc436490f8 | |||
| d83f204d8c | |||
| 873973a088 | |||
| d371cfd78d | |||
| 7b1bad5b29 | |||
| 0c2adfa1d2 | |||
| 07d9649b22 | |||
| b7339e00ba | |||
| 58e68f8f80 | |||
| cfa4151cff | |||
| 3d33b5d564 | |||
| b516a8561f | |||
| 38383a426f | |||
| 7e34de23f1 | |||
| f02ec375a6 |
@@ -1,37 +0,0 @@
|
||||
name: build-and-push-dev-image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
build-and-push-dev-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: neosmemo
|
||||
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Build and Push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: neosmemo/memos:dev
|
||||
@@ -1,37 +0,0 @@
|
||||
name: build-and-push-test-image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "test/*"
|
||||
|
||||
jobs:
|
||||
build-and-push-dev-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: neosmemo
|
||||
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Build and Push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: neosmemo/memos:test
|
||||
@@ -13,12 +13,12 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '27 12 * * 0'
|
||||
- cron: "27 12 * * 0"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -32,39 +32,39 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript' ]
|
||||
language: ["go", "javascript"]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "release/v*.*.*"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
go-static-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: Verify go.mod is tidy
|
||||
run: |
|
||||
go mod tidy
|
||||
git diff --exit-code
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
args: -v
|
||||
skip-cache: true
|
||||
|
||||
eslint-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
cache: yarn
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
- run: yarn
|
||||
working-directory: web
|
||||
- name: Run eslint check
|
||||
run: yarn lint
|
||||
working-directory: web
|
||||
|
||||
frontend-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
cache: yarn
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
- run: yarn
|
||||
working-directory: web
|
||||
- name: Run frontend build
|
||||
run: yarn build
|
||||
working-directory: web
|
||||
|
||||
go-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: Run all tests
|
||||
run: go test -v ./... | tee test.log; exit ${PIPESTATUS[0]}
|
||||
- name: Pretty print tests running time
|
||||
run: grep --color=never -e '--- PASS:' -e '--- FAIL:' test.log | sed 's/[:()]//g' | awk '{print $2,$3,$4}' | sort -t' ' -nk3 -r | awk '{sum += $3; print $1,$2,$3,sum"s"}'
|
||||
+4
-1
@@ -1,10 +1,13 @@
|
||||
# Air (hot reload) generated
|
||||
.air
|
||||
|
||||
# temp folder
|
||||
tmp
|
||||
|
||||
# Frontend asset
|
||||
web/dist
|
||||
|
||||
# build folder
|
||||
memos-build
|
||||
build
|
||||
|
||||
.DS_Store
|
||||
@@ -0,0 +1,64 @@
|
||||
linters:
|
||||
enable:
|
||||
- goimports
|
||||
- revive
|
||||
- govet
|
||||
- staticcheck
|
||||
- misspell
|
||||
- gocritic
|
||||
- sqlclosecheck
|
||||
- rowserrcheck
|
||||
- nilerr
|
||||
- godot
|
||||
|
||||
issues:
|
||||
exclude:
|
||||
- Rollback
|
||||
- fmt.Printf
|
||||
- fmt.Print
|
||||
|
||||
linters-settings:
|
||||
revive:
|
||||
enable-all-rules: true
|
||||
rules:
|
||||
- name: file-header
|
||||
disabled: true
|
||||
- name: line-length-limit
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: max-public-structs
|
||||
disabled: true
|
||||
- name: function-result-limit
|
||||
disabled: true
|
||||
- name: banned-characters
|
||||
disabled: true
|
||||
- name: argument-limit
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
- name: confusing-results
|
||||
disabled: true
|
||||
- name: add-constant
|
||||
disabled: true
|
||||
- name: flag-parameter
|
||||
disabled: true
|
||||
- name: nested-structs
|
||||
disabled: true
|
||||
- name: import-shadowing
|
||||
disabled: true
|
||||
- name: early-return
|
||||
disabled: true
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
govet:
|
||||
settings:
|
||||
printf:
|
||||
funcs:
|
||||
- common.Errorf
|
||||
forbidigo:
|
||||
forbid:
|
||||
- 'fmt\.Errorf(# Please use errors\.Wrap\|Wrapf\|Errorf instead)?'
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.lintTool": "golangci-lint"
|
||||
}
|
||||
+2
-4
@@ -17,9 +17,7 @@ RUN apk --no-cache add gcc musl-dev
|
||||
COPY . .
|
||||
COPY --from=frontend /frontend-build/dist ./server/dist
|
||||
|
||||
RUN go build \
|
||||
-o memos \
|
||||
./bin/server/main.go
|
||||
RUN go build -o memos ./bin/server/main.go
|
||||
|
||||
# Make workspace with above generated files.
|
||||
FROM alpine:3.16.0 AS monolithic
|
||||
@@ -30,4 +28,4 @@ COPY --from=backend /backend-build/memos /usr/local/memos/
|
||||
# Directory to store the data, which can be referenced as the mounting point.
|
||||
RUN mkdir -p /var/opt/memos
|
||||
|
||||
ENTRYPOINT ["./memos"]
|
||||
ENTRYPOINT ["./memos", "--mode", "prod", "--port", "5230"]
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<h1 align="center">✍️ Memos</h1>
|
||||
<p align="center"><a href="https://usememos.com"><img height="64px" src="https://raw.githubusercontent.com/usememos/memos/main/resources/logo-full.webp" alt="✍️ memos" /></a></p>
|
||||
|
||||
<p align="center">An open source, self-hosted knowledge base that works with a SQLite db file.</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" />
|
||||
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
|
||||
<a href="https://hub.docker.com/r/neosmemo/memos"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" /></a>
|
||||
<img alt="Go report" src="https://goreportcard.com/badge/github.com/usememos/memos" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://memos.onrender.com/">Live Demo</a> •
|
||||
<a href="https://demo.usememos.com/">Live Demo</a> •
|
||||
<a href="https://t.me/+-_tNF1k70UU4ZTc9">Discuss in Telegram 👾</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## ✨ Features
|
||||
## Features
|
||||
|
||||
- 🦄 Fully open source;
|
||||
- 📜 Writing in plain textarea without any burden,
|
||||
@@ -24,66 +24,26 @@
|
||||
- 🚀 Fast self-hosting with `Docker`;
|
||||
- 🤠 Pleasant UI and UX;
|
||||
|
||||
## ⚓️ Deploy with Docker
|
||||
## Deploy with Docker
|
||||
|
||||
### Docker Run
|
||||
|
||||
```docker
|
||||
docker run \
|
||||
--name memos \
|
||||
--publish 5230:5230 \
|
||||
--volume ~/.memos/:/var/opt/memos \
|
||||
neosmemo/memos:latest \
|
||||
--mode prod \
|
||||
--port 5230
|
||||
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:latest
|
||||
```
|
||||
|
||||
Memos should now be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then `memos` will auto generate it.
|
||||
Memos should be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then memos will auto generate it.
|
||||
|
||||
⚠️ Please DO NOT use `dev` tag of docker image if you have no experience.
|
||||
### Docker Compose
|
||||
|
||||
## 🏗 Development
|
||||
See more in the example [`docker-compose.yaml`](./docker-compose.yaml) file.
|
||||
|
||||
Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
|
||||
## Contributing
|
||||
|
||||
1. It has no external dependency.
|
||||
2. It requires zero config.
|
||||
3. 1 command to start backend and 1 command to start frontend, both with live reload support.
|
||||
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
|
||||
|
||||
### Tech Stack
|
||||
Gets more about [development guide](https://github.com/usememos/memos/tree/main/docs/development.md).
|
||||
|
||||
<img alt="tech stack" src="https://raw.githubusercontent.com/usememos/memos/main/resources/tech-stack.png" width="360" />
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Go](https://golang.org/doc/install) (1.16 or later)
|
||||
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
|
||||
- [yarn](https://yarnpkg.com/getting-started/install)
|
||||
|
||||
### Steps
|
||||
|
||||
1. pull source code
|
||||
|
||||
```bash
|
||||
git clone https://github.com/usememos/memos
|
||||
```
|
||||
|
||||
2. start backend using air(with live reload)
|
||||
|
||||
```bash
|
||||
air -c scripts/.air.toml
|
||||
```
|
||||
|
||||
3. start frontend dev server
|
||||
|
||||
```bash
|
||||
cd web && yarn && yarn dev
|
||||
```
|
||||
|
||||
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
|
||||
|
||||
## 🌟 Star history
|
||||
## Star history
|
||||
|
||||
[](https://star-history.com/#usememos/memos&Date)
|
||||
|
||||
---
|
||||
|
||||
Just enjoy it.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package api
|
||||
|
||||
// CacheNamespace is the type of a cache.
|
||||
type CacheNamespace string
|
||||
|
||||
const (
|
||||
// UserCache is the cache type of users.
|
||||
UserCache CacheNamespace = "u"
|
||||
// MemoCache is the cache type of memos.
|
||||
MemoCache CacheNamespace = "m"
|
||||
// ShortcutCache is the cache type of shortcuts.
|
||||
ShortcutCache CacheNamespace = "s"
|
||||
// ResourceCache is the cache type of resources.
|
||||
ResourceCache CacheNamespace = "r"
|
||||
)
|
||||
|
||||
// CacheService is the service for caches.
|
||||
type CacheService interface {
|
||||
FindCache(namespace CacheNamespace, id int, entry interface{}) (bool, error)
|
||||
UpsertCache(namespace CacheNamespace, id int, entry interface{}) error
|
||||
DeleteCache(namespace CacheNamespace, id int)
|
||||
}
|
||||
+13
-7
@@ -6,6 +6,8 @@ type Visibility string
|
||||
const (
|
||||
// Public is the PUBLIC visibility.
|
||||
Public Visibility = "PUBLIC"
|
||||
// Protected is the PROTECTED visibility.
|
||||
Protected Visibility = "PROTECTED"
|
||||
// Privite is the PRIVATE visibility.
|
||||
Privite Visibility = "PRIVATE"
|
||||
)
|
||||
@@ -14,6 +16,8 @@ func (e Visibility) String() string {
|
||||
switch e {
|
||||
case Public:
|
||||
return "PUBLIC"
|
||||
case Protected:
|
||||
return "PROTECTED"
|
||||
case Privite:
|
||||
return "PRIVATE"
|
||||
}
|
||||
@@ -33,23 +37,25 @@ type Memo struct {
|
||||
Content string `json:"content"`
|
||||
Visibility Visibility `json:"visibility"`
|
||||
Pinned bool `json:"pinned"`
|
||||
|
||||
// Related fields
|
||||
Creator *User `json:"creator"`
|
||||
}
|
||||
|
||||
type MemoCreate struct {
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
// Used to import memos with a clearly created ts.
|
||||
CreatedTs *int64 `json:"createdTs"`
|
||||
|
||||
// Domain specific fields
|
||||
Content string `json:"content"`
|
||||
Visibility *Visibility `json:"visibility"`
|
||||
Visibility Visibility `json:"visibility"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type MemoPatch struct {
|
||||
ID int
|
||||
|
||||
// Standard fields
|
||||
CreatedTs *int64 `json:"createdTs"`
|
||||
RowStatus *RowStatus `json:"rowStatus"`
|
||||
|
||||
// Domain specific fields
|
||||
@@ -65,9 +71,9 @@ type MemoFind struct {
|
||||
CreatorID *int `json:"creatorId"`
|
||||
|
||||
// Domain specific fields
|
||||
Pinned *bool
|
||||
ContentSearch *string
|
||||
Visibility *Visibility
|
||||
Pinned *bool
|
||||
ContentSearch *string
|
||||
VisibilityList []Visibility
|
||||
|
||||
// Pagination
|
||||
Limit int
|
||||
|
||||
+4
-1
@@ -10,7 +10,7 @@ type Resource struct {
|
||||
|
||||
// Domain specific fields
|
||||
Filename string `json:"filename"`
|
||||
Blob []byte `json:"blob"`
|
||||
Blob []byte `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
@@ -38,4 +38,7 @@ type ResourceFind struct {
|
||||
|
||||
type ResourceDelete struct {
|
||||
ID int
|
||||
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
}
|
||||
|
||||
+30
-5
@@ -1,5 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/usememos/memos/common"
|
||||
)
|
||||
|
||||
// Role is the type of a role.
|
||||
type Role string
|
||||
|
||||
@@ -29,11 +35,12 @@ type User struct {
|
||||
UpdatedTs int64 `json:"updatedTs"`
|
||||
|
||||
// Domain specific fields
|
||||
Email string `json:"email"`
|
||||
Role Role `json:"role"`
|
||||
Name string `json:"name"`
|
||||
PasswordHash string `json:"-"`
|
||||
OpenID string `json:"openId"`
|
||||
Email string `json:"email"`
|
||||
Role Role `json:"role"`
|
||||
Name string `json:"name"`
|
||||
PasswordHash string `json:"-"`
|
||||
OpenID string `json:"openId"`
|
||||
UserSettingList []*UserSetting `json:"userSettingList"`
|
||||
}
|
||||
|
||||
type UserCreate struct {
|
||||
@@ -46,6 +53,20 @@ type UserCreate struct {
|
||||
OpenID string
|
||||
}
|
||||
|
||||
func (create UserCreate) Validate() error {
|
||||
if !common.ValidateEmail(create.Email) {
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
if len(create.Email) < 6 {
|
||||
return fmt.Errorf("email is too short, minimum length is 6")
|
||||
}
|
||||
if len(create.Password) < 6 {
|
||||
return fmt.Errorf("password is too short, minimum length is 6")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserPatch struct {
|
||||
ID int
|
||||
|
||||
@@ -73,3 +94,7 @@ type UserFind struct {
|
||||
Name *string `json:"name"`
|
||||
OpenID *string
|
||||
}
|
||||
|
||||
type UserDelete struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type UserSettingKey string
|
||||
|
||||
const (
|
||||
// UserSettingLocaleKey is the key type for user locale.
|
||||
UserSettingLocaleKey UserSettingKey = "locale"
|
||||
// UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
|
||||
UserSettingMemoVisibilityKey UserSettingKey = "memoVisibility"
|
||||
// UserSettingEditorFontStyleKey is the key type for editor font style.
|
||||
UserSettingEditorFontStyleKey UserSettingKey = "editorFontStyle"
|
||||
)
|
||||
|
||||
// String returns the string format of UserSettingKey type.
|
||||
func (key UserSettingKey) String() string {
|
||||
switch key {
|
||||
case UserSettingLocaleKey:
|
||||
return "locale"
|
||||
case UserSettingMemoVisibilityKey:
|
||||
return "memoVisibility"
|
||||
case UserSettingEditorFontStyleKey:
|
||||
return "editorFontFamily"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
UserSettingLocaleValue = []string{"en", "zh"}
|
||||
UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public}
|
||||
UserSettingEditorFontStyleValue = []string{"normal", "mono"}
|
||||
)
|
||||
|
||||
type UserSetting struct {
|
||||
UserID int
|
||||
Key UserSettingKey `json:"key"`
|
||||
// Value is a JSON string with basic value
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type UserSettingUpsert struct {
|
||||
UserID int
|
||||
Key UserSettingKey `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (upsert UserSettingUpsert) Validate() error {
|
||||
if upsert.Key == UserSettingLocaleKey {
|
||||
localeValue := "en"
|
||||
err := json.Unmarshal([]byte(upsert.Value), &localeValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal user setting locale value")
|
||||
}
|
||||
|
||||
invalid := true
|
||||
for _, value := range UserSettingLocaleValue {
|
||||
if localeValue == value {
|
||||
invalid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if invalid {
|
||||
return fmt.Errorf("invalid user setting locale value")
|
||||
}
|
||||
} else if upsert.Key == UserSettingMemoVisibilityKey {
|
||||
memoVisibilityValue := Privite
|
||||
err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
|
||||
}
|
||||
|
||||
invalid := true
|
||||
for _, value := range UserSettingMemoVisibilityValue {
|
||||
if memoVisibilityValue == value {
|
||||
invalid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if invalid {
|
||||
return fmt.Errorf("invalid user setting memo visibility value")
|
||||
}
|
||||
} else if upsert.Key == UserSettingEditorFontStyleKey {
|
||||
editorFontStyleValue := "normal"
|
||||
err := json.Unmarshal([]byte(upsert.Value), &editorFontStyleValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal user setting editor font style")
|
||||
}
|
||||
|
||||
invalid := true
|
||||
for _, value := range UserSettingEditorFontStyleValue {
|
||||
if editorFontStyleValue == value {
|
||||
invalid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if invalid {
|
||||
return fmt.Errorf("invalid user setting editor font style value")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("invalid user setting key")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserSettingFind struct {
|
||||
UserID int
|
||||
|
||||
Key *UserSettingKey `json:"key"`
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/usememos/memos/server"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
"github.com/usememos/memos/store"
|
||||
DB "github.com/usememos/memos/store/db"
|
||||
)
|
||||
|
||||
const (
|
||||
greetingBanner = `
|
||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
|
||||
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
|
||||
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
|
||||
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
|
||||
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
|
||||
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
||||
`
|
||||
)
|
||||
|
||||
type Main struct {
|
||||
profile *profile.Profile
|
||||
}
|
||||
|
||||
func (m *Main) Run() error {
|
||||
db := DB.NewDB(m.profile)
|
||||
if err := db.Open(); err != nil {
|
||||
return fmt.Errorf("cannot open db: %w", err)
|
||||
}
|
||||
|
||||
s := server.NewServer(m.profile)
|
||||
|
||||
storeInstance := store.New(db.Db, m.profile)
|
||||
s.Store = storeInstance
|
||||
|
||||
println(greetingBanner)
|
||||
fmt.Printf("Version %s has started at :%d\n", m.profile.Version, m.profile.Port)
|
||||
|
||||
return s.Run()
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
profile := profile.GetProfile()
|
||||
m := Main{
|
||||
profile: profile,
|
||||
}
|
||||
|
||||
println("---")
|
||||
println("profile")
|
||||
println("mode:", profile.Mode)
|
||||
println("port:", profile.Port)
|
||||
println("dsn:", profile.DSN)
|
||||
println("version:", profile.Version)
|
||||
println("---")
|
||||
|
||||
if err := m.Run(); err != nil {
|
||||
fmt.Printf("error: %+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
+69
-2
@@ -1,7 +1,74 @@
|
||||
package main
|
||||
|
||||
import "github.com/usememos/memos/bin/server/cmd"
|
||||
import (
|
||||
"os"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/usememos/memos/server"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
"github.com/usememos/memos/store"
|
||||
|
||||
DB "github.com/usememos/memos/store/db"
|
||||
)
|
||||
|
||||
const (
|
||||
greetingBanner = `
|
||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
|
||||
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
|
||||
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
|
||||
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
|
||||
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
|
||||
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
||||
`
|
||||
)
|
||||
|
||||
func run(profile *profile.Profile) error {
|
||||
ctx := context.Background()
|
||||
|
||||
db := DB.NewDB(profile)
|
||||
if err := db.Open(ctx); err != nil {
|
||||
return fmt.Errorf("cannot open db: %w", err)
|
||||
}
|
||||
|
||||
s := server.NewServer(profile)
|
||||
|
||||
storeInstance := store.New(db.Db, profile)
|
||||
s.Store = storeInstance
|
||||
|
||||
println(greetingBanner)
|
||||
fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port)
|
||||
|
||||
return s.Run()
|
||||
}
|
||||
|
||||
func execute() error {
|
||||
profile, err := profile.GetProfile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
println("---")
|
||||
println("profile")
|
||||
println("mode:", profile.Mode)
|
||||
println("port:", profile.Port)
|
||||
println("dsn:", profile.DSN)
|
||||
println("version:", profile.Version)
|
||||
println("---")
|
||||
|
||||
if err := run(profile); err != nil {
|
||||
fmt.Printf("error: %+v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
if err := execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -9,7 +9,7 @@ type Code int
|
||||
|
||||
// Application error codes.
|
||||
const (
|
||||
// 0 ~ 99 general error
|
||||
// 0 ~ 99 general error.
|
||||
Ok Code = 0
|
||||
Internal Code = 1
|
||||
NotAuthorized Code = 2
|
||||
@@ -17,11 +17,6 @@ const (
|
||||
NotFound Code = 4
|
||||
Conflict Code = 5
|
||||
NotImplemented Code = 6
|
||||
|
||||
// 101 ~ 199 db error
|
||||
DbConnectionFailure Code = 101
|
||||
DbStatementSyntaxError Code = 102
|
||||
DbExecutionError Code = 103
|
||||
)
|
||||
|
||||
// Error represents an application-specific error. Application errors can be
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -16,6 +17,14 @@ func HasPrefixes(src string, prefixes ...string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateEmail validates the email.
|
||||
func ValidateEmail(email string) bool {
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func GenUUID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
email string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
email: "t@gmail.com",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
email: "@qq.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
email: "1@gmail",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
result := ValidateEmail(test.email)
|
||||
if result != test.want {
|
||||
t.Errorf("Validate Email %s: got result %v, want %v.", test.email, result, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
// Version is the service current released version.
|
||||
// Semantic versioning: https://semver.org/
|
||||
var Version = "0.2.1"
|
||||
var Version = "0.4.3"
|
||||
|
||||
// DevVersion is the service current development version.
|
||||
var DevVersion = "0.2.1"
|
||||
var DevVersion = "0.4.3"
|
||||
|
||||
func GetCurrentVersion(mode string) string {
|
||||
if mode == "dev" {
|
||||
@@ -27,6 +27,12 @@ func GetMinorVersion(version string) string {
|
||||
return versionList[0] + "." + versionList[1]
|
||||
}
|
||||
|
||||
func GetSchemaVersion(version string) string {
|
||||
minorVersion := GetMinorVersion(version)
|
||||
|
||||
return minorVersion + ".0"
|
||||
}
|
||||
|
||||
// convSemanticVersionToInt converts version string to int.
|
||||
func convSemanticVersionToInt(version string) int {
|
||||
versionList := strings.Split(version, ".")
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
version: "3.0"
|
||||
services:
|
||||
memos:
|
||||
image: neosmemo/memos:latest
|
||||
container_name: memos
|
||||
volumes:
|
||||
- ~/.memos/:/var/opt/memos
|
||||
ports:
|
||||
- 5230:5230
|
||||
@@ -0,0 +1,40 @@
|
||||
# Development
|
||||
|
||||
Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
|
||||
|
||||
1. It has no external dependency.
|
||||
2. It requires zero config.
|
||||
3. 1 command to start backend and 1 command to start frontend, both with live reload support.
|
||||
|
||||
## Tech Stack
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Go](https://golang.org/doc/install)
|
||||
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [yarn](https://yarnpkg.com/getting-started/install)
|
||||
|
||||
## Steps
|
||||
|
||||
1. pull source code
|
||||
|
||||
```bash
|
||||
git clone https://github.com/usememos/memos
|
||||
```
|
||||
|
||||
2. start backend using air(with live reload)
|
||||
|
||||
```bash
|
||||
air -c scripts/.air.toml
|
||||
```
|
||||
|
||||
3. start frontend dev server
|
||||
|
||||
```bash
|
||||
cd web && yarn && yarn dev
|
||||
```
|
||||
|
||||
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
|
||||
@@ -8,25 +8,31 @@ 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-colorable v0.1.12 // 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
|
||||
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
||||
golang.org/x/net v0.0.0-20220728030405-41545e8bf201 // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/labstack/echo/v4 v4.6.3
|
||||
github.com/labstack/echo/v4 v4.9.0
|
||||
github.com/labstack/gommon v0.3.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/VictoriaMetrics/fastcache v1.10.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/labstack/echo-contrib v0.12.0
|
||||
github.com/labstack/echo-contrib v0.13.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
)
|
||||
|
||||
@@ -37,27 +37,37 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY
|
||||
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/VictoriaMetrics/fastcache v1.10.0 h1:5hDJnLsKLpnUEToub7ETuRu8RCkb40woBZAUiKonXzY=
|
||||
github.com/VictoriaMetrics/fastcache v1.10.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8=
|
||||
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/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
|
||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||
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/casbin/casbin/v2 v2.51.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
|
||||
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/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/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/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/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=
|
||||
@@ -71,6 +81,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
|
||||
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/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
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=
|
||||
@@ -84,9 +95,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
|
||||
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-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
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=
|
||||
@@ -120,6 +133,7 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
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 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
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=
|
||||
@@ -132,6 +146,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
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/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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=
|
||||
@@ -171,6 +187,7 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX
|
||||
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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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=
|
||||
@@ -186,21 +203,16 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
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/echo-contrib v0.13.0 h1:bzSG0SpuZZd7BmJLvsWtPfU23W0Enh3K0tok3aENVKA=
|
||||
github.com/labstack/echo-contrib v0.13.0/go.mod h1:IF9+MJu22ADOZEHD+bAV67XMIO3vNXUy7Naz/ABPHEs=
|
||||
github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
|
||||
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
|
||||
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
|
||||
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-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
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=
|
||||
@@ -210,6 +222,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
||||
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/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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=
|
||||
@@ -223,7 +236,7 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
|
||||
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/openzipkin/zipkin-go v0.4.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=
|
||||
@@ -234,6 +247,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
||||
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_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
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=
|
||||
@@ -242,10 +257,13 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||
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/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
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/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
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=
|
||||
@@ -270,7 +288,6 @@ github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6
|
||||
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=
|
||||
@@ -295,8 +312,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
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/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
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=
|
||||
@@ -365,15 +383,20 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
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/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0=
|
||||
golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
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/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
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=
|
||||
@@ -384,12 +407,12 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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/sync v0.0.0-20220601150217-0de741cfad7f/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=
|
||||
@@ -398,7 +421,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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=
|
||||
@@ -408,7 +430,6 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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=
|
||||
@@ -426,16 +447,22 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-20210119212857-b64e53b001e4/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/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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=
|
||||
@@ -448,8 +475,9 @@ 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/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/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=
|
||||
@@ -568,6 +596,7 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
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/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
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=
|
||||
@@ -581,6 +610,7 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
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=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
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=
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
version: "3.2"
|
||||
services:
|
||||
portainer:
|
||||
image: neosmemo/memos:latest
|
||||
container_name: memos
|
||||
ports:
|
||||
- "5230:5230"
|
||||
volumes:
|
||||
- ~/.memos/:/var/opt/memos
|
||||
command: --mode=prod --port=5230
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
+1
-1
@@ -5,7 +5,7 @@ tmp_dir = ".air"
|
||||
bin = "./.air/memos"
|
||||
cmd = "go build -o ./.air/memos ./bin/server/main.go"
|
||||
delay = 1000
|
||||
exclude_dir = [".air", "web"]
|
||||
exclude_dir = [".air", "web", "build"]
|
||||
exclude_file = []
|
||||
exclude_regex = []
|
||||
exclude_unchanged = false
|
||||
|
||||
Regular → Executable
+7
-4
@@ -1,10 +1,13 @@
|
||||
# Usage: sh ./scripts/build.sh
|
||||
#!/bin/bash
|
||||
|
||||
# Usage: ./scripts/build.sh
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../"
|
||||
|
||||
echo "Start building..."
|
||||
echo "Start building backend..."
|
||||
|
||||
go build -o ./memos-build/memos ./bin/server/main.go
|
||||
go build -o ./build/memos ./bin/server/main.go
|
||||
|
||||
echo "Build finished"
|
||||
echo "Backend built!"
|
||||
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage: ./scripts/start.sh
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../"
|
||||
|
||||
air -c ./scripts/.air.toml
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
userIDContextKey = "user-id"
|
||||
)
|
||||
|
||||
func getUserIDContextKey() string {
|
||||
return userIDContextKey
|
||||
}
|
||||
|
||||
func setUserSession(ctx echo.Context, user *api.User) error {
|
||||
sess, _ := session.Get("session", ctx)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 1000 * 3600 * 24 * 30,
|
||||
HttpOnly: true,
|
||||
}
|
||||
sess.Values[userIDContextKey] = user.ID
|
||||
err := sess.Save(ctx.Request(), ctx.Response())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set session, err: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeUserSession(ctx echo.Context) error {
|
||||
sess, _ := session.Get("session", ctx)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 0,
|
||||
HttpOnly: true,
|
||||
}
|
||||
sess.Values[userIDContextKey] = nil
|
||||
err := sess.Save(ctx.Request(), ctx.Response())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set session, err: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
path := c.Path()
|
||||
// Skip auth.
|
||||
if common.HasPrefixes(path, "/api/auth") {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/user/:id") && c.Request().Method == http.MethodGet {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// If there is openId in query string and related user is found, then skip auth.
|
||||
openID := c.QueryParam("openId")
|
||||
if openID != "" {
|
||||
userFind := &api.UserFind{
|
||||
OpenID: &openID,
|
||||
}
|
||||
user, err := s.Store.FindUser(ctx, userFind)
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), user.ID)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
sess, _ := session.Get("session", c)
|
||||
userIDValue := sess.Values[userIDContextKey]
|
||||
if userIDValue != nil {
|
||||
userID, _ := strconv.Atoi(fmt.Sprintf("%v", userIDValue))
|
||||
userFind := &api.UserFind{
|
||||
ID: &userID,
|
||||
}
|
||||
user, err := s.Store.FindUser(ctx, userFind)
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
if user.RowStatus == api.Archived {
|
||||
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", user.Email))
|
||||
}
|
||||
c.Set(getUserIDContextKey(), userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if common.HasPrefixes(path, "/api/memo/all") && c.Request().Method == http.MethodGet {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
if common.HasPrefixes(path, "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
|
||||
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
userID := c.Get(getUserIDContextKey())
|
||||
if userID == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
+17
-18
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
g.POST("/auth/signin", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
signin := &api.Signin{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
|
||||
@@ -22,8 +23,8 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
userFind := &api.UserFind{
|
||||
Email: &signin.Email,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
if err != nil {
|
||||
user, err := s.Store.FindUser(ctx, userFind)
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signin.Email)).SetInternal(err)
|
||||
}
|
||||
if user == nil {
|
||||
@@ -60,13 +61,14 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.POST("/auth/signup", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
// Don't allow to signup by this api if site host existed.
|
||||
hostUserType := api.Host
|
||||
hostUserFind := api.UserFind{
|
||||
Role: &hostUserType,
|
||||
}
|
||||
hostUser, err := s.Store.FindUser(&hostUserFind)
|
||||
if err != nil {
|
||||
hostUser, err := s.Store.FindUser(ctx, &hostUserFind)
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
|
||||
}
|
||||
if hostUser != nil {
|
||||
@@ -78,13 +80,15 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
|
||||
}
|
||||
|
||||
// Validate signup form.
|
||||
// We can do stricter checks later.
|
||||
if len(signup.Email) < 6 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Email is too short, minimum length is 6.")
|
||||
userCreate := &api.UserCreate{
|
||||
Email: signup.Email,
|
||||
Role: api.Role(signup.Role),
|
||||
Name: signup.Name,
|
||||
Password: signup.Password,
|
||||
OpenID: common.GenUUID(),
|
||||
}
|
||||
if len(signup.Password) < 6 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Password is too short, minimum length is 6.")
|
||||
if err := userCreate.Validate(); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format.").SetInternal(err)
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
|
||||
@@ -92,14 +96,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
|
||||
}
|
||||
|
||||
userCreate := &api.UserCreate{
|
||||
Email: signup.Email,
|
||||
Role: api.Role(signup.Role),
|
||||
Name: signup.Name,
|
||||
PasswordHash: string(passwordHash),
|
||||
OpenID: common.GenUUID(),
|
||||
}
|
||||
user, err := s.Store.CreateUser(userCreate)
|
||||
userCreate.PasswordHash = string(passwordHash)
|
||||
|
||||
user, err := s.Store.CreateUser(ctx, userCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
userIDContextKey = "user-id"
|
||||
)
|
||||
|
||||
func getUserIDContextKey() string {
|
||||
return userIDContextKey
|
||||
}
|
||||
|
||||
func setUserSession(c echo.Context, user *api.User) error {
|
||||
sess, _ := session.Get("session", c)
|
||||
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, _ := session.Get("session", c)
|
||||
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 to store user.id.
|
||||
func BasicAuthMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// Skip auth for some paths.
|
||||
if common.HasPrefixes(c.Path(), "/api/auth", "/api/ping", "/api/status", "/api/user/:userId") {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// If there is openId in query string and related user is found, then skip auth.
|
||||
openID := c.QueryParam("openId")
|
||||
if openID != "" {
|
||||
userFind := &api.UserFind{
|
||||
OpenID: &openID,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), user.ID)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
if common.HasPrefixes(c.Path(), "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
|
||||
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
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.StatusInternalServerError, "Failed to malformatted user id in the session.").SetInternal(err)
|
||||
}
|
||||
|
||||
// Even if there is no error, we still need to make sure the user still exists.
|
||||
userFind := &api.UserFind{
|
||||
ID: &userID,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
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))
|
||||
} else if user.RowStatus == api.Archived {
|
||||
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", user.Email))
|
||||
}
|
||||
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), userID)
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
//go:embed dist
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
func getFileSystem() http.FileSystem {
|
||||
fs, err := fs.Sub(embeddedFiles, "dist")
|
||||
func getFileSystem(path string) http.FileSystem {
|
||||
fs, err := fs.Sub(embeddedFiles, path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -22,8 +22,22 @@ func getFileSystem() http.FileSystem {
|
||||
}
|
||||
|
||||
func embedFrontend(e *echo.Echo) {
|
||||
// Use echo static middleware to serve the built dist folder
|
||||
// refer: https://github.com/labstack/echo/blob/master/middleware/static.go
|
||||
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
HTML5: true,
|
||||
Filesystem: getFileSystem(),
|
||||
Filesystem: getFileSystem("dist"),
|
||||
}))
|
||||
|
||||
g := e.Group("assets")
|
||||
g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
g.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
HTML5: true,
|
||||
Filesystem: getFileSystem("dist/assets"),
|
||||
}))
|
||||
}
|
||||
|
||||
+123
-27
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
@@ -14,20 +15,42 @@ import (
|
||||
|
||||
func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
g.POST("/memo", func(c echo.Context) error {
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
memoCreate := &api.MemoCreate{
|
||||
CreatorID: userID,
|
||||
// Private is the default memo visibility.
|
||||
Visibility: api.Privite,
|
||||
}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoCreate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
|
||||
}
|
||||
|
||||
if memoCreate.Visibility == nil || *memoCreate.Visibility == "" {
|
||||
private := api.Privite
|
||||
memoCreate.Visibility = &private
|
||||
if memoCreate.Content == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Memo content shouldn't be empty")
|
||||
}
|
||||
|
||||
memo, err := s.Store.CreateMemo(memoCreate)
|
||||
userSettingMemoVisibilityKey := api.UserSettingMemoVisibilityKey
|
||||
userMemoVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{
|
||||
UserID: userID,
|
||||
Key: &userSettingMemoVisibilityKey,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
|
||||
}
|
||||
if userMemoVisibilitySetting != nil {
|
||||
memoVisibility := api.Privite
|
||||
err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
|
||||
}
|
||||
memoCreate.Visibility = memoVisibility
|
||||
}
|
||||
|
||||
memo, err := s.Store.CreateMemo(ctx, memoCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
|
||||
}
|
||||
@@ -40,6 +63,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.PATCH("/memo/:memoId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
@@ -52,7 +76,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
|
||||
}
|
||||
|
||||
memo, err := s.Store.PatchMemo(memoPatch)
|
||||
memo, err := s.Store.PatchMemo(ctx, memoPatch)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
|
||||
}
|
||||
@@ -65,24 +89,25 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/memo", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
memoFind := &api.MemoFind{}
|
||||
|
||||
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
memoFind.CreatorID = &userID
|
||||
} else {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
|
||||
}
|
||||
|
||||
memoFind.CreatorID = &userID
|
||||
}
|
||||
|
||||
// Only can get PUBLIC memos in visitor mode
|
||||
_, ok := c.Get(getUserIDContextKey()).(int)
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
publicVisibility := api.Public
|
||||
memoFind.Visibility = &publicVisibility
|
||||
if memoFind.CreatorID == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
|
||||
}
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public}
|
||||
} else {
|
||||
if memoFind.CreatorID == nil {
|
||||
memoFind.CreatorID = ¤tUserID
|
||||
} else {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
||||
}
|
||||
}
|
||||
|
||||
rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
|
||||
@@ -99,6 +124,14 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
contentSearch := "#" + tag + " "
|
||||
memoFind.ContentSearch = &contentSearch
|
||||
}
|
||||
visibilitListStr := c.QueryParam("visibility")
|
||||
if visibilitListStr != "" {
|
||||
visibilityList := []api.Visibility{}
|
||||
for _, visibility := range strings.Split(visibilitListStr, ",") {
|
||||
visibilityList = append(visibilityList, api.Visibility(visibility))
|
||||
}
|
||||
memoFind.VisibilityList = visibilityList
|
||||
}
|
||||
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
|
||||
memoFind.Limit = limit
|
||||
}
|
||||
@@ -106,7 +139,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
memoFind.Offset = offset
|
||||
}
|
||||
|
||||
list, err := s.Store.FindMemoList(memoFind)
|
||||
list, err := s.Store.FindMemoList(ctx, memoFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
|
||||
}
|
||||
@@ -118,13 +151,69 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
return nil
|
||||
})
|
||||
|
||||
g.GET("/memo/all", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
memoFind := &api.MemoFind{}
|
||||
|
||||
_, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public}
|
||||
} else {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
||||
}
|
||||
|
||||
pinnedStr := c.QueryParam("pinned")
|
||||
if pinnedStr != "" {
|
||||
pinned := pinnedStr == "true"
|
||||
memoFind.Pinned = &pinned
|
||||
}
|
||||
tag := c.QueryParam("tag")
|
||||
if tag != "" {
|
||||
contentSearch := "#" + tag + " "
|
||||
memoFind.ContentSearch = &contentSearch
|
||||
}
|
||||
visibilitListStr := c.QueryParam("visibility")
|
||||
if visibilitListStr != "" {
|
||||
visibilityList := []api.Visibility{}
|
||||
for _, visibility := range strings.Split(visibilitListStr, ",") {
|
||||
visibilityList = append(visibilityList, api.Visibility(visibility))
|
||||
}
|
||||
memoFind.VisibilityList = visibilityList
|
||||
}
|
||||
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
|
||||
memoFind.Limit = limit
|
||||
}
|
||||
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
|
||||
memoFind.Offset = offset
|
||||
}
|
||||
|
||||
// Only fetch normal status memos.
|
||||
normalStatus := api.Normal
|
||||
memoFind.RowStatus = &normalStatus
|
||||
|
||||
list, err := s.Store.FindMemoList(ctx, memoFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all 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 all memo list response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
}
|
||||
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
memoOrganizerUpsert := &api.MemoOrganizerUpsert{
|
||||
MemoID: memoID,
|
||||
UserID: userID,
|
||||
@@ -133,12 +222,12 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
|
||||
}
|
||||
|
||||
err = s.Store.UpsertMemoOrganizer(memoOrganizerUpsert)
|
||||
err = s.Store.UpsertMemoOrganizer(ctx, memoOrganizerUpsert)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
|
||||
}
|
||||
|
||||
memo, err := s.Store.FindMemo(&api.MemoFind{
|
||||
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -157,6 +246,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/memo/:memoId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
@@ -165,7 +255,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
memoFind := &api.MemoFind{
|
||||
ID: &memoID,
|
||||
}
|
||||
memo, err := s.Store.FindMemo(memoFind)
|
||||
memo, err := s.Store.FindMemo(ctx, memoFind)
|
||||
if err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
|
||||
@@ -182,6 +272,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.DELETE("/memo/:memoId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
@@ -190,9 +281,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
memoDelete := &api.MemoDelete{
|
||||
ID: memoID,
|
||||
}
|
||||
|
||||
err = s.Store.DeleteMemo(memoDelete)
|
||||
if err != nil {
|
||||
if err := s.Store.DeleteMemo(ctx, memoDelete); 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)
|
||||
}
|
||||
|
||||
@@ -200,14 +292,18 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/memo/amount", func(c echo.Context) error {
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
normalRowStatus := api.Normal
|
||||
memoFind := &api.MemoFind{
|
||||
CreatorID: &userID,
|
||||
RowStatus: &normalRowStatus,
|
||||
}
|
||||
|
||||
memoList, err := s.Store.FindMemoList(memoFind)
|
||||
memoList, err := s.Store.FindMemoList(ctx, memoFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func checkDSN(dataDir string) (string, error) {
|
||||
}
|
||||
|
||||
// GetDevProfile will return a profile for dev or prod.
|
||||
func GetProfile() *Profile {
|
||||
func GetProfile() (*Profile, error) {
|
||||
profile := Profile{}
|
||||
flag.StringVar(&profile.Mode, "mode", "dev", "mode of server")
|
||||
flag.IntVar(&profile.Port, "port", 8080, "port of server")
|
||||
@@ -63,12 +63,12 @@ func GetProfile() *Profile {
|
||||
dataDir, err := checkDSN(profile.Data)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to check dsn: %s, err: %+v\n", dataDir, err)
|
||||
os.Exit(1)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profile.Data = dataDir
|
||||
profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode)
|
||||
profile.Version = common.GetCurrentVersion(profile.Mode)
|
||||
|
||||
return &profile
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
+69
-12
@@ -3,18 +3,24 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"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)
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
err := c.Request().ParseMultipartForm(64 << 20)
|
||||
if err != nil {
|
||||
@@ -35,7 +41,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
fileBytes, err := ioutil.ReadAll(src)
|
||||
fileBytes, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
|
||||
}
|
||||
@@ -48,7 +54,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
CreatorID: userID,
|
||||
}
|
||||
|
||||
resource, err := s.Store.CreateResource(resourceCreate)
|
||||
resource, err := s.Store.CreateResource(ctx, resourceCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
|
||||
}
|
||||
@@ -61,11 +67,15 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/resource", func(c echo.Context) error {
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
resourceFind := &api.ResourceFind{
|
||||
CreatorID: &userID,
|
||||
}
|
||||
list, err := s.Store.FindResourceList(resourceFind)
|
||||
list, err := s.Store.FindResourceList(ctx, resourceFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
|
||||
}
|
||||
@@ -78,17 +88,21 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/resource/:resourceId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
}
|
||||
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
resourceFind := &api.ResourceFind{
|
||||
ID: &resourceID,
|
||||
CreatorID: &userID,
|
||||
}
|
||||
resource, err := s.Store.FindResource(resourceFind)
|
||||
resource, err := s.Store.FindResource(ctx, resourceFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
|
||||
}
|
||||
@@ -101,17 +115,21 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/resource/:resourceId/blob", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
}
|
||||
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
resourceFind := &api.ResourceFind{
|
||||
ID: &resourceID,
|
||||
CreatorID: &userID,
|
||||
}
|
||||
resource, err := s.Store.FindResource(resourceFind)
|
||||
resource, err := s.Store.FindResource(ctx, resourceFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
|
||||
}
|
||||
@@ -126,18 +144,57 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.DELETE("/resource/:resourceId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
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,
|
||||
ID: resourceID,
|
||||
CreatorID: userID,
|
||||
}
|
||||
if err := s.Store.DeleteResource(resourceDelete); err != nil {
|
||||
if err := s.Store.DeleteResource(ctx, resourceDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource ID not found: %d", resourceID))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
|
||||
g.GET("/r/:resourceId/:filename", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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 := html.UnescapeString(c.Param("filename"))
|
||||
resourceFind := &api.ResourceFind{
|
||||
ID: &resourceID,
|
||||
Filename: &filename,
|
||||
}
|
||||
resource, err := s.Store.FindResource(ctx, 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", resource.Type)
|
||||
c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
|
||||
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
+10
-4
@@ -29,9 +29,13 @@ func NewServer(profile *profile.Profile) *Server {
|
||||
e.HidePort = true
|
||||
|
||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: "${method} ${uri} ${status}\n",
|
||||
Format: `{"time":"${time_rfc3339}",` +
|
||||
`"method":"${method}","uri":"${uri}",` +
|
||||
`"status":${status},"error":"${error}"}` + "\n",
|
||||
}))
|
||||
|
||||
e.Use(middleware.CORS())
|
||||
|
||||
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
|
||||
Skipper: middleware.DefaultSkipper,
|
||||
ErrorMessage: "Request timeout",
|
||||
@@ -52,13 +56,15 @@ func NewServer(profile *profile.Profile) *Server {
|
||||
Profile: profile,
|
||||
}
|
||||
|
||||
// Webhooks api skips auth checker.
|
||||
webhookGroup := e.Group("/h")
|
||||
s.registerWebhookRoutes(webhookGroup)
|
||||
s.registerResourcePublicRoutes(webhookGroup)
|
||||
|
||||
publicGroup := e.Group("/o")
|
||||
s.registerResourcePublicRoutes(publicGroup)
|
||||
|
||||
apiGroup := e.Group("/api")
|
||||
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return BasicAuthMiddleware(s, next)
|
||||
return aclMiddleware(s, next)
|
||||
})
|
||||
s.registerSystemRoutes(apiGroup)
|
||||
s.registerAuthRoutes(apiGroup)
|
||||
|
||||
+18
-6
@@ -7,13 +7,18 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"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)
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
shortcutCreate := &api.ShortcutCreate{
|
||||
CreatorID: userID,
|
||||
}
|
||||
@@ -21,7 +26,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err)
|
||||
}
|
||||
|
||||
shortcut, err := s.Store.CreateShortcut(shortcutCreate)
|
||||
shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err)
|
||||
}
|
||||
@@ -34,6 +39,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.PATCH("/shortcut/:shortcutId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
@@ -46,7 +52,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err)
|
||||
}
|
||||
|
||||
shortcut, err := s.Store.PatchShortcut(shortcutPatch)
|
||||
shortcut, err := s.Store.PatchShortcut(ctx, shortcutPatch)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err)
|
||||
}
|
||||
@@ -59,6 +65,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/shortcut", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
shortcutFind := &api.ShortcutFind{}
|
||||
|
||||
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
@@ -72,7 +79,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
shortcutFind.CreatorID = &userID
|
||||
}
|
||||
|
||||
list, err := s.Store.FindShortcutList(shortcutFind)
|
||||
list, err := s.Store.FindShortcutList(ctx, shortcutFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut list").SetInternal(err)
|
||||
}
|
||||
@@ -85,6 +92,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/shortcut/:shortcutId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
@@ -93,7 +101,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
shortcutFind := &api.ShortcutFind{
|
||||
ID: &shortcutID,
|
||||
}
|
||||
shortcut, err := s.Store.FindShortcut(shortcutFind)
|
||||
shortcut, err := s.Store.FindShortcut(ctx, shortcutFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch shortcut by ID %d", *shortcutFind.ID)).SetInternal(err)
|
||||
}
|
||||
@@ -106,6 +114,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.DELETE("/shortcut/:shortcutId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
@@ -114,7 +123,10 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
shortcutDelete := &api.ShortcutDelete{
|
||||
ID: shortcutID,
|
||||
}
|
||||
if err := s.Store.DeleteShortcut(shortcutDelete); err != nil {
|
||||
if err := s.Store.DeleteShortcut(ctx, shortcutDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Shortcut ID not found: %d", shortcutID))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete shortcut").SetInternal(err)
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
@@ -21,12 +22,13 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/status", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
hostUserType := api.Host
|
||||
hostUserFind := api.UserFind{
|
||||
Role: &hostUserType,
|
||||
}
|
||||
hostUser, err := s.Store.FindUser(&hostUserFind)
|
||||
if err != nil {
|
||||
hostUser, err := s.Store.FindUser(ctx, &hostUserFind)
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
|
||||
}
|
||||
|
||||
@@ -36,7 +38,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
systemStatus := api.SystemStatus{
|
||||
Host: hostUser,
|
||||
Host: hostUser,
|
||||
Profile: s.Profile,
|
||||
}
|
||||
|
||||
|
||||
+17
-18
@@ -12,8 +12,11 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var tagRegexp = regexp.MustCompile(`#([^\s#]+?) `)
|
||||
|
||||
func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
g.GET("/tag", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
contentSearch := "#"
|
||||
normalRowStatus := api.Normal
|
||||
memoFind := api.MemoFind{
|
||||
@@ -22,37 +25,33 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
memoFind.CreatorID = &userID
|
||||
} else {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
|
||||
}
|
||||
|
||||
memoFind.CreatorID = &userID
|
||||
}
|
||||
|
||||
// Only can get PUBLIC memos in visitor mode
|
||||
_, ok := c.Get(getUserIDContextKey()).(int)
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
publicVisibility := api.Public
|
||||
memoFind.Visibility = &publicVisibility
|
||||
if memoFind.CreatorID == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
|
||||
}
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public}
|
||||
} else {
|
||||
if memoFind.CreatorID == nil {
|
||||
memoFind.CreatorID = ¤tUserID
|
||||
} else {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
||||
}
|
||||
}
|
||||
|
||||
memoList, err := s.Store.FindMemoList(&memoFind)
|
||||
memoList, err := s.Store.FindMemoList(ctx, &memoFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
|
||||
}
|
||||
|
||||
tagMapSet := make(map[string]bool)
|
||||
|
||||
r, err := regexp.Compile("#(.+?) ")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile regexp").SetInternal(err)
|
||||
}
|
||||
for _, memo := range memoList {
|
||||
for _, rawTag := range r.FindAllString(memo.Content, -1) {
|
||||
tag := r.ReplaceAllString(rawTag, "$1")
|
||||
for _, rawTag := range tagRegexp.FindAllString(memo.Content, -1) {
|
||||
tag := tagRegexp.ReplaceAllString(rawTag, "$1")
|
||||
tagMapSet[tag] = true
|
||||
}
|
||||
}
|
||||
|
||||
+130
-54
@@ -15,18 +15,39 @@ import (
|
||||
|
||||
func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
g.POST("/user", func(c echo.Context) error {
|
||||
userCreate := &api.UserCreate{}
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||
}
|
||||
currentUser, err := s.Store.FindUser(ctx, &api.UserFind{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
|
||||
}
|
||||
if currentUser.Role != api.Host {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Only Host user can create member.")
|
||||
}
|
||||
|
||||
userCreate := &api.UserCreate{
|
||||
OpenID: common.GenUUID(),
|
||||
}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
|
||||
}
|
||||
|
||||
if err := userCreate.Validate(); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format.").SetInternal(err)
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
|
||||
}
|
||||
|
||||
userCreate.PasswordHash = string(passwordHash)
|
||||
user, err := s.Store.CreateUser(userCreate)
|
||||
user, err := s.Store.CreateUser(ctx, userCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
|
||||
}
|
||||
@@ -39,11 +60,17 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/user", func(c echo.Context) error {
|
||||
userList, err := s.Store.FindUserList(&api.UserFind{})
|
||||
ctx := c.Request().Context()
|
||||
userList, err := s.Store.FindUserList(ctx, &api.UserFind{})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
|
||||
}
|
||||
|
||||
for _, user := range userList {
|
||||
// data desensitize
|
||||
user.OpenID = ""
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userList)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user list response").SetInternal(err)
|
||||
@@ -51,13 +78,73 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return nil
|
||||
})
|
||||
|
||||
// GET /api/user/me is used to check if the user is logged in.
|
||||
g.GET("/user/me", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||
}
|
||||
|
||||
userFind := &api.UserFind{
|
||||
ID: &userID,
|
||||
}
|
||||
user, err := s.Store.FindUser(ctx, userFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||
}
|
||||
|
||||
userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
|
||||
}
|
||||
user.UserSettingList = userSettingList
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.POST("/user/setting", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||
}
|
||||
|
||||
userSettingUpsert := &api.UserSettingUpsert{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err)
|
||||
}
|
||||
if err := userSettingUpsert.Validate(); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user setting format").SetInternal(err)
|
||||
}
|
||||
|
||||
userSettingUpsert.UserID = userID
|
||||
userSetting, err := s.Store.UpsertUserSetting(ctx, userSettingUpsert)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userSetting)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user setting response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.GET("/user/:id", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err)
|
||||
}
|
||||
|
||||
user, err := s.Store.FindUser(&api.UserFind{
|
||||
user, err := s.Store.FindUser(ctx, &api.UserFind{
|
||||
ID: &id,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -76,31 +163,28 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return nil
|
||||
})
|
||||
|
||||
// GET /api/user/me is used to check if the user is logged in.
|
||||
g.GET("/user/me", func(c echo.Context) error {
|
||||
userSessionID := c.Get(getUserIDContextKey())
|
||||
if userSessionID == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||
}
|
||||
|
||||
userID := userSessionID.(int)
|
||||
userFind := &api.UserFind{
|
||||
ID: &userID,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
g.PATCH("/user/:id", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||
}
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
currentUser, err := s.Store.FindUser(ctx, &api.UserFind{
|
||||
ID: ¤tUserID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||
}
|
||||
if currentUser == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
|
||||
} else if currentUser.Role != api.Host && currentUserID != userID {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session 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.PATCH("/user/me", func(c echo.Context) error {
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
userPatch := &api.UserPatch{
|
||||
ID: userID,
|
||||
}
|
||||
@@ -108,6 +192,10 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
|
||||
}
|
||||
|
||||
if userPatch.Email != nil && !common.ValidateEmail(*userPatch.Email) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid email format")
|
||||
}
|
||||
|
||||
if userPatch.Password != nil && *userPatch.Password != "" {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -123,7 +211,7 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
userPatch.OpenID = &openID
|
||||
}
|
||||
|
||||
user, err := s.Store.PatchUser(userPatch)
|
||||
user, err := s.Store.PatchUser(ctx, userPatch)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
|
||||
}
|
||||
@@ -135,9 +223,13 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return nil
|
||||
})
|
||||
|
||||
g.PATCH("/user/:userId", func(c echo.Context) error {
|
||||
currentUserID := c.Get(getUserIDContextKey()).(int)
|
||||
currentUser, err := s.Store.FindUser(&api.UserFind{
|
||||
g.DELETE("/user/:id", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
currentUser, err := s.Store.FindUser(ctx, &api.UserFind{
|
||||
ID: ¤tUserID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -149,37 +241,21 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err)
|
||||
}
|
||||
|
||||
userID, err := strconv.Atoi(c.Param("userId"))
|
||||
userID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("userId"))).SetInternal(err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||
}
|
||||
|
||||
userPatch := &api.UserPatch{
|
||||
userDelete := &api.UserDelete{
|
||||
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.Password != nil && *userPatch.Password != "" {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
|
||||
if err := s.Store.DeleteUser(ctx, userDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("User ID not found: %d", userID))
|
||||
}
|
||||
|
||||
passwordHashStr := string(passwordHash)
|
||||
userPatch.PasswordHash = &passwordHashStr
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err)
|
||||
}
|
||||
|
||||
user, err := s.Store.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
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
|
||||
"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.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.Store.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", resource.Type)
|
||||
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
"github.com/usememos/memos/api"
|
||||
)
|
||||
|
||||
var (
|
||||
// 64 MiB.
|
||||
cacheSize = 1024 * 1024 * 64
|
||||
_ api.CacheService = (*CacheService)(nil)
|
||||
)
|
||||
|
||||
// CacheService implements a cache.
|
||||
type CacheService struct {
|
||||
cache *fastcache.Cache
|
||||
}
|
||||
|
||||
// NewCacheService creates a cache service.
|
||||
func NewCacheService() *CacheService {
|
||||
return &CacheService{
|
||||
cache: fastcache.New(cacheSize),
|
||||
}
|
||||
}
|
||||
|
||||
// FindCache finds the value in cache.
|
||||
func (s *CacheService) FindCache(namespace api.CacheNamespace, id int, entry interface{}) (bool, error) {
|
||||
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
binary.LittleEndian.PutUint64(buf1, uint64(id))
|
||||
|
||||
buf2, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
|
||||
if has {
|
||||
dec := gob.NewDecoder(bytes.NewReader(buf2))
|
||||
if err := dec.Decode(entry); err != nil {
|
||||
return false, fmt.Errorf("failed to decode entry for cache namespace: %s, error: %w", namespace, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// UpsertCache upserts the value to cache.
|
||||
func (s *CacheService) UpsertCache(namespace api.CacheNamespace, id int, entry interface{}) error {
|
||||
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
binary.LittleEndian.PutUint64(buf1, uint64(id))
|
||||
|
||||
var buf2 bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf2)
|
||||
if err := enc.Encode(entry); err != nil {
|
||||
return fmt.Errorf("failed to encode entry for cache namespace: %s, error: %w", namespace, err)
|
||||
}
|
||||
s.cache.Set(append([]byte(namespace), buf1...), buf2.Bytes())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCache deletes the cache.
|
||||
func (s *CacheService) DeleteCache(namespace api.CacheNamespace, id int) {
|
||||
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
binary.LittleEndian.PutUint64(buf1, uint64(id))
|
||||
|
||||
_, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
|
||||
if has {
|
||||
s.cache.Del(append([]byte(namespace), buf1...))
|
||||
}
|
||||
}
|
||||
+41
-31
@@ -1,12 +1,12 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -14,8 +14,6 @@ import (
|
||||
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//go:embed migration
|
||||
@@ -38,7 +36,7 @@ func NewDB(profile *profile.Profile) *DB {
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *DB) Open() (err error) {
|
||||
func (db *DB) Open(ctx context.Context) (err error) {
|
||||
// Ensure a DSN is set before attempting to open the database.
|
||||
if db.profile.DSN == "" {
|
||||
return fmt.Errorf("dsn required")
|
||||
@@ -53,32 +51,32 @@ func (db *DB) Open() (err error) {
|
||||
db.Db = sqlDB
|
||||
// If mode is dev, we should migrate and seed the database.
|
||||
if db.profile.Mode == "dev" {
|
||||
if err := db.applyLatestSchema(); err != nil {
|
||||
if err := db.applyLatestSchema(ctx); err != nil {
|
||||
return fmt.Errorf("failed to apply latest schema: %w", err)
|
||||
}
|
||||
if err := db.seed(); err != nil {
|
||||
if err := db.seed(ctx); err != nil {
|
||||
return fmt.Errorf("failed to seed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// If db file not exists, we should migrate the database.
|
||||
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
|
||||
err := db.applyLatestSchema()
|
||||
err := db.applyLatestSchema(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply latest schema: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := db.createMigrationHistoryTable()
|
||||
err := db.createMigrationHistoryTable(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migration_history table: %w", err)
|
||||
}
|
||||
|
||||
currentVersion := common.GetCurrentVersion(db.profile.Mode)
|
||||
migrationHistory, err := findMigrationHistory(db.Db, &MigrationHistoryFind{})
|
||||
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if migrationHistory == nil {
|
||||
migrationHistory, err = upsertMigrationHistory(db.Db, &MigrationHistoryUpsert{
|
||||
migrationHistory, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
|
||||
Version: currentVersion,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -86,16 +84,16 @@ func (db *DB) Open() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if common.IsVersionGreaterThan(currentVersion, migrationHistory.Version) {
|
||||
if common.IsVersionGreaterThan(common.GetSchemaVersion(currentVersion), migrationHistory.Version) {
|
||||
minorVersionList := getMinorVersionList()
|
||||
|
||||
// backup the raw database file before migration
|
||||
rawBytes, err := ioutil.ReadFile(db.profile.DSN)
|
||||
rawBytes, err := os.ReadFile(db.profile.DSN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read raw database file, err: %w", err)
|
||||
}
|
||||
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
|
||||
if err := ioutil.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
|
||||
if err := os.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write raw database file, err: %w", err)
|
||||
}
|
||||
|
||||
@@ -105,7 +103,7 @@ func (db *DB) Open() (err error) {
|
||||
normalizedVersion := minorVersion + ".0"
|
||||
if common.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && common.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
|
||||
println("applying migration for", normalizedVersion)
|
||||
if err := db.applyMigrationForMinorVersion(minorVersion); err != nil {
|
||||
if err := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
|
||||
return fmt.Errorf("failed to apply minor version migration: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -127,21 +125,21 @@ const (
|
||||
latestSchemaFileName = "LATEST__SCHEMA.sql"
|
||||
)
|
||||
|
||||
func (db *DB) applyLatestSchema() error {
|
||||
latestSchemaPath := fmt.Sprintf("%s/%s", "migration", latestSchemaFileName)
|
||||
func (db *DB) applyLatestSchema(ctx context.Context) error {
|
||||
latestSchemaPath := fmt.Sprintf("%s/%s/%s", "migration", db.profile.Mode, latestSchemaFileName)
|
||||
buf, err := migrationFS.ReadFile(latestSchemaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read latest schema %q, error %w", latestSchemaPath, err)
|
||||
}
|
||||
stmt := string(buf)
|
||||
if err := db.execute(stmt); err != nil {
|
||||
if err := db.execute(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) applyMigrationForMinorVersion(minorVersion string) error {
|
||||
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration", minorVersion))
|
||||
func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
|
||||
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration/prod", minorVersion))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -157,22 +155,28 @@ func (db *DB) applyMigrationForMinorVersion(minorVersion string) error {
|
||||
}
|
||||
stmt := string(buf)
|
||||
migrationStmt += stmt
|
||||
if err := db.execute(stmt); err != nil {
|
||||
if err := db.execute(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// upsert the newest version to migration_history
|
||||
if _, err = upsertMigrationHistory(db.Db, &MigrationHistoryUpsert{
|
||||
if _, err = upsertMigrationHistory(ctx, tx, &MigrationHistoryUpsert{
|
||||
Version: minorVersion + ".0",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (db *DB) seed() error {
|
||||
func (db *DB) seed(ctx context.Context) error {
|
||||
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -187,22 +191,22 @@ func (db *DB) seed() error {
|
||||
return fmt.Errorf("failed to read seed file, filename=%s err=%w", filename, err)
|
||||
}
|
||||
stmt := string(buf)
|
||||
if err := db.execute(stmt); err != nil {
|
||||
if err := db.execute(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("seed error: statement:%s err=%w", stmt, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// excecute runs a single SQL statement within a transaction.
|
||||
func (db *DB) execute(stmt string) error {
|
||||
// execute runs a single SQL statement within a transaction.
|
||||
func (db *DB) execute(ctx context.Context, stmt string) error {
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -210,7 +214,7 @@ func (db *DB) execute(stmt string) error {
|
||||
}
|
||||
|
||||
// minorDirRegexp is a regular expression for minor version directory.
|
||||
var minorDirRegexp = regexp.MustCompile(`^migration/[0-9]+\.[0-9]+$`)
|
||||
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
|
||||
|
||||
func getMinorVersionList() []string {
|
||||
minorVersionList := []string{}
|
||||
@@ -234,8 +238,14 @@ func getMinorVersionList() []string {
|
||||
}
|
||||
|
||||
// createMigrationHistoryTable creates the migration_history table if it doesn't exist.
|
||||
func (db *DB) createMigrationHistoryTable() error {
|
||||
if err := createTable(db.Db, `
|
||||
func (db *DB) createMigrationHistoryTable(ctx context.Context) error {
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := createTable(ctx, tx, `
|
||||
CREATE TABLE IF NOT EXISTS migration_history (
|
||||
version TEXT NOT NULL PRIMARY KEY,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
@@ -244,5 +254,5 @@ func (db *DB) createMigrationHistoryTable() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ DROP TABLE IF EXISTS `memo_organizer`;
|
||||
DROP TABLE IF EXISTS `memo`;
|
||||
DROP TABLE IF EXISTS `shortcut`;
|
||||
DROP TABLE IF EXISTS `resource`;
|
||||
DROP TABLE IF EXISTS `user_setting`;
|
||||
DROP TABLE IF EXISTS `user`;
|
||||
|
||||
-- user
|
||||
@@ -44,7 +45,7 @@ CREATE TABLE memo (
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -139,3 +140,13 @@ SET
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- user_setting
|
||||
CREATE TABLE user_setting (
|
||||
user_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
|
||||
+1
-2
@@ -4,8 +4,7 @@ PRAGMA foreign_keys = off;
|
||||
|
||||
DROP TABLE IF EXISTS _user_old;
|
||||
|
||||
ALTER TABLE
|
||||
user RENAME TO _user_old;
|
||||
ALTER TABLE user RENAME TO _user_old;
|
||||
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -0,0 +1,37 @@
|
||||
-- change memo visibility field from "PRIVATE"/"PUBLIC" to "PRIVATE"/"PROTECTED"/"PUBLIC".
|
||||
|
||||
PRAGMA foreign_keys = off;
|
||||
|
||||
DROP TABLE IF EXISTS _memo_old;
|
||||
|
||||
ALTER TABLE memo RENAME TO _memo_old;
|
||||
|
||||
CREATE TABLE memo (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO memo (
|
||||
id, creator_id, created_ts, updated_ts,
|
||||
row_status, content, visibility
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
creator_id,
|
||||
created_ts,
|
||||
updated_ts,
|
||||
row_status,
|
||||
content,
|
||||
visibility
|
||||
FROM
|
||||
_memo_old;
|
||||
|
||||
DROP TABLE IF EXISTS _memo_old;
|
||||
|
||||
PRAGMA foreign_keys = on;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- user_setting
|
||||
CREATE TABLE user_setting (
|
||||
user_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
|
||||
@@ -0,0 +1,152 @@
|
||||
-- drop all tables
|
||||
DROP TABLE IF EXISTS `memo_organizer`;
|
||||
DROP TABLE IF EXISTS `memo`;
|
||||
DROP TABLE IF EXISTS `shortcut`;
|
||||
DROP TABLE IF EXISTS `resource`;
|
||||
DROP TABLE IF EXISTS `user_setting`;
|
||||
DROP TABLE IF EXISTS `user`;
|
||||
|
||||
-- user
|
||||
CREATE TABLE user (
|
||||
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', 'ARCHIVED'.
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
open_id TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
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,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('memo', 1000);
|
||||
|
||||
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;
|
||||
|
||||
-- memo_organizer
|
||||
CREATE TABLE memo_organizer (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
memo_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
|
||||
FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
UNIQUE(memo_id, user_id)
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('memo_organizer', 1000);
|
||||
|
||||
-- shortcut
|
||||
CREATE TABLE shortcut (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
payload TEXT NOT NULL DEFAULT '{}',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('shortcut', 10000);
|
||||
|
||||
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,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
filename TEXT NOT NULL DEFAULT '',
|
||||
blob BLOB NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT '',
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('resource', 10000);
|
||||
|
||||
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;
|
||||
|
||||
-- user_setting
|
||||
CREATE TABLE user_setting (
|
||||
user_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
|
||||
@@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
)
|
||||
@@ -18,23 +19,62 @@ type MigrationHistoryFind struct {
|
||||
Version *string
|
||||
}
|
||||
|
||||
func findMigrationHistoryList(db *sql.DB, find *MigrationHistoryFind) ([]*MigrationHistory, error) {
|
||||
func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFind) (*MigrationHistory, error) {
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findMigrationHistoryList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
migrationHistory := list[0]
|
||||
return migrationHistory, nil
|
||||
}
|
||||
|
||||
func (db *DB) UpsertMigrationHistory(ctx context.Context, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
migrationHistory, err := upsertMigrationHistory(ctx, tx, upsert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return migrationHistory, nil
|
||||
}
|
||||
|
||||
func findMigrationHistoryList(ctx context.Context, tx *sql.Tx, find *MigrationHistoryFind) ([]*MigrationHistory, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.Version; v != nil {
|
||||
where, args = append(where, "version = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
query := `
|
||||
SELECT
|
||||
version,
|
||||
created_ts
|
||||
FROM
|
||||
migration_history
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY created_ts DESC`,
|
||||
args...,
|
||||
)
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY version DESC
|
||||
`
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -53,24 +93,15 @@ func findMigrationHistoryList(db *sql.DB, find *MigrationHistoryFind) ([]*Migrat
|
||||
migrationHistoryList = append(migrationHistoryList, &migrationHistory)
|
||||
}
|
||||
|
||||
return migrationHistoryList, nil
|
||||
}
|
||||
|
||||
func findMigrationHistory(db *sql.DB, find *MigrationHistoryFind) (*MigrationHistory, error) {
|
||||
list, err := findMigrationHistoryList(db, find)
|
||||
if err != nil {
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
} else {
|
||||
return list[0], nil
|
||||
}
|
||||
return migrationHistoryList, nil
|
||||
}
|
||||
|
||||
func upsertMigrationHistory(db *sql.DB, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
|
||||
row, err := db.Query(`
|
||||
func upsertMigrationHistory(ctx context.Context, tx *sql.Tx, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
|
||||
query := `
|
||||
INSERT INTO migration_history (
|
||||
version
|
||||
)
|
||||
@@ -79,9 +110,8 @@ func upsertMigrationHistory(db *sql.DB, upsert *MigrationHistoryUpsert) (*Migrat
|
||||
SET
|
||||
version=EXCLUDED.version
|
||||
RETURNING version, created_ts
|
||||
`,
|
||||
upsert.Version,
|
||||
)
|
||||
`
|
||||
row, err := tx.QueryContext(ctx, query, upsert.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -96,5 +126,9 @@ func upsertMigrationHistory(db *sql.DB, upsert *MigrationHistoryUpsert) (*Migrat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &migrationHistory, nil
|
||||
}
|
||||
|
||||
@@ -37,3 +37,25 @@ VALUES
|
||||
-- raw password: secret
|
||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
user (
|
||||
`id`,
|
||||
`row_status`,
|
||||
`email`,
|
||||
`role`,
|
||||
`name`,
|
||||
`open_id`,
|
||||
`password_hash`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
103,
|
||||
'ARCHIVED',
|
||||
'bob@usememos.com',
|
||||
'USER',
|
||||
'Bob',
|
||||
'bob_open_id',
|
||||
-- raw password: secret
|
||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
||||
);
|
||||
@@ -16,7 +16,8 @@ INSERT INTO
|
||||
memo (
|
||||
`id`,
|
||||
`content`,
|
||||
`creator_id`
|
||||
`creator_id`,
|
||||
`visibility`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -26,7 +27,8 @@ VALUES
|
||||
- [x] Clean the room;
|
||||
- [x] Read *📖 The Little Prince*;
|
||||
(👆 click to toggle status)',
|
||||
101
|
||||
101,
|
||||
'PROTECTED'
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
@@ -48,7 +50,8 @@ INSERT INTO
|
||||
memo (
|
||||
`id`,
|
||||
`content`,
|
||||
`creator_id`
|
||||
`creator_id`,
|
||||
`visibility`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -59,7 +62,8 @@ VALUES
|
||||
- [ ] Watch *👦 The Boys*;
|
||||
(👆 click to toggle status)
|
||||
',
|
||||
102
|
||||
102,
|
||||
'PROTECTED'
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
INSERT INTO
|
||||
shortcut (
|
||||
`title`,
|
||||
`creator_id`
|
||||
`creator_id`,
|
||||
`payload`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'All my memos',
|
||||
101
|
||||
'inbox',
|
||||
101,
|
||||
'[{"type":"TYPE","value":{"operator":"IS","value":"NOT_TAGGED"},"relation":"AND"}]'
|
||||
);
|
||||
|
||||
+9
-10
@@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
)
|
||||
@@ -11,20 +12,20 @@ type Table struct {
|
||||
}
|
||||
|
||||
//lint:ignore U1000 Ignore unused function temporarily for debugging
|
||||
func findTable(db *sql.DB, tableName string) (*Table, error) {
|
||||
//nolint:all
|
||||
func findTable(ctx context.Context, tx *sql.Tx, tableName string) (*Table, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
where, args = append(where, "type = ?"), append(args, "table")
|
||||
where, args = append(where, "name = ?"), append(args, tableName)
|
||||
|
||||
rows, err := db.Query(`
|
||||
query := `
|
||||
SELECT
|
||||
tbl_name,
|
||||
sql
|
||||
FROM sqlite_schema
|
||||
WHERE `+strings.Join(where, " AND "),
|
||||
args...,
|
||||
)
|
||||
WHERE ` + strings.Join(where, " AND ")
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -54,13 +55,11 @@ func findTable(db *sql.DB, tableName string) (*Table, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func createTable(db *sql.DB, sql string) error {
|
||||
result, err := db.Exec(sql)
|
||||
func createTable(ctx context.Context, tx *sql.Tx, stmt string) error {
|
||||
_, err := tx.ExecContext(ctx, stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = result.RowsAffected()
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
+142
-81
@@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -43,13 +44,20 @@ func (raw *memoRaw) toMemo() *api.Memo {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateMemo(create *api.MemoCreate) (*api.Memo, error) {
|
||||
memoRaw, err := createMemoRaw(s.db, create)
|
||||
if err != nil {
|
||||
func (s *Store) composeMemo(ctx context.Context, raw *memoRaw) (*api.Memo, error) {
|
||||
memo := raw.toMemo()
|
||||
|
||||
memoOrganizer, err := s.FindMemoOrganizer(ctx, &api.MemoOrganizerFind{
|
||||
MemoID: memo.ID,
|
||||
UserID: memo.CreatorID,
|
||||
})
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return nil, err
|
||||
} else if memoOrganizer != nil {
|
||||
memo.Pinned = memoOrganizer.Pinned
|
||||
}
|
||||
|
||||
memo, err := s.composeMemo(memoRaw)
|
||||
err = s.ComposeMemoCreator(ctx, memo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -57,29 +65,77 @@ func (s *Store) CreateMemo(create *api.MemoCreate) (*api.Memo, error) {
|
||||
return memo, nil
|
||||
}
|
||||
|
||||
func (s *Store) PatchMemo(patch *api.MemoPatch) (*api.Memo, error) {
|
||||
memoRaw, err := patchMemoRaw(s.db, patch)
|
||||
func (s *Store) CreateMemo(ctx context.Context, create *api.MemoCreate) (*api.Memo, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
memoRaw, err := createMemoRaw(ctx, tx, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
memo, err := s.composeMemo(memoRaw)
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
memo, err := s.composeMemo(ctx, memoRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.cache.UpsertCache(api.MemoCache, memo.ID, memo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memo, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindMemoList(find *api.MemoFind) ([]*api.Memo, error) {
|
||||
memoRawList, err := findMemoRawList(s.db, find)
|
||||
func (s *Store) PatchMemo(ctx context.Context, patch *api.MemoPatch) (*api.Memo, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
memoRaw, err := patchMemoRaw(ctx, tx, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
memo, err := s.composeMemo(ctx, memoRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.cache.UpsertCache(api.MemoCache, memo.ID, memo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memo, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindMemoList(ctx context.Context, find *api.MemoFind) ([]*api.Memo, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
memoRawList, err := findMemoRawList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*api.Memo{}
|
||||
for _, raw := range memoRawList {
|
||||
memo, err := s.composeMemo(raw)
|
||||
memo, err := s.composeMemo(ctx, raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -90,8 +146,25 @@ func (s *Store) FindMemoList(find *api.MemoFind) ([]*api.Memo, error) {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindMemo(find *api.MemoFind) (*api.Memo, error) {
|
||||
list, err := findMemoRawList(s.db, find)
|
||||
func (s *Store) FindMemo(ctx context.Context, find *api.MemoFind) (*api.Memo, error) {
|
||||
if find.ID != nil {
|
||||
memo := &api.Memo{}
|
||||
has, err := s.cache.FindCache(api.MemoCache, *find.ID, memo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return memo, nil
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findMemoRawList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -100,52 +173,52 @@ func (s *Store) FindMemo(find *api.MemoFind) (*api.Memo, error) {
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
|
||||
}
|
||||
|
||||
memo, err := s.composeMemo(list[0])
|
||||
memo, err := s.composeMemo(ctx, list[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.cache.UpsertCache(api.MemoCache, memo.ID, memo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memo, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteMemo(delete *api.MemoDelete) error {
|
||||
err := deleteMemo(s.db, delete)
|
||||
func (s *Store) DeleteMemo(ctx context.Context, delete *api.MemoDelete) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := deleteMemo(ctx, tx, delete); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.MemoCache, delete.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createMemoRaw(db *sql.DB, create *api.MemoCreate) (*memoRaw, error) {
|
||||
set := []string{"creator_id", "content"}
|
||||
placeholder := []string{"?", "?"}
|
||||
args := []interface{}{create.CreatorID, create.Content}
|
||||
|
||||
if v := create.Visibility; v != nil {
|
||||
set, placeholder, args = append(set, "visibility"), append(placeholder, "?"), append(args, *v)
|
||||
}
|
||||
if v := create.CreatedTs; v != nil {
|
||||
set, placeholder, args = append(set, "created_ts"), append(placeholder, "?"), append(args, *v)
|
||||
}
|
||||
func createMemoRaw(ctx context.Context, tx *sql.Tx, create *api.MemoCreate) (*memoRaw, error) {
|
||||
set := []string{"creator_id", "content", "visibility"}
|
||||
args := []interface{}{create.CreatorID, create.Content, create.Visibility}
|
||||
placeholder := []string{"?", "?", "?"}
|
||||
|
||||
query := `
|
||||
INSERT INTO memo (
|
||||
` + strings.Join(set, ", ") + `
|
||||
)
|
||||
VALUES (` + strings.Join(placeholder, ",") + `)
|
||||
RETURNING id, creator_id, created_ts, updated_ts, row_status, content, visibility`
|
||||
row, err := db.Query(query,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
row.Next()
|
||||
INSERT INTO memo (
|
||||
` + strings.Join(set, ", ") + `
|
||||
)
|
||||
VALUES (` + strings.Join(placeholder, ",") + `)
|
||||
RETURNING id, creator_id, created_ts, updated_ts, row_status, content, visibility
|
||||
`
|
||||
var memoRaw memoRaw
|
||||
if err := row.Scan(
|
||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||
&memoRaw.ID,
|
||||
&memoRaw.CreatorID,
|
||||
&memoRaw.CreatedTs,
|
||||
@@ -160,36 +233,32 @@ func createMemoRaw(db *sql.DB, create *api.MemoCreate) (*memoRaw, error) {
|
||||
return &memoRaw, nil
|
||||
}
|
||||
|
||||
func patchMemoRaw(db *sql.DB, patch *api.MemoPatch) (*memoRaw, error) {
|
||||
func patchMemoRaw(ctx context.Context, tx *sql.Tx, patch *api.MemoPatch) (*memoRaw, error) {
|
||||
set, args := []string{}, []interface{}{}
|
||||
|
||||
if v := patch.Content; v != nil {
|
||||
set, args = append(set, "content = ?"), append(args, *v)
|
||||
if v := patch.CreatedTs; v != nil {
|
||||
set, args = append(set, "created_ts = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.RowStatus; v != nil {
|
||||
set, args = append(set, "row_status = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.Content; v != nil {
|
||||
set, args = append(set, "content = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.Visibility; v != nil {
|
||||
set, args = append(set, "visibility = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
args = append(args, patch.ID)
|
||||
|
||||
row, err := db.Query(`
|
||||
query := `
|
||||
UPDATE memo
|
||||
SET `+strings.Join(set, ", ")+`
|
||||
SET ` + strings.Join(set, ", ") + `
|
||||
WHERE id = ?
|
||||
RETURNING id, creator_id, created_ts, updated_ts, row_status, content, visibility
|
||||
`, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
row.Next()
|
||||
|
||||
`
|
||||
var memoRaw memoRaw
|
||||
if err := row.Scan(
|
||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||
&memoRaw.ID,
|
||||
&memoRaw.CreatorID,
|
||||
&memoRaw.CreatedTs,
|
||||
@@ -204,7 +273,7 @@ func patchMemoRaw(db *sql.DB, patch *api.MemoPatch) (*memoRaw, error) {
|
||||
return &memoRaw, nil
|
||||
}
|
||||
|
||||
func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
|
||||
func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*memoRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
@@ -222,8 +291,13 @@ func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
|
||||
if v := find.ContentSearch; v != nil {
|
||||
where, args = append(where, "content LIKE ?"), append(args, "%"+*v+"%")
|
||||
}
|
||||
if v := find.Visibility; v != nil {
|
||||
where, args = append(where, "visibility = ?"), append(args, *v)
|
||||
if v := find.VisibilityList; len(v) != 0 {
|
||||
list := []string{}
|
||||
for _, visibility := range v {
|
||||
list = append(list, fmt.Sprintf("$%d", len(args)+1))
|
||||
args = append(args, visibility)
|
||||
}
|
||||
where = append(where, fmt.Sprintf("visibility in (%s)", strings.Join(list, ",")))
|
||||
}
|
||||
|
||||
pagination := ""
|
||||
@@ -234,7 +308,7 @@ func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
query := `
|
||||
SELECT
|
||||
id,
|
||||
creator_id,
|
||||
@@ -244,10 +318,10 @@ func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
|
||||
content,
|
||||
visibility
|
||||
FROM memo
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY created_ts DESC`+pagination,
|
||||
args...,
|
||||
)
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY created_ts DESC
|
||||
` + pagination
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
@@ -278,8 +352,11 @@ func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
|
||||
return memoRawList, nil
|
||||
}
|
||||
|
||||
func deleteMemo(db *sql.DB, delete *api.MemoDelete) error {
|
||||
result, err := db.Exec(`DELETE FROM memo WHERE id = ?`, delete.ID)
|
||||
func deleteMemo(ctx context.Context, tx *sql.Tx, delete *api.MemoDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM memo WHERE id = ?
|
||||
`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
@@ -291,19 +368,3 @@ func deleteMemo(db *sql.DB, delete *api.MemoDelete) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) composeMemo(raw *memoRaw) (*api.Memo, error) {
|
||||
memo := raw.toMemo()
|
||||
|
||||
memoOrganizer, err := s.FindMemoOrganizer(&api.MemoOrganizerFind{
|
||||
MemoID: memo.ID,
|
||||
UserID: memo.CreatorID,
|
||||
})
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return nil, err
|
||||
} else if memoOrganizer != nil {
|
||||
memo.Pinned = memoOrganizer.Pinned
|
||||
}
|
||||
|
||||
return memo, nil
|
||||
}
|
||||
|
||||
+32
-22
@@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
@@ -29,8 +30,14 @@ func (raw *memoOrganizerRaw) toMemoOrganizer() *api.MemoOrganizer {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) FindMemoOrganizer(find *api.MemoOrganizerFind) (*api.MemoOrganizer, error) {
|
||||
memoOrganizerRaw, err := findMemoOrganizer(s.db, find)
|
||||
func (s *Store) FindMemoOrganizer(ctx context.Context, find *api.MemoOrganizerFind) (*api.MemoOrganizer, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
memoOrganizerRaw, err := findMemoOrganizer(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,17 +47,26 @@ func (s *Store) FindMemoOrganizer(find *api.MemoOrganizerFind) (*api.MemoOrganiz
|
||||
return memoOrganizer, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpsertMemoOrganizer(upsert *api.MemoOrganizerUpsert) error {
|
||||
err := upsertMemoOrganizer(s.db, upsert)
|
||||
func (s *Store) UpsertMemoOrganizer(ctx context.Context, upsert *api.MemoOrganizerUpsert) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := upsertMemoOrganizer(ctx, tx, upsert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findMemoOrganizer(db *sql.DB, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) {
|
||||
row, err := db.Query(`
|
||||
func findMemoOrganizer(ctx context.Context, tx *sql.Tx, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) {
|
||||
query := `
|
||||
SELECT
|
||||
id,
|
||||
memo_id,
|
||||
@@ -58,7 +74,8 @@ func findMemoOrganizer(db *sql.DB, find *api.MemoOrganizerFind) (*memoOrganizerR
|
||||
pinned
|
||||
FROM memo_organizer
|
||||
WHERE memo_id = ? AND user_id = ?
|
||||
`, find.MemoID, find.UserID)
|
||||
`
|
||||
row, err := tx.QueryContext(ctx, query, find.MemoID, find.UserID)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
@@ -78,11 +95,15 @@ func findMemoOrganizer(db *sql.DB, find *api.MemoOrganizerFind) (*memoOrganizerR
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &memoOrganizerRaw, nil
|
||||
}
|
||||
|
||||
func upsertMemoOrganizer(db *sql.DB, upsert *api.MemoOrganizerUpsert) error {
|
||||
row, err := db.Query(`
|
||||
func upsertMemoOrganizer(ctx context.Context, tx *sql.Tx, upsert *api.MemoOrganizerUpsert) error {
|
||||
query := `
|
||||
INSERT INTO memo_organizer (
|
||||
memo_id,
|
||||
user_id,
|
||||
@@ -93,20 +114,9 @@ func upsertMemoOrganizer(db *sql.DB, upsert *api.MemoOrganizerUpsert) error {
|
||||
SET
|
||||
pinned = EXCLUDED.pinned
|
||||
RETURNING id, memo_id, user_id, pinned
|
||||
`,
|
||||
upsert.MemoID,
|
||||
upsert.UserID,
|
||||
upsert.Pinned,
|
||||
)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
|
||||
row.Next()
|
||||
`
|
||||
var memoOrganizer api.MemoOrganizer
|
||||
if err := row.Scan(
|
||||
if err := tx.QueryRowContext(ctx, query, upsert.MemoID, upsert.UserID, upsert.Pinned).Scan(
|
||||
&memoOrganizer.ID,
|
||||
&memoOrganizer.MemoID,
|
||||
&memoOrganizer.UserID,
|
||||
|
||||
+81
-33
@@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -43,19 +44,39 @@ func (raw *resourceRaw) toResource() *api.Resource {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateResource(create *api.ResourceCreate) (*api.Resource, error) {
|
||||
resourceRaw, err := createResource(s.db, create)
|
||||
func (s *Store) CreateResource(ctx context.Context, create *api.ResourceCreate) (*api.Resource, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
resourceRaw, err := createResource(ctx, tx, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
resource := resourceRaw.toResource()
|
||||
|
||||
if err := s.cache.UpsertCache(api.ResourceCache, resource.ID, resource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindResourceList(find *api.ResourceFind) ([]*api.Resource, error) {
|
||||
resourceRawList, err := findResourceList(s.db, find)
|
||||
func (s *Store) FindResourceList(ctx context.Context, find *api.ResourceFind) ([]*api.Resource, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
resourceRawList, err := findResourceList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -68,8 +89,25 @@ func (s *Store) FindResourceList(find *api.ResourceFind) ([]*api.Resource, error
|
||||
return resourceList, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindResource(find *api.ResourceFind) (*api.Resource, error) {
|
||||
list, err := findResourceList(s.db, find)
|
||||
func (s *Store) FindResource(ctx context.Context, find *api.ResourceFind) (*api.Resource, error) {
|
||||
if find.ID != nil {
|
||||
resource := &api.Resource{}
|
||||
has, err := s.cache.FindCache(api.ResourceCache, *find.ID, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return resource, nil
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findResourceList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -80,20 +118,36 @@ func (s *Store) FindResource(find *api.ResourceFind) (*api.Resource, error) {
|
||||
|
||||
resource := list[0].toResource()
|
||||
|
||||
if err := s.cache.UpsertCache(api.ResourceCache, resource.ID, resource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteResource(delete *api.ResourceDelete) error {
|
||||
err := deleteResource(s.db, delete)
|
||||
func (s *Store) DeleteResource(ctx context.Context, delete *api.ResourceDelete) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = deleteResource(ctx, tx, delete)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.ResourceCache, delete.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error) {
|
||||
row, err := db.Query(`
|
||||
func createResource(ctx context.Context, tx *sql.Tx, create *api.ResourceCreate) (*resourceRaw, error) {
|
||||
query := `
|
||||
INSERT INTO resource (
|
||||
filename,
|
||||
blob,
|
||||
@@ -102,27 +156,16 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
|
||||
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()
|
||||
|
||||
row.Next()
|
||||
RETURNING id, filename, blob, type, size, creator_id, created_ts, updated_ts
|
||||
`
|
||||
var resourceRaw resourceRaw
|
||||
if err := row.Scan(
|
||||
if err := tx.QueryRowContext(ctx, query, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID).Scan(
|
||||
&resourceRaw.ID,
|
||||
&resourceRaw.Filename,
|
||||
&resourceRaw.Blob,
|
||||
&resourceRaw.Type,
|
||||
&resourceRaw.Size,
|
||||
&resourceRaw.CreatorID,
|
||||
&resourceRaw.CreatedTs,
|
||||
&resourceRaw.UpdatedTs,
|
||||
); err != nil {
|
||||
@@ -132,7 +175,7 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
|
||||
return &resourceRaw, nil
|
||||
}
|
||||
|
||||
func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error) {
|
||||
func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) ([]*resourceRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
@@ -145,20 +188,21 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
|
||||
where, args = append(where, "filename = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
query := `
|
||||
SELECT
|
||||
id,
|
||||
filename,
|
||||
blob,
|
||||
type,
|
||||
size,
|
||||
creator_id,
|
||||
created_ts,
|
||||
updated_ts
|
||||
FROM resource
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY created_ts DESC`,
|
||||
args...,
|
||||
)
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY created_ts DESC
|
||||
`
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
@@ -173,6 +217,7 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
|
||||
&resourceRaw.Blob,
|
||||
&resourceRaw.Type,
|
||||
&resourceRaw.Size,
|
||||
&resourceRaw.CreatorID,
|
||||
&resourceRaw.CreatedTs,
|
||||
&resourceRaw.UpdatedTs,
|
||||
); err != nil {
|
||||
@@ -189,8 +234,11 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
|
||||
return resourceRawList, nil
|
||||
}
|
||||
|
||||
func deleteResource(db *sql.DB, delete *api.ResourceDelete) error {
|
||||
result, err := db.Exec(`DELETE FROM resource WHERE id = ?`, delete.ID)
|
||||
func deleteResource(ctx context.Context, tx *sql.Tx, delete *api.ResourceDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM resource WHERE id = ? AND creator_id = ?
|
||||
`, delete.ID, delete.CreatorID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
+101
-49
@@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -39,30 +40,64 @@ func (raw *shortcutRaw) toShortcut() *api.Shortcut {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateShortcut(create *api.ShortcutCreate) (*api.Shortcut, error) {
|
||||
shortcutRaw, err := createShortcut(s.db, create)
|
||||
func (s *Store) CreateShortcut(ctx context.Context, create *api.ShortcutCreate) (*api.Shortcut, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
shortcutRaw, err := createShortcut(ctx, tx, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
shortcut := shortcutRaw.toShortcut()
|
||||
|
||||
return shortcut, nil
|
||||
}
|
||||
|
||||
func (s *Store) PatchShortcut(patch *api.ShortcutPatch) (*api.Shortcut, error) {
|
||||
shortcutRaw, err := patchShortcut(s.db, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
shortcut := shortcutRaw.toShortcut()
|
||||
|
||||
if err := s.cache.UpsertCache(api.ShortcutCache, shortcut.ID, shortcut); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shortcut, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindShortcutList(find *api.ShortcutFind) ([]*api.Shortcut, error) {
|
||||
shortcutRawList, err := findShortcutList(s.db, find)
|
||||
func (s *Store) PatchShortcut(ctx context.Context, patch *api.ShortcutPatch) (*api.Shortcut, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
shortcutRaw, err := patchShortcut(ctx, tx, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
shortcut := shortcutRaw.toShortcut()
|
||||
|
||||
if err := s.cache.UpsertCache(api.ShortcutCache, shortcut.ID, shortcut); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shortcut, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindShortcutList(ctx context.Context, find *api.ShortcutFind) ([]*api.Shortcut, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
shortcutRawList, err := findShortcutList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -75,8 +110,25 @@ func (s *Store) FindShortcutList(find *api.ShortcutFind) ([]*api.Shortcut, error
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindShortcut(find *api.ShortcutFind) (*api.Shortcut, error) {
|
||||
list, err := findShortcutList(s.db, find)
|
||||
func (s *Store) FindShortcut(ctx context.Context, find *api.ShortcutFind) (*api.Shortcut, error) {
|
||||
if find.ID != nil {
|
||||
shortcut := &api.Shortcut{}
|
||||
has, err := s.cache.FindCache(api.ShortcutCache, *find.ID, shortcut)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return shortcut, nil
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findShortcutList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -87,20 +139,36 @@ func (s *Store) FindShortcut(find *api.ShortcutFind) (*api.Shortcut, error) {
|
||||
|
||||
shortcut := list[0].toShortcut()
|
||||
|
||||
if err := s.cache.UpsertCache(api.ShortcutCache, shortcut.ID, shortcut); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shortcut, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteShortcut(delete *api.ShortcutDelete) error {
|
||||
err := deleteShortcut(s.db, delete)
|
||||
func (s *Store) DeleteShortcut(ctx context.Context, delete *api.ShortcutDelete) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = deleteShortcut(ctx, tx, delete)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.ShortcutCache, delete.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createShortcut(db *sql.DB, create *api.ShortcutCreate) (*shortcutRaw, error) {
|
||||
row, err := db.Query(`
|
||||
func createShortcut(ctx context.Context, tx *sql.Tx, create *api.ShortcutCreate) (*shortcutRaw, error) {
|
||||
query := `
|
||||
INSERT INTO shortcut (
|
||||
title,
|
||||
payload,
|
||||
@@ -108,19 +176,9 @@ func createShortcut(db *sql.DB, create *api.ShortcutCreate) (*shortcutRaw, error
|
||||
)
|
||||
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 shortcutRaw shortcutRaw
|
||||
if err := row.Scan(
|
||||
if err := tx.QueryRowContext(ctx, query, create.Title, create.Payload, create.CreatorID).Scan(
|
||||
&shortcutRaw.ID,
|
||||
&shortcutRaw.Title,
|
||||
&shortcutRaw.Payload,
|
||||
@@ -135,7 +193,7 @@ func createShortcut(db *sql.DB, create *api.ShortcutCreate) (*shortcutRaw, error
|
||||
return &shortcutRaw, nil
|
||||
}
|
||||
|
||||
func patchShortcut(db *sql.DB, patch *api.ShortcutPatch) (*shortcutRaw, error) {
|
||||
func patchShortcut(ctx context.Context, tx *sql.Tx, patch *api.ShortcutPatch) (*shortcutRaw, error) {
|
||||
set, args := []string{}, []interface{}{}
|
||||
|
||||
if v := patch.Title; v != nil {
|
||||
@@ -150,23 +208,14 @@ func patchShortcut(db *sql.DB, patch *api.ShortcutPatch) (*shortcutRaw, error) {
|
||||
|
||||
args = append(args, patch.ID)
|
||||
|
||||
row, err := db.Query(`
|
||||
query := `
|
||||
UPDATE shortcut
|
||||
SET `+strings.Join(set, ", ")+`
|
||||
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 shortcutRaw shortcutRaw
|
||||
if err := row.Scan(
|
||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||
&shortcutRaw.ID,
|
||||
&shortcutRaw.Title,
|
||||
&shortcutRaw.Payload,
|
||||
@@ -180,7 +229,7 @@ func patchShortcut(db *sql.DB, patch *api.ShortcutPatch) (*shortcutRaw, error) {
|
||||
return &shortcutRaw, nil
|
||||
}
|
||||
|
||||
func findShortcutList(db *sql.DB, find *api.ShortcutFind) ([]*shortcutRaw, error) {
|
||||
func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) ([]*shortcutRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
@@ -193,7 +242,7 @@ func findShortcutList(db *sql.DB, find *api.ShortcutFind) ([]*shortcutRaw, error
|
||||
where, args = append(where, "title = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
@@ -237,8 +286,11 @@ func findShortcutList(db *sql.DB, find *api.ShortcutFind) ([]*shortcutRaw, error
|
||||
return shortcutRawList, nil
|
||||
}
|
||||
|
||||
func deleteShortcut(db *sql.DB, delete *api.ShortcutDelete) error {
|
||||
result, err := db.Exec(`DELETE FROM shortcut WHERE id = ?`, delete.ID)
|
||||
func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM shortcut WHERE id = ?
|
||||
`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
+7
-2
@@ -3,19 +3,24 @@ package store
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
)
|
||||
|
||||
// Store provides database access to all raw objects
|
||||
// Store provides database access to all raw objects.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
profile *profile.Profile
|
||||
cache api.CacheService
|
||||
}
|
||||
|
||||
// New creates a new instance of Store
|
||||
// New creates a new instance of Store.
|
||||
func New(db *sql.DB, profile *profile.Profile) *Store {
|
||||
cacheService := NewCacheService()
|
||||
|
||||
return &Store{
|
||||
db: db,
|
||||
profile: profile,
|
||||
cache: cacheService,
|
||||
}
|
||||
}
|
||||
|
||||
+146
-40
@@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -43,30 +44,78 @@ func (raw *userRaw) toUser() *api.User {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateUser(create *api.UserCreate) (*api.User, error) {
|
||||
userRaw, err := createUser(s.db, create)
|
||||
func (s *Store) ComposeMemoCreator(ctx context.Context, memo *api.Memo) error {
|
||||
user, err := s.FindUser(ctx, &api.UserFind{
|
||||
ID: &memo.CreatorID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.OpenID = ""
|
||||
user.UserSettingList = nil
|
||||
memo.Creator = user
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateUser(ctx context.Context, create *api.UserCreate) (*api.User, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userRaw, err := createUser(ctx, tx, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := userRaw.toUser()
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) {
|
||||
userRaw, err := patchUser(s.db, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
user := userRaw.toUser()
|
||||
|
||||
if err := s.cache.UpsertCache(api.UserCache, user.ID, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserList(find *api.UserFind) ([]*api.User, error) {
|
||||
userRawList, err := findUserList(s.db, find)
|
||||
func (s *Store) PatchUser(ctx context.Context, patch *api.UserPatch) (*api.User, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userRaw, err := patchUser(ctx, tx, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
user := userRaw.toUser()
|
||||
|
||||
if err := s.cache.UpsertCache(api.UserCache, user.ID, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserList(ctx context.Context, find *api.UserFind) ([]*api.User, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userRawList, err := findUserList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -79,25 +128,67 @@ func (s *Store) FindUserList(find *api.UserFind) ([]*api.User, error) {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUser(find *api.UserFind) (*api.User, error) {
|
||||
list, err := findUserList(s.db, find)
|
||||
func (s *Store) FindUser(ctx context.Context, find *api.UserFind) (*api.User, error) {
|
||||
if find.ID != nil {
|
||||
user := &api.User{}
|
||||
has, err := s.cache.FindCache(api.UserCache, *find.ID, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findUserList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found user with filter %+v", find)}
|
||||
} 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 nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d users with filter %+v, expect 1", len(list), find)}
|
||||
}
|
||||
|
||||
user := list[0].toUser()
|
||||
|
||||
if err := s.cache.UpsertCache(api.UserCache, user.ID, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func createUser(db *sql.DB, create *api.UserCreate) (*userRaw, error) {
|
||||
row, err := db.Query(`
|
||||
func (s *Store) DeleteUser(ctx context.Context, delete *api.UserDelete) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = deleteUser(ctx, tx, delete)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.UserCache, delete.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userRaw, error) {
|
||||
query := `
|
||||
INSERT INTO user (
|
||||
email,
|
||||
role,
|
||||
@@ -107,21 +198,15 @@ func createUser(db *sql.DB, create *api.UserCreate) (*userRaw, error) {
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
|
||||
`,
|
||||
`
|
||||
var userRaw userRaw
|
||||
if err := tx.QueryRowContext(ctx, query,
|
||||
create.Email,
|
||||
create.Role,
|
||||
create.Name,
|
||||
create.PasswordHash,
|
||||
create.OpenID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
row.Next()
|
||||
var userRaw userRaw
|
||||
if err := row.Scan(
|
||||
).Scan(
|
||||
&userRaw.ID,
|
||||
&userRaw.Email,
|
||||
&userRaw.Role,
|
||||
@@ -138,7 +223,7 @@ func createUser(db *sql.DB, create *api.UserCreate) (*userRaw, error) {
|
||||
return &userRaw, nil
|
||||
}
|
||||
|
||||
func patchUser(db *sql.DB, patch *api.UserPatch) (*userRaw, error) {
|
||||
func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw, error) {
|
||||
set, args := []string{}, []interface{}{}
|
||||
|
||||
if v := patch.RowStatus; v != nil {
|
||||
@@ -159,12 +244,13 @@ func patchUser(db *sql.DB, patch *api.UserPatch) (*userRaw, error) {
|
||||
|
||||
args = append(args, patch.ID)
|
||||
|
||||
row, err := db.Query(`
|
||||
query := `
|
||||
UPDATE user
|
||||
SET `+strings.Join(set, ", ")+`
|
||||
SET ` + strings.Join(set, ", ") + `
|
||||
WHERE id = ?
|
||||
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
|
||||
`, args...)
|
||||
`
|
||||
row, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
@@ -186,13 +272,17 @@ func patchUser(db *sql.DB, patch *api.UserPatch) (*userRaw, error) {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userRaw, nil
|
||||
}
|
||||
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", patch.ID)}
|
||||
}
|
||||
|
||||
func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
|
||||
func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
@@ -211,7 +301,7 @@ func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
|
||||
where, args = append(where, "open_id = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
query := `
|
||||
SELECT
|
||||
id,
|
||||
email,
|
||||
@@ -223,10 +313,10 @@ func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
|
||||
updated_ts,
|
||||
row_status
|
||||
FROM user
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY created_ts DESC`,
|
||||
args...,
|
||||
)
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY created_ts DESC, row_status DESC
|
||||
`
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
@@ -246,7 +336,6 @@ func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
|
||||
&userRaw.UpdatedTs,
|
||||
&userRaw.RowStatus,
|
||||
); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
@@ -259,3 +348,20 @@ func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
|
||||
|
||||
return userRawList, nil
|
||||
}
|
||||
|
||||
func deleteUser(ctx context.Context, tx *sql.Tx, delete *api.UserDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM user 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("user ID not found: %d", delete.ID)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
)
|
||||
|
||||
type userSettingRaw struct {
|
||||
UserID int
|
||||
Key api.UserSettingKey
|
||||
Value string
|
||||
}
|
||||
|
||||
func (raw *userSettingRaw) toUserSetting() *api.UserSetting {
|
||||
return &api.UserSetting{
|
||||
UserID: raw.UserID,
|
||||
Key: raw.Key,
|
||||
Value: raw.Value,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUpsert) (*api.UserSetting, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userSettingRaw, err := upsertUserSetting(ctx, tx, upsert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userSetting := userSettingRaw.toUserSetting()
|
||||
|
||||
return userSetting, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserSettingList(ctx context.Context, find *api.UserSettingFind) ([]*api.UserSetting, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userSettingRawList, err := findUserSettingList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*api.UserSetting{}
|
||||
for _, raw := range userSettingRawList {
|
||||
list = append(list, raw.toUserSetting())
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserSetting(ctx context.Context, find *api.UserSettingFind) (*api.UserSetting, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findUserSettingList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userSetting := list[0].toUserSetting()
|
||||
|
||||
return userSetting, nil
|
||||
}
|
||||
|
||||
func upsertUserSetting(ctx context.Context, tx *sql.Tx, upsert *api.UserSettingUpsert) (*userSettingRaw, error) {
|
||||
query := `
|
||||
INSERT INTO user_setting (
|
||||
user_id, key, value
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id, key) DO UPDATE
|
||||
SET
|
||||
value = EXCLUDED.value
|
||||
RETURNING user_id, key, value
|
||||
`
|
||||
var userSettingRaw userSettingRaw
|
||||
if err := tx.QueryRowContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value).Scan(
|
||||
&userSettingRaw.UserID,
|
||||
&userSettingRaw.Key,
|
||||
&userSettingRaw.Value,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return &userSettingRaw, nil
|
||||
}
|
||||
|
||||
func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingFind) ([]*userSettingRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.Key; v != nil {
|
||||
where, args = append(where, "key = ?"), append(args, v.String())
|
||||
}
|
||||
|
||||
where, args = append(where, "user_id = ?"), append(args, find.UserID)
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
user_id,
|
||||
key,
|
||||
value
|
||||
FROM user_setting
|
||||
WHERE ` + strings.Join(where, " AND ")
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
userSettingRawList := make([]*userSettingRaw, 0)
|
||||
for rows.Next() {
|
||||
var userSettingRaw userSettingRaw
|
||||
if err := rows.Scan(
|
||||
&userSettingRaw.UserID,
|
||||
&userSettingRaw.Key,
|
||||
&userSettingRaw.Value,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
userSettingRawList = append(userSettingRawList, &userSettingRaw)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return userSettingRawList, nil
|
||||
}
|
||||
@@ -21,7 +21,6 @@
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-empty-interface": ["off"],
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
"react/react-in-jsx-scope": "off"
|
||||
},
|
||||
|
||||
+10
-3
@@ -2,14 +2,21 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.svg" sizes="64x64" type="image/*" />
|
||||
<link rel="icon" href="/logo.webp" type="image/*" />
|
||||
<meta name="theme-color" content="#f6f5f4" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Memos</title>
|
||||
<script src="https://kit.fontawesome.com/41e3aaa6af.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script>
|
||||
window.addEventListener("load", () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/sw.js");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "memos",
|
||||
"version": "0.2.1",
|
||||
"version": "0.4.3",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
@@ -15,6 +15,7 @@
|
||||
"qs": "^6.11.0",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-redux": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="0.1em" y=".9em" font-size="90">✍️</text></svg>
|
||||
|
Before Width: | Height: | Size: 121 B |
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"short_name": "Memos",
|
||||
"name": "Memos",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo.webp",
|
||||
"type": "image/*",
|
||||
"sizes": "200x200"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"background_color": "#f6f5f4",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#f6f5f4",
|
||||
"description": "usememos/memos"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil((async () => {})());
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil((async () => {})());
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {});
|
||||
|
||||
+32
-1
@@ -1,10 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import useI18n from "./hooks/useI18n";
|
||||
import { appRouterSwitch } from "./routers";
|
||||
import { globalService, locationService } from "./services";
|
||||
import { useAppSelector } from "./store";
|
||||
import * as storage from "./helpers/storage";
|
||||
|
||||
function App() {
|
||||
const { setLocale } = useI18n();
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
const global = useAppSelector((state) => state.global);
|
||||
const pathname = useAppSelector((state) => state.location.pathname);
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
|
||||
return <>{appRouterSwitch(pathname)}</>;
|
||||
useEffect(() => {
|
||||
locationService.updateStateWithLocation();
|
||||
window.onpopstate = () => {
|
||||
locationService.updateStateWithLocation();
|
||||
};
|
||||
globalService.initialState().then(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.setting.locale) {
|
||||
globalService.setLocale(user.setting.locale);
|
||||
}
|
||||
}, [user?.setting.locale]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocale(global.locale);
|
||||
storage.set({
|
||||
locale: global.locale,
|
||||
});
|
||||
}, [global.locale]);
|
||||
|
||||
return <>{isLoading ? null : appRouterSwitch(pathname)}</>;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import * as api from "../helpers/api";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import { showDialog } from "./Dialog";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import GitHubBadge from "./GitHubBadge";
|
||||
import "../less/about-site-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
type Props = DialogProps;
|
||||
|
||||
const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
const { t } = useI18n();
|
||||
const [profile, setProfile] = useState<Profile>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,22 +37,22 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">
|
||||
<span className="icon-text">🤠</span>About <b>Memos</b>
|
||||
<span className="icon-text">🤠</span>
|
||||
{t("common.about")}
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<p>
|
||||
Memos is an <i>open source</i>, <i>self-hosted</i> knowledge base that works with a SQLite db file.
|
||||
</p>
|
||||
<img className="logo-img" src="/logo-full.webp" alt="" />
|
||||
<p>{t("slogan")}</p>
|
||||
<br />
|
||||
<div className="addtion-info-container">
|
||||
<GitHubBadge />
|
||||
<Only when={profile !== undefined}>
|
||||
<>
|
||||
version:
|
||||
{t("common.version")}:
|
||||
<span className="pre-text">
|
||||
{profile?.version}-{profile?.mode}
|
||||
</span>
|
||||
@@ -63,7 +66,7 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
};
|
||||
|
||||
export default function showAboutSiteDialog(): void {
|
||||
showDialog(
|
||||
generateDialog(
|
||||
{
|
||||
className: "about-site-dialog",
|
||||
},
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { IMAGE_URL_REG } from "../helpers/consts";
|
||||
import { IMAGE_URL_REG } from "../helpers/marked";
|
||||
import * as utils from "../helpers/utils";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import { memoService } from "../services";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import Image from "./Image";
|
||||
import toastHelper from "./Toast";
|
||||
import { formatMemoContent } from "./Memo";
|
||||
import "../less/memo.less";
|
||||
|
||||
interface Props {
|
||||
@@ -19,6 +20,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
createdAtStr: utils.getDateTimeString(propsMemo.createdTs),
|
||||
archivedAtStr: utils.getDateTimeString(propsMemo.updatedTs ?? Date.now()),
|
||||
};
|
||||
const { t } = useI18n();
|
||||
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
|
||||
@@ -26,9 +28,10 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
if (showConfirmDeleteBtn) {
|
||||
try {
|
||||
await memoService.deleteMemoById(memo.id);
|
||||
await memoService.fetchAllMemos();
|
||||
await memoService.fetchMemos();
|
||||
} catch (error: any) {
|
||||
toastHelper.error(error.message);
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
}
|
||||
} else {
|
||||
toggleConfirmDeleteBtn();
|
||||
@@ -41,10 +44,11 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
id: memo.id,
|
||||
rowStatus: "NORMAL",
|
||||
});
|
||||
await memoService.fetchAllMemos();
|
||||
await memoService.fetchMemos();
|
||||
toastHelper.info("Restored successfully");
|
||||
} catch (error: any) {
|
||||
toastHelper.error(error.message);
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,10 +64,11 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
<span className="time-text">Archived at {memo.archivedAtStr}</span>
|
||||
<div className="btns-container">
|
||||
<span className="btn restore-btn" onClick={handleRestoreMemoClick}>
|
||||
Restore
|
||||
{t("common.restore")}
|
||||
</span>
|
||||
<span className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`} onClick={handleDeleteMemoClick}>
|
||||
{showConfirmDeleteBtn ? "Delete!" : "Delete"}
|
||||
{t("common.delete")}
|
||||
{showConfirmDeleteBtn ? "!" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,13 @@ import { useEffect, useState } from "react";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { memoService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import { showDialog } from "./Dialog";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import ArchivedMemo from "./ArchivedMemo";
|
||||
import "../less/archived-memo-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
type Props = DialogProps;
|
||||
|
||||
const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
const { destroy } = props;
|
||||
@@ -22,7 +23,8 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
setArchivedMemos(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastHelper.error("Failed to fetch archived memos: ", error);
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
})
|
||||
.finally(() => {
|
||||
loadingState.setFinish();
|
||||
@@ -37,7 +39,7 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
Archived Memos
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
<Icon.X className="icon-img" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
@@ -47,7 +49,7 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
) : archivedMemos.length === 0 ? (
|
||||
<div className="tip-text-container">
|
||||
<p className="tip-text">Here is No Zettels.</p>
|
||||
<p className="tip-text">No archived memos.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="archived-memos-container">
|
||||
@@ -61,11 +63,10 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default function showArchivedMemo(): void {
|
||||
showDialog(
|
||||
export default function showArchivedMemoDialog(): void {
|
||||
generateDialog(
|
||||
{
|
||||
className: "archived-memo-dialog",
|
||||
useAppContext: true,
|
||||
},
|
||||
ArchivedMemoDialog,
|
||||
{}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import "../less/beta-badge.less";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BetaBadge: React.FC<Props> = (props: Props) => {
|
||||
const { className } = props;
|
||||
|
||||
return <span className={`beta-badge ${className ?? ""}`}>beta</span>;
|
||||
};
|
||||
|
||||
export default BetaBadge;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import { memoService } from "../services";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import "../less/change-memo-created-ts-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
memoId: MemoId;
|
||||
}
|
||||
|
||||
const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
||||
const { t } = useI18n();
|
||||
const { destroy, memoId } = props;
|
||||
const [createdAt, setCreatedAt] = useState("");
|
||||
const maxDatetimeValue = dayjs().format("YYYY-MM-DDTHH:mm");
|
||||
|
||||
useEffect(() => {
|
||||
const memo = memoService.getMemoById(memoId);
|
||||
if (memo) {
|
||||
const datetime = dayjs(memo.createdTs).format("YYYY-MM-DDTHH:mm");
|
||||
setCreatedAt(datetime);
|
||||
} else {
|
||||
toastHelper.error("Memo not found.");
|
||||
destroy();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleDatetimeInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const datetime = e.target.value as string;
|
||||
setCreatedAt(datetime);
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
const nowTs = dayjs().unix();
|
||||
const createdTs = dayjs(createdAt).unix();
|
||||
|
||||
if (createdTs > nowTs) {
|
||||
toastHelper.error("Invalid created datetime.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await memoService.patchMemo({
|
||||
id: memoId,
|
||||
createdTs,
|
||||
});
|
||||
toastHelper.info("Memo created datetime changed.");
|
||||
handleCloseBtnClick();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Change memo created time</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<label className="form-label input-form-label">
|
||||
<input type="datetime-local" value={createdAt} max={maxDatetimeValue} onChange={handleDatetimeInputChange} />
|
||||
</label>
|
||||
<div className="btns-container">
|
||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||
{t("common.cancel")}
|
||||
</span>
|
||||
<span className="btn confirm-btn" onClick={handleSaveBtnClick}>
|
||||
{t("common.save")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function showChangeMemoCreatedTsDialog(memoId: MemoId) {
|
||||
generateDialog(
|
||||
{
|
||||
className: "change-memo-created-ts-dialog",
|
||||
},
|
||||
ChangeMemoCreatedTsDialog,
|
||||
{
|
||||
memoId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default showChangeMemoCreatedTsDialog;
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { validate, ValidatorConfig } from "../helpers/validator";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import { userService } from "../services";
|
||||
import { showDialog } from "./Dialog";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import "../less/change-password-dialog.less";
|
||||
|
||||
@@ -12,9 +14,10 @@ const validateConfig: ValidatorConfig = {
|
||||
noChinese: true,
|
||||
};
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
type Props = DialogProps;
|
||||
|
||||
const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
const { t } = useI18n();
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newPasswordAgain, setNewPasswordAgain] = useState("");
|
||||
|
||||
@@ -55,13 +58,16 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const user = userService.getState().user as User;
|
||||
await userService.patchUser({
|
||||
id: user.id,
|
||||
password: newPassword,
|
||||
});
|
||||
toastHelper.info("Password changed.");
|
||||
handleCloseBtnClick();
|
||||
} catch (error: any) {
|
||||
toastHelper.error(error);
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,7 +76,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Change Password</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
@@ -82,10 +88,10 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
</label>
|
||||
<div className="btns-container">
|
||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</span>
|
||||
<span className="btn confirm-btn" onClick={handleSaveBtnClick}>
|
||||
Save
|
||||
{t("common.save")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,7 +100,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
};
|
||||
|
||||
function showChangePasswordDialog() {
|
||||
showDialog(
|
||||
generateDialog(
|
||||
{
|
||||
className: "change-password-dialog",
|
||||
},
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { userService } from "../services";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { showDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import "../less/confirm-reset-openid-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
|
||||
const ConfirmResetOpenIdDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
const resetBtnClickLoadingState = useLoading(false);
|
||||
|
||||
useEffect(() => {
|
||||
// do nth
|
||||
}, []);
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleConfirmBtnClick = async () => {
|
||||
if (resetBtnClickLoadingState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetBtnClickLoadingState.setLoading();
|
||||
try {
|
||||
await userService.patchUser({
|
||||
resetOpenId: true,
|
||||
});
|
||||
} catch (error) {
|
||||
toastHelper.error("Request reset open API failed.");
|
||||
return;
|
||||
}
|
||||
toastHelper.success("Reset open API succeeded.");
|
||||
handleCloseBtnClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Reset Open API</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<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">
|
||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||
Cancel
|
||||
</span>
|
||||
<span className={`btn confirm-btn ${resetBtnClickLoadingState.isLoading ? "loading" : ""}`} onClick={handleConfirmBtnClick}>
|
||||
Reset!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function showConfirmResetOpenIdDialog() {
|
||||
showDialog(
|
||||
{
|
||||
className: "confirm-reset-openid-dialog",
|
||||
},
|
||||
ConfirmResetOpenIdDialog
|
||||
);
|
||||
}
|
||||
|
||||
export default showConfirmResetOpenIdDialog;
|
||||
@@ -1,8 +1,9 @@
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { memoService, shortcutService } from "../services";
|
||||
import { checkShouldShowMemoWithFilters, filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { showDialog } from "./Dialog";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import Selector from "./common/Selector";
|
||||
import "../less/create-shortcut-dialog.less";
|
||||
@@ -60,7 +61,8 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
toastHelper.error(error.message);
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
}
|
||||
destroy();
|
||||
};
|
||||
@@ -100,7 +102,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
{shortcutId ? "Edit Shortcut" : "Create Shortcut"}
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
@@ -150,129 +152,66 @@ interface MemoFilterInputerProps {
|
||||
handleFilterRemove: (index: number) => void;
|
||||
}
|
||||
|
||||
const FilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInputerProps) => {
|
||||
const MemoFilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInputerProps) => {
|
||||
const { index, filter, handleFilterChange, handleFilterRemove } = props;
|
||||
const [value, setValue] = useState<string>(filter.value.value);
|
||||
|
||||
const tags = Array.from(memoService.getState().tags);
|
||||
const { type } = filter;
|
||||
const [inputElements, setInputElements] = useState<JSX.Element>(<></>);
|
||||
const dataSource =
|
||||
type === "TYPE"
|
||||
? filterConsts["TYPE"].values
|
||||
: tags.sort().map((t) => {
|
||||
return { text: t, value: t };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let operatorElement = <></>;
|
||||
if (Object.keys(filterConsts).includes(type)) {
|
||||
operatorElement = (
|
||||
<Selector
|
||||
className="operator-selector"
|
||||
dataSource={Object.values(filterConsts[type as FilterType].operators)}
|
||||
value={filter.value.operator}
|
||||
handleValueChanged={handleOperatorChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
setValue(filter.value.value);
|
||||
}, [type]);
|
||||
|
||||
let valueElement = <></>;
|
||||
switch (type) {
|
||||
case "TYPE": {
|
||||
valueElement = (
|
||||
<Selector
|
||||
className="value-selector"
|
||||
dataSource={filterConsts["TYPE"].values}
|
||||
value={filter.value.value}
|
||||
handleValueChanged={handleValueChange}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "TAG": {
|
||||
valueElement = (
|
||||
<Selector
|
||||
className="value-selector"
|
||||
dataSource={tags.sort().map((t) => {
|
||||
return { text: t, value: t };
|
||||
})}
|
||||
value={filter.value.value}
|
||||
handleValueChanged={handleValueChange}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "TEXT": {
|
||||
valueElement = (
|
||||
<input
|
||||
type="text"
|
||||
className="value-inputer"
|
||||
value={filter.value.value}
|
||||
onChange={(event) => {
|
||||
handleValueChange(event.target.value);
|
||||
event.target.focus();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setInputElements(
|
||||
<>
|
||||
{operatorElement}
|
||||
{valueElement}
|
||||
</>
|
||||
);
|
||||
}, [type, filter]);
|
||||
|
||||
const handleRelationChange = useCallback(
|
||||
(value: string) => {
|
||||
if (["AND", "OR"].includes(value)) {
|
||||
handleFilterChange(index, {
|
||||
...filter,
|
||||
relation: value as MemoFilterRalation,
|
||||
});
|
||||
}
|
||||
},
|
||||
[filter]
|
||||
);
|
||||
|
||||
const handleTypeChange = useCallback(
|
||||
(value: string) => {
|
||||
if (filter.type !== value) {
|
||||
const ops = Object.values(filterConsts[value as FilterType].operators);
|
||||
handleFilterChange(index, {
|
||||
...filter,
|
||||
type: value as FilterType,
|
||||
value: {
|
||||
operator: ops[0].value,
|
||||
value: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[filter]
|
||||
);
|
||||
|
||||
const handleOperatorChange = useCallback(
|
||||
(value: string) => {
|
||||
const handleRelationChange = (value: string) => {
|
||||
if (["AND", "OR"].includes(value)) {
|
||||
handleFilterChange(index, {
|
||||
...filter,
|
||||
value: {
|
||||
...filter.value,
|
||||
operator: value,
|
||||
},
|
||||
relation: value as MemoFilterRalation,
|
||||
});
|
||||
},
|
||||
[filter]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(value: string) => {
|
||||
const handleTypeChange = (value: string) => {
|
||||
if (filter.type !== value) {
|
||||
const ops = Object.values(filterConsts[value as FilterType].operators);
|
||||
handleFilterChange(index, {
|
||||
...filter,
|
||||
type: value as FilterType,
|
||||
value: {
|
||||
...filter.value,
|
||||
value,
|
||||
operator: ops[0].value,
|
||||
value: "",
|
||||
},
|
||||
});
|
||||
},
|
||||
[filter]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOperatorChange = (value: string) => {
|
||||
handleFilterChange(index, {
|
||||
...filter,
|
||||
value: {
|
||||
...filter.value,
|
||||
operator: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleValueChange = (value: string) => {
|
||||
setValue(value);
|
||||
handleFilterChange(index, {
|
||||
...filter,
|
||||
value: {
|
||||
...filter.value,
|
||||
value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveBtnClick = () => {
|
||||
handleFilterRemove(index);
|
||||
@@ -294,17 +233,31 @@ const FilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInpute
|
||||
value={filter.type}
|
||||
handleValueChanged={handleTypeChange}
|
||||
/>
|
||||
|
||||
{inputElements}
|
||||
<i className="fa-solid fa-xmark remove-btn" onClick={handleRemoveBtnClick}></i>
|
||||
<Selector
|
||||
className="operator-selector"
|
||||
dataSource={Object.values(filterConsts[type as FilterType].operators)}
|
||||
value={filter.value.operator}
|
||||
handleValueChanged={handleOperatorChange}
|
||||
/>
|
||||
{type === "TEXT" ? (
|
||||
<input
|
||||
type="text"
|
||||
className="value-inputer"
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
handleValueChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Selector className="value-selector" dataSource={dataSource} value={value} handleValueChanged={handleValueChange} />
|
||||
)}
|
||||
<Icon.X className="remove-btn" onClick={handleRemoveBtnClick} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoFilterInputer: React.FC<MemoFilterInputerProps> = memo(FilterInputer);
|
||||
|
||||
export default function showCreateShortcutDialog(shortcutId?: ShortcutId): void {
|
||||
showDialog(
|
||||
generateDialog(
|
||||
{
|
||||
className: "create-shortcut-dialog",
|
||||
},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { IMAGE_URL_REG } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import { formatMemoContent } from "./Memo";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import "../less/daily-memo.less";
|
||||
|
||||
interface DailyMemo extends Memo {
|
||||
@@ -20,7 +18,6 @@ const DailyMemo: React.FC<Props> = (props: Props) => {
|
||||
createdAtStr: utils.getDateTimeString(propsMemo.createdTs),
|
||||
timeStr: utils.getTimeString(propsMemo.createdTs),
|
||||
};
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
|
||||
return (
|
||||
<div className="daily-memo-wrapper">
|
||||
@@ -28,14 +25,14 @@ const DailyMemo: React.FC<Props> = (props: Props) => {
|
||||
<span className="normal-text">{memo.timeStr}</span>
|
||||
</div>
|
||||
<div className="memo-content-container">
|
||||
<div className="memo-content-text" dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}></div>
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className="images-container">
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
<img key={idx} src={imgUrl} decoding="async" />
|
||||
))}
|
||||
</div>
|
||||
</Only>
|
||||
<div
|
||||
className="memo-content-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatMemoContent(memo.content, {
|
||||
inlineImage: true,
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="split-line"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useAppSelector } from "../store";
|
||||
import toImage from "../labs/html2image";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import { DAILY_TIMESTAMP } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { showDialog } from "./Dialog";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import DatePicker from "./common/DatePicker";
|
||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||
import DailyMemo from "./DailyMemo";
|
||||
@@ -18,6 +20,7 @@ const monthChineseStrArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "
|
||||
const weekdayChineseStrArray = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
|
||||
const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
||||
const { t } = useI18n();
|
||||
const memos = useAppSelector((state) => state.memo.memos);
|
||||
const [currentDateStamp, setCurrentDateStamp] = useState(utils.getDateStampByDate(utils.getDateString(props.currentDateStamp)));
|
||||
const [showDatePicker, toggleShowDatePicker] = useToggle(false);
|
||||
@@ -60,20 +63,21 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text" onClick={() => toggleShowDatePicker()}>
|
||||
<span className="icon-text">📅</span> Daily Review
|
||||
<span className="icon-text">📅</span> {t("sidebar.daily-review")}
|
||||
</p>
|
||||
<div className="btns-container">
|
||||
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp - DAILY_TIMESTAMP)}>
|
||||
<i className="fa-solid fa-chevron-left icon-img"></i>
|
||||
<Icon.ChevronLeft className="icon-img" />
|
||||
</button>
|
||||
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp + DAILY_TIMESTAMP)}>
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
<Icon.ChevronRight className="icon-img" />
|
||||
</button>
|
||||
<button className="btn-text" onClick={handleShareBtnClick}>
|
||||
<i className="fa-solid fa-share-nodes icon-img"></i>
|
||||
<button className="btn-text share" onClick={handleShareBtnClick}>
|
||||
<Icon.Share className="icon-img" />
|
||||
</button>
|
||||
<span className="split-line">/</span>
|
||||
<button className="btn-text" onClick={() => props.destroy()}>
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
<Icon.X className="icon-img" />
|
||||
</button>
|
||||
</div>
|
||||
<DatePicker
|
||||
@@ -108,10 +112,9 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
export default function showDailyReviewDialog(datestamp: DateStamp = Date.now()): void {
|
||||
showDialog(
|
||||
generateDialog(
|
||||
{
|
||||
className: "daily-review-dialog",
|
||||
useAppContext: true,
|
||||
},
|
||||
DailyReviewDialog,
|
||||
{ currentDateStamp: datestamp }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ANIMATION_DURATION } from "../helpers/consts";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "../store";
|
||||
import "../less/dialog.less";
|
||||
import { ANIMATION_DURATION } from "../../helpers/consts";
|
||||
import store from "../../store";
|
||||
import "../../less/base-dialog.less";
|
||||
|
||||
interface DialogConfig {
|
||||
className: string;
|
||||
useAppContext?: boolean;
|
||||
clickSpaceDestroy?: boolean;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,20 @@ interface Props extends DialogConfig, DialogProps {
|
||||
const BaseDialog: React.FC<Props> = (props: Props) => {
|
||||
const { children, className, clickSpaceDestroy, destroy } = props;
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.code === "Escape") {
|
||||
destroy();
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSpaceClicked = () => {
|
||||
if (clickSpaceDestroy) {
|
||||
destroy();
|
||||
@@ -32,7 +46,7 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export function showDialog<T extends DialogProps>(
|
||||
export function generateDialog<T extends DialogProps>(
|
||||
config: DialogConfig,
|
||||
DialogComponent: React.FC<T>,
|
||||
props?: Omit<T, "destroy">
|
||||
@@ -61,16 +75,14 @@ export function showDialog<T extends DialogProps>(
|
||||
destroy: cbs.destroy,
|
||||
} as T;
|
||||
|
||||
let Fragment = (
|
||||
<BaseDialog destroy={cbs.destroy} clickSpaceDestroy={true} {...config}>
|
||||
<DialogComponent {...dialogProps} />
|
||||
</BaseDialog>
|
||||
const Fragment = (
|
||||
<Provider store={store}>
|
||||
<BaseDialog destroy={cbs.destroy} clickSpaceDestroy={true} {...config}>
|
||||
<DialogComponent {...dialogProps} />
|
||||
</BaseDialog>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
if (config.useAppContext) {
|
||||
Fragment = <Provider store={store}>{Fragment}</Provider>;
|
||||
}
|
||||
|
||||
dialog.render(Fragment);
|
||||
|
||||
return cbs;
|
||||
@@ -0,0 +1,85 @@
|
||||
import Icon from "../Icon";
|
||||
import { generateDialog } from "./BaseDialog";
|
||||
import "../../less/common-dialog.less";
|
||||
|
||||
type DialogStyle = "info" | "warning";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
title: string;
|
||||
content: string;
|
||||
style?: DialogStyle;
|
||||
closeBtnText?: string;
|
||||
confirmBtnText?: string;
|
||||
onClose?: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
title: "",
|
||||
content: "",
|
||||
style: "info",
|
||||
closeBtnText: "Close",
|
||||
confirmBtnText: "Confirm",
|
||||
onClose: () => null,
|
||||
onConfirm: () => null,
|
||||
};
|
||||
|
||||
const CommonDialog: React.FC<Props> = (props: Props) => {
|
||||
const { title, content, destroy, closeBtnText, confirmBtnText, onClose, onConfirm, style } = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
onClose();
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleConfirmBtnClick = async () => {
|
||||
onConfirm();
|
||||
destroy();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">{title}</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<p className="content-text">{content}</p>
|
||||
<div className="btns-container">
|
||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||
{closeBtnText}
|
||||
</span>
|
||||
<span className={`btn confirm-btn ${style}`} onClick={handleConfirmBtnClick}>
|
||||
{confirmBtnText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface CommonDialogProps {
|
||||
title: string;
|
||||
content: string;
|
||||
className?: string;
|
||||
style?: DialogStyle;
|
||||
closeBtnText?: string;
|
||||
confirmBtnText?: string;
|
||||
onClose?: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
export const showCommonDialog = (props: CommonDialogProps) => {
|
||||
generateDialog(
|
||||
{
|
||||
className: `common-dialog ${props?.className ?? ""}`,
|
||||
},
|
||||
CommonDialog,
|
||||
props
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { generateDialog } from "./BaseDialog";
|
||||
@@ -1,4 +1,5 @@
|
||||
import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useRef } from "react";
|
||||
import useI18n from "../../hooks/useI18n";
|
||||
import useRefresh from "../../hooks/useRefresh";
|
||||
import Only from "../common/OnlyWhen";
|
||||
import "../../less/editor.less";
|
||||
@@ -9,6 +10,7 @@ export interface EditorRefActions {
|
||||
insertText: (text: string) => void;
|
||||
setContent: (text: string) => void;
|
||||
getContent: () => string;
|
||||
getCursorPosition: () => number;
|
||||
}
|
||||
|
||||
interface EditorProps {
|
||||
@@ -17,10 +19,8 @@ interface EditorProps {
|
||||
placeholder: string;
|
||||
fullscreen: boolean;
|
||||
showConfirmBtn: boolean;
|
||||
showCancelBtn: boolean;
|
||||
tools?: ReactNode;
|
||||
onConfirmBtnClick: (content: string) => void;
|
||||
onCancelBtnClick: () => void;
|
||||
onContentChange: (content: string) => void;
|
||||
}
|
||||
|
||||
@@ -32,11 +32,10 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
|
||||
placeholder,
|
||||
fullscreen,
|
||||
showConfirmBtn,
|
||||
showCancelBtn,
|
||||
onConfirmBtnClick: handleConfirmBtnClickCallback,
|
||||
onCancelBtnClick: handleCancelBtnClickCallback,
|
||||
onContentChange: handleContentChangeCallback,
|
||||
} = props;
|
||||
const { t } = useI18n();
|
||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
||||
const refresh = useRefresh();
|
||||
|
||||
@@ -66,14 +65,17 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
|
||||
}
|
||||
|
||||
const prevValue = editorRef.current.value;
|
||||
editorRef.current.value =
|
||||
prevValue.slice(0, editorRef.current.selectionStart) + rawText + prevValue.slice(editorRef.current.selectionStart);
|
||||
const cursorPosition = editorRef.current.selectionStart;
|
||||
editorRef.current.value = prevValue.slice(0, cursorPosition) + rawText + prevValue.slice(cursorPosition);
|
||||
editorRef.current.focus();
|
||||
editorRef.current.selectionEnd = cursorPosition + rawText.length;
|
||||
handleContentChangeCallback(editorRef.current.value);
|
||||
refresh();
|
||||
},
|
||||
setContent: (text: string) => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.value = text;
|
||||
editorRef.current.focus();
|
||||
handleContentChangeCallback(editorRef.current.value);
|
||||
refresh();
|
||||
}
|
||||
@@ -81,6 +83,9 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
|
||||
getContent: (): string => {
|
||||
return editorRef.current?.value ?? "";
|
||||
},
|
||||
getCursorPosition: (): number => {
|
||||
return editorRef.current?.selectionStart ?? 0;
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
@@ -109,10 +114,6 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
|
||||
editorRef.current.value = "";
|
||||
}, []);
|
||||
|
||||
const handleCommonCancelBtnClick = useCallback(() => {
|
||||
handleCancelBtnClickCallback();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={"common-editor-wrapper " + className}>
|
||||
<textarea
|
||||
@@ -128,14 +129,9 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
|
||||
<Only when={props.tools !== undefined}>{props.tools}</Only>
|
||||
</div>
|
||||
<div className="btns-container">
|
||||
<Only when={showCancelBtn}>
|
||||
<button className="action-btn cancel-btn" onClick={handleCommonCancelBtnClick}>
|
||||
Cancel editting
|
||||
</button>
|
||||
</Only>
|
||||
<Only when={showConfirmBtn}>
|
||||
<button className="action-btn confirm-btn" disabled={editorRef.current?.value === ""} onClick={handleCommonConfirmBtnClick}>
|
||||
Save <span className="icon-text">✍️</span>
|
||||
{t("editor.save")} <span className="icon-text">✍️</span>
|
||||
</button>
|
||||
</Only>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import * as api from "../helpers/api";
|
||||
import Icon from "./Icon";
|
||||
import "../less/github-badge.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const GitHubBadge: React.FC<Props> = () => {
|
||||
const GitHubBadge = () => {
|
||||
const [starCount, setStarCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -16,13 +15,10 @@ const GitHubBadge: React.FC<Props> = () => {
|
||||
return (
|
||||
<a className="github-badge-container" href="https://github.com/usememos/memos">
|
||||
<div className="github-icon">
|
||||
<i className="fa-brands fa-github fa-lg icon-img"></i>
|
||||
<Icon.GitHub className="icon-img" />
|
||||
Star
|
||||
</div>
|
||||
<div className="count-text">
|
||||
{starCount || ""}
|
||||
<span className="icon-text">🌟</span>
|
||||
</div>
|
||||
<div className="count-text">{starCount || ""}</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as Icon from "react-feather";
|
||||
|
||||
export default Icon;
|
||||
+65
-55
@@ -1,13 +1,16 @@
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { escape, indexOf } from "lodash-es";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG, UNKNOWN_ID } from "../helpers/consts";
|
||||
import { DONE_BLOCK_REG, parseMarkedToHtml, TODO_BLOCK_REG } from "../helpers/marked";
|
||||
import { indexOf } from "lodash-es";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import "dayjs/locale/zh";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import { UNKNOWN_ID } from "../helpers/consts";
|
||||
import { DONE_BLOCK_REG, formatMemoContent, TODO_BLOCK_REG } from "../helpers/marked";
|
||||
import { editorStateService, locationService, memoService, userService } from "../services";
|
||||
import Icon from "./Icon";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import toastHelper from "./Toast";
|
||||
import Image from "./Image";
|
||||
import MemoResources from "./MemoResources";
|
||||
import showMemoCardDialog from "./MemoCardDialog";
|
||||
import showShareMemoImageDialog from "./ShareMemoImageDialog";
|
||||
import "../less/memo.less";
|
||||
@@ -26,42 +29,48 @@ interface State {
|
||||
expandButtonStatus: ExpandButtonStatus;
|
||||
}
|
||||
|
||||
export const getFormatedMemoCreatedAtStr = (createdTs: number): string => {
|
||||
export const getFormatedMemoCreatedAtStr = (createdTs: number, locale = "en"): string => {
|
||||
if (Date.now() - createdTs < 1000 * 60 * 60 * 24) {
|
||||
return dayjs(createdTs).fromNow();
|
||||
return dayjs(createdTs).locale(locale).fromNow();
|
||||
} else {
|
||||
return dayjs(createdTs).format("YYYY/MM/DD HH:mm:ss");
|
||||
return dayjs(createdTs).locale(locale).format("YYYY/MM/DD HH:mm:ss");
|
||||
}
|
||||
};
|
||||
|
||||
const Memo: React.FC<Props> = (props: Props) => {
|
||||
const memo = props.memo;
|
||||
const { t, locale } = useI18n();
|
||||
const [state, setState] = useState<State>({
|
||||
expandButtonStatus: -1,
|
||||
});
|
||||
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs));
|
||||
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs, locale));
|
||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
||||
const isVisitorMode = userService.isVisitorMode();
|
||||
|
||||
useEffect(() => {
|
||||
if (!memoContainerRef) {
|
||||
if (!memoContentContainerRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number(memoContainerRef.current?.clientHeight) > MAX_MEMO_CONTAINER_HEIGHT) {
|
||||
if (Number(memoContentContainerRef.current?.clientHeight) > MAX_MEMO_CONTAINER_HEIGHT) {
|
||||
setState({
|
||||
...state,
|
||||
expandButtonStatus: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let intervalFlag = -1;
|
||||
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
|
||||
setInterval(() => {
|
||||
setCreatedAtStr(dayjs(memo.createdTs).fromNow());
|
||||
intervalFlag = setInterval(() => {
|
||||
setCreatedAtStr(getFormatedMemoCreatedAtStr(memo.createdTs, locale));
|
||||
}, 1000 * 1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalFlag);
|
||||
};
|
||||
}, [locale]);
|
||||
|
||||
const handleShowMemoStoryDialog = () => {
|
||||
showMemoCardDialog(memo);
|
||||
@@ -94,7 +103,8 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
rowStatus: "ARCHIVED",
|
||||
});
|
||||
} catch (error: any) {
|
||||
toastHelper.error(error.message);
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
}
|
||||
|
||||
if (editorStateService.getState().editMemoId === memo.id) {
|
||||
@@ -116,7 +126,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
if (memoTemp) {
|
||||
showMemoCardDialog(memoTemp);
|
||||
} else {
|
||||
toastHelper.error("MEMO Not Found");
|
||||
toastHelper.error("Memo Not Found");
|
||||
targetEl.classList.remove("memo-link-text");
|
||||
}
|
||||
} else if (targetEl.className === "tag-span") {
|
||||
@@ -133,7 +143,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
}
|
||||
|
||||
const status = targetEl.dataset?.value;
|
||||
const todoElementList = [...(memoContainerRef.current?.querySelectorAll(`span.todo-block[data-value=${status}]`) ?? [])];
|
||||
const todoElementList = [...(memoContentContainerRef.current?.querySelectorAll(`span.todo-block[data-value=${status}]`) ?? [])];
|
||||
for (const element of todoElementList) {
|
||||
if (element === targetEl) {
|
||||
const index = indexOf(todoElementList, element);
|
||||
@@ -161,91 +171,91 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMemoContentDoubleClick = (e: React.MouseEvent) => {
|
||||
const targetEl = e.target as HTMLElement;
|
||||
|
||||
if (targetEl.className === "memo-link-text") {
|
||||
return;
|
||||
} else if (targetEl.className === "tag-span") {
|
||||
return;
|
||||
} else if (targetEl.classList.contains("todo-block")) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorStateService.setEditMemoWithId(memo.id);
|
||||
};
|
||||
|
||||
const handleExpandBtnClick = () => {
|
||||
const expandButtonStatus = Boolean(!state.expandButtonStatus);
|
||||
if (!expandButtonStatus) {
|
||||
memoContainerRef.current?.scrollIntoView();
|
||||
}
|
||||
|
||||
setState({
|
||||
expandButtonStatus: Number(Boolean(!state.expandButtonStatus)) as ExpandButtonStatus,
|
||||
expandButtonStatus: Number(expandButtonStatus) as ExpandButtonStatus,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`}>
|
||||
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`} ref={memoContainerRef}>
|
||||
<div className="memo-top-wrapper">
|
||||
<div className="status-text-container" onClick={handleShowMemoStoryDialog}>
|
||||
<span className="time-text">{createdAtStr}</span>
|
||||
<Only when={memo.pinned}>
|
||||
<span className="status-text">PINNED</span>
|
||||
</Only>
|
||||
<Only when={memo.visibility === "PUBLIC" && !isVisitorMode}>
|
||||
<span className="status-text">PUBLIC</span>
|
||||
<Only when={memo.visibility !== "PRIVATE" && !isVisitorMode}>
|
||||
<span className={`status-text ${memo.visibility.toLocaleLowerCase()}`}>{memo.visibility}</span>
|
||||
</Only>
|
||||
</div>
|
||||
<div className={`btns-container ${userService.isVisitorMode() ? "!hidden" : ""}`}>
|
||||
<span className="btn more-action-btn">
|
||||
<i className="fa-solid fa-ellipsis icon-img"></i>
|
||||
<Icon.MoreHorizontal className="icon-img" />
|
||||
</span>
|
||||
<div className="more-action-btns-wrapper">
|
||||
<div className="more-action-btns-container">
|
||||
<div className="btns-container">
|
||||
<div className="btn" onClick={handleTogglePinMemoBtnClick}>
|
||||
<i className={`fa-solid fa-thumbtack icon-img ${memo.pinned ? "" : "opacity-20"}`}></i>
|
||||
<span className="tip-text">{memo.pinned ? "Unpin" : "Pin"}</span>
|
||||
<Icon.Flag className={`icon-img ${memo.pinned ? "" : "opacity-20"}`} />
|
||||
<span className="tip-text">{memo.pinned ? t("common.unpin") : t("common.pin")}</span>
|
||||
</div>
|
||||
<div className="btn" onClick={handleEditMemoClick}>
|
||||
<i className="fa-solid fa-pen-to-square icon-img"></i>
|
||||
<span className="tip-text">Edit</span>
|
||||
<Icon.Edit3 className="icon-img" />
|
||||
<span className="tip-text">{t("common.edit")}</span>
|
||||
</div>
|
||||
<div className="btn" onClick={handleGenMemoImageBtnClick}>
|
||||
<i className="fa-solid fa-share-nodes icon-img"></i>
|
||||
<span className="tip-text">Share</span>
|
||||
<Icon.Share className="icon-img" />
|
||||
<span className="tip-text">{t("common.share")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="btn" onClick={handleMarkMemoClick}>
|
||||
Mark
|
||||
{t("common.mark")}
|
||||
</span>
|
||||
<span className="btn" onClick={handleShowMemoStoryDialog}>
|
||||
View Story
|
||||
{t("memo.view-story")}
|
||||
</span>
|
||||
<span className="btn archive-btn" onClick={handleArchiveMemoClick}>
|
||||
Archive
|
||||
{t("common.archive")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={memoContainerRef}
|
||||
ref={memoContentContainerRef}
|
||||
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
|
||||
onClick={handleMemoContentClick}
|
||||
onDoubleClick={handleMemoContentDoubleClick}
|
||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}
|
||||
></div>
|
||||
{state.expandButtonStatus !== -1 && (
|
||||
<div className="expand-btn-container">
|
||||
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
|
||||
{state.expandButtonStatus === 0 ? "Expand" : "Fold"}
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
<Icon.ChevronRight className="icon-img" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className="images-wrapper">
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
<Image className="memo-img" key={idx} imgUrl={imgUrl} />
|
||||
))}
|
||||
</div>
|
||||
</Only>
|
||||
<MemoResources memo={memo} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function formatMemoContent(content: string) {
|
||||
const tempElement = document.createElement("div");
|
||||
tempElement.innerHTML = parseMarkedToHtml(escape(content));
|
||||
|
||||
return tempElement.innerHTML
|
||||
.replace(IMAGE_URL_REG, "")
|
||||
.replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>")
|
||||
.replace(LINK_URL_REG, "<a class='link' target='_blank' rel='noreferrer' href='$2'>$1</a>")
|
||||
.replace(TAG_REG, "<span class='tag-span'>#$1</span> ");
|
||||
}
|
||||
|
||||
export default memo(Memo);
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { editorStateService, memoService, userService } from "../services";
|
||||
import { IMAGE_URL_REG, MEMO_LINK_REG, UNKNOWN_ID } from "../helpers/consts";
|
||||
import { useAppSelector } from "../store";
|
||||
import { UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { parseHtmlToRawText } from "../helpers/marked";
|
||||
import { formatMemoContent, MEMO_LINK_REG, parseHtmlToRawText } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import toastHelper from "./Toast";
|
||||
import { showDialog } from "./Dialog";
|
||||
import Image from "./Image";
|
||||
import { formatMemoContent } from "./Memo";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import Icon from "./Icon";
|
||||
import Selector from "./common/Selector";
|
||||
import MemoResources from "./MemoResources";
|
||||
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
|
||||
import "../less/memo-card-dialog.less";
|
||||
|
||||
interface LinkedMemo extends Memo {
|
||||
@@ -20,12 +23,12 @@ interface Props extends DialogProps {
|
||||
}
|
||||
|
||||
const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
const memos = useAppSelector((state) => state.memo.memos);
|
||||
const [memo, setMemo] = useState<Memo>({
|
||||
...props.memo,
|
||||
});
|
||||
const [linkMemos, setLinkMemos] = useState<LinkedMemo[]>([]);
|
||||
const [linkedMemos, setLinkedMemos] = useState<LinkedMemo[]>([]);
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLinkedMemos = async () => {
|
||||
@@ -68,7 +71,12 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
fetchLinkedMemos();
|
||||
}, [memo.id]);
|
||||
setMemo(memoService.getMemoById(memo.id) as Memo);
|
||||
}, [memos, memo.id]);
|
||||
|
||||
const handleMemoCreatedAtClick = () => {
|
||||
showChangeMemoCreatedTsDialog(memo.id);
|
||||
};
|
||||
|
||||
const handleMemoContentClick = useCallback(async (e: React.MouseEvent) => {
|
||||
const targetEl = e.target as HTMLElement;
|
||||
@@ -86,7 +94,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
setLinkedMemos([]);
|
||||
setMemo(nextMemo);
|
||||
} else {
|
||||
toastHelper.error("MEMO Not Found");
|
||||
toastHelper.error("Memo Not Found");
|
||||
targetEl.classList.remove("memo-link-text");
|
||||
}
|
||||
}
|
||||
@@ -98,25 +106,16 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
setMemo(memo);
|
||||
}, []);
|
||||
|
||||
const handleEditMemoBtnClick = useCallback(() => {
|
||||
const handleEditMemoBtnClick = () => {
|
||||
props.destroy();
|
||||
editorStateService.setEditMemoWithId(memo.id);
|
||||
}, [memo.id]);
|
||||
|
||||
const handlePinClick = async () => {
|
||||
if (memo.pinned) {
|
||||
await memoService.unpinMemo(memo.id);
|
||||
} else {
|
||||
await memoService.pinMemo(memo.id);
|
||||
}
|
||||
setMemo({
|
||||
...memo,
|
||||
pinned: !memo.pinned,
|
||||
});
|
||||
};
|
||||
|
||||
const handleVisibilityClick = async () => {
|
||||
const visibility = memo.visibility === "PRIVATE" ? "PUBLIC" : "PRIVATE";
|
||||
const handleVisibilitySelectorChange = async (visibility: Visibility) => {
|
||||
if (memo.visibility === visibility) {
|
||||
return;
|
||||
}
|
||||
|
||||
await memoService.patchMemo({
|
||||
id: memo.id,
|
||||
visibility: visibility,
|
||||
@@ -129,25 +128,35 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<div className="card-header-container">
|
||||
<div className="visibility-selector-container">
|
||||
<Icon.Eye className="icon-img" />
|
||||
<Selector
|
||||
className="visibility-selector"
|
||||
dataSource={VISIBILITY_SELECTOR_ITEMS}
|
||||
value={memo.visibility}
|
||||
handleValueChanged={(value) => handleVisibilitySelectorChange(value as Visibility)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Only>
|
||||
<div className="memo-card-container">
|
||||
<div className="header-container">
|
||||
<p className="time-text">{utils.getDateTimeString(memo.createdTs)}</p>
|
||||
<p className="time-text" onClick={handleMemoCreatedAtClick}>
|
||||
{utils.getDateTimeString(memo.createdTs)}
|
||||
</p>
|
||||
<div className="btns-container">
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<>
|
||||
<button className="btn" onClick={handlePinClick}>
|
||||
<i className={`fa-solid fa-thumbtack icon-img ${memo.pinned ? "" : "opacity-20"}`}></i>
|
||||
</button>
|
||||
<button className="btn" onClick={handleVisibilityClick}>
|
||||
<i className={`fa-solid fa-eye icon-img ${memo.visibility === "PUBLIC" ? "" : "opacity-20"}`}></i>
|
||||
</button>
|
||||
<button className="btn edit-btn" onClick={handleEditMemoBtnClick}>
|
||||
<i className="fa-solid fa-pen-to-square icon-img"></i>
|
||||
<Icon.Edit3 className="icon-img" />
|
||||
</button>
|
||||
<span className="split-line">/</span>
|
||||
</>
|
||||
</Only>
|
||||
<button className="btn close-btn" onClick={props.destroy}>
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
<Icon.X className="icon-img" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,13 +166,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
onClick={handleMemoContentClick}
|
||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}
|
||||
></div>
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className="images-wrapper">
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
<Image className="memo-img" key={idx} imgUrl={imgUrl} />
|
||||
))}
|
||||
</div>
|
||||
</Only>
|
||||
<MemoResources memo={memo} />
|
||||
</div>
|
||||
<div className="layer-container"></div>
|
||||
{linkMemos.map((_, idx) => {
|
||||
@@ -218,7 +221,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
export default function showMemoCardDialog(memo: Memo): void {
|
||||
showDialog(
|
||||
generateDialog(
|
||||
{
|
||||
className: "memo-card-dialog",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useRef } from "react";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import "../less/memo-content.less";
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
content: string;
|
||||
onMemoContentClick: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const MemoContent: React.FC<Props> = (props: Props) => {
|
||||
const { className, content, onMemoContentClick } = props;
|
||||
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
||||
onMemoContentClick(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={memoContentContainerRef}
|
||||
className={`memo-content-text ${className}`}
|
||||
onClick={handleMemoContentClick}
|
||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(content) }}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoContent;
|
||||
@@ -1,20 +1,22 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { UNKNOWN_ID } from "../helpers/consts";
|
||||
import { editorStateService, locationService, memoService, resourceService } from "../services";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import { useAppSelector } from "../store";
|
||||
import * as storage from "../helpers/storage";
|
||||
import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
import Editor, { EditorRefActions } from "./Editor/Editor";
|
||||
import "../less/memo-editor.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface State {
|
||||
isUploadingResource: boolean;
|
||||
fullscreen: boolean;
|
||||
}
|
||||
|
||||
const MemoEditor: React.FC<Props> = () => {
|
||||
const MemoEditor = () => {
|
||||
const { t, locale } = useI18n();
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
const editorState = useAppSelector((state) => state.editor);
|
||||
const tags = useAppSelector((state) => state.memo.tags);
|
||||
const [state, setState] = useState<State>({
|
||||
@@ -24,6 +26,7 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
const editorRef = useRef<EditorRefActions>(null);
|
||||
const prevGlobalStateRef = useRef(editorState);
|
||||
const tagSeletorRef = useRef<HTMLDivElement>(null);
|
||||
const editorFontStyle = user?.setting.editorFontStyle || "normal";
|
||||
|
||||
useEffect(() => {
|
||||
if (editorState.markMemoId && editorState.markMemoId !== UNKNOWN_ID) {
|
||||
@@ -107,16 +110,17 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
const { type } = file;
|
||||
|
||||
if (!type.startsWith("image")) {
|
||||
toastHelper.error("Only image file supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const image = await resourceService.upload(file);
|
||||
const url = `/h/r/${image.id}/${image.filename}`;
|
||||
|
||||
const url = `/o/r/${image.id}/${image.filename}`;
|
||||
return url;
|
||||
} catch (error: any) {
|
||||
toastHelper.error(error);
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
} finally {
|
||||
setState({
|
||||
...state,
|
||||
@@ -152,7 +156,8 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
locationService.clearQuery();
|
||||
}
|
||||
} catch (error: any) {
|
||||
toastHelper.error(error.message);
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
}
|
||||
|
||||
setState({
|
||||
@@ -162,7 +167,7 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
setEditorContentCache("");
|
||||
};
|
||||
|
||||
const handleCancelBtnClick = useCallback(() => {
|
||||
const handleCancelEditingBtnClick = useCallback(() => {
|
||||
editorStateService.clearEditMemo();
|
||||
editorRef.current?.setContent("");
|
||||
setEditorContentCache("");
|
||||
@@ -172,21 +177,55 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
setEditorContentCache(content);
|
||||
}, []);
|
||||
|
||||
const handleCheckBoxBtnClick = () => {
|
||||
if (!editorRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPosition = editorRef.current.getCursorPosition();
|
||||
const prevValue = editorRef.current.getContent().slice(0, cursorPosition);
|
||||
if (prevValue === "" || prevValue.endsWith("\n")) {
|
||||
editorRef.current?.insertText("- [ ] ");
|
||||
} else {
|
||||
editorRef.current?.insertText("\n- [ ] ");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCodeBlockBtnClick = () => {
|
||||
if (!editorRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPosition = editorRef.current.getCursorPosition();
|
||||
const prevValue = editorRef.current.getContent().slice(0, cursorPosition);
|
||||
if (prevValue === "" || prevValue.endsWith("\n")) {
|
||||
editorRef.current?.insertText("```\n\n```");
|
||||
} else {
|
||||
editorRef.current?.insertText("\n```\n\n```");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadFileBtnClick = useCallback(() => {
|
||||
const inputEl = document.createElement("input");
|
||||
inputEl.style.position = "fixed";
|
||||
inputEl.style.top = "-100vh";
|
||||
inputEl.style.left = "-100vw";
|
||||
document.body.appendChild(inputEl);
|
||||
inputEl.type = "file";
|
||||
inputEl.multiple = false;
|
||||
inputEl.accept = "image/png, image/gif, image/jpeg";
|
||||
inputEl.multiple = true;
|
||||
inputEl.accept = "image/*";
|
||||
inputEl.onchange = async () => {
|
||||
if (!inputEl.files || inputEl.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = inputEl.files[0];
|
||||
const url = await handleUploadFile(file);
|
||||
if (url) {
|
||||
editorRef.current?.insertText(``);
|
||||
for (const file of inputEl.files) {
|
||||
const url = await handleUploadFile(file);
|
||||
if (url) {
|
||||
editorRef.current?.insertText(``);
|
||||
}
|
||||
}
|
||||
document.body.removeChild(inputEl);
|
||||
};
|
||||
inputEl.click();
|
||||
}, []);
|
||||
@@ -209,41 +248,60 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
|
||||
const editorConfig = useMemo(
|
||||
() => ({
|
||||
className: "memo-editor",
|
||||
className: `memo-editor ${editorFontStyle}`,
|
||||
initialContent: getEditorContentCache(),
|
||||
placeholder: "Any thoughts...",
|
||||
placeholder: t("editor.placeholder"),
|
||||
fullscreen: state.fullscreen,
|
||||
showConfirmBtn: true,
|
||||
showCancelBtn: isEditing,
|
||||
onConfirmBtnClick: handleSaveBtnClick,
|
||||
onCancelBtnClick: handleCancelBtnClick,
|
||||
onContentChange: handleContentChange,
|
||||
}),
|
||||
[isEditing, state.fullscreen]
|
||||
[isEditing, state.fullscreen, locale, editorFontStyle]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`memo-editor-container ${isEditing ? "edit-ing" : ""} ${state.fullscreen ? "fullscreen" : ""}`}>
|
||||
<p className={"tip-text " + (isEditing ? "" : "hidden")}>Editting...</p>
|
||||
<div className={`tip-container ${isEditing ? "" : "!hidden"}`}>
|
||||
<span className="tip-text">{t("editor.editing")}</span>
|
||||
<button className="cancel-btn" onClick={handleCancelEditingBtnClick}>
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
{...editorConfig}
|
||||
tools={
|
||||
<>
|
||||
<div className="action-btn tag-action">
|
||||
<i className="fa-solid fa-hashtag icon-img"></i>
|
||||
<Icon.Hash className="icon-img" />
|
||||
<div ref={tagSeletorRef} className="tag-list" onClick={handleTagSeletorClick}>
|
||||
{tags.map((t) => {
|
||||
return <span key={t}>{t}</span>;
|
||||
})}
|
||||
{tags.length > 0 ? (
|
||||
tags.map((tag) => {
|
||||
return (
|
||||
<span className="item-container" key={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="tip-text" onClick={(e) => e.stopPropagation()}>
|
||||
{t("common.null")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button className="action-btn">
|
||||
<i className="fa-solid fa-image icon-img" onClick={handleUploadFileBtnClick}></i>
|
||||
<Icon.CheckSquare className="icon-img" onClick={handleCheckBoxBtnClick} />
|
||||
</button>
|
||||
<button className="action-btn">
|
||||
<Icon.Code className="icon-img" onClick={handleCodeBlockBtnClick} />
|
||||
</button>
|
||||
<button className="action-btn">
|
||||
<Icon.Image className="icon-img" onClick={handleUploadFileBtnClick} />
|
||||
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
|
||||
</button>
|
||||
<button className="action-btn" onClick={handleFullscreenBtnClick}>
|
||||
<i className={`fa-solid fa-${state.fullscreen ? "compress" : "expand"} icon-img`}></i>
|
||||
{state.fullscreen ? <Icon.Minimize className="icon-img" /> : <Icon.Maximize className="icon-img" />}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import * as utils from "../helpers/utils";
|
||||
import { getTextWithMemoType } from "../helpers/filter";
|
||||
import "../less/memo-filter.less";
|
||||
|
||||
interface FilterProps {}
|
||||
|
||||
const MemoFilter: React.FC<FilterProps> = () => {
|
||||
const MemoFilter = () => {
|
||||
const query = useAppSelector((state) => state.location.query);
|
||||
useAppSelector((state) => state.shortcut.shortcuts);
|
||||
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { memoService, shortcutService } from "../services";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import { useAppSelector } from "../store";
|
||||
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts";
|
||||
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/marked";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { checkShouldShowMemoWithFilters } from "../helpers/filter";
|
||||
import toastHelper from "./Toast";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import Memo from "./Memo";
|
||||
import "../less/memo-list.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const MemoList: React.FC<Props> = () => {
|
||||
const MemoList = () => {
|
||||
const { t } = useI18n();
|
||||
const query = useAppSelector((state) => state.location.query);
|
||||
const memos = useAppSelector((state) => state.memo.memos);
|
||||
const [isFetching, setFetchStatus] = useState(true);
|
||||
const { memos, isFetching } = useAppSelector((state) => state.memo);
|
||||
const wrapperElement = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query ?? {};
|
||||
@@ -80,30 +80,37 @@ const MemoList: React.FC<Props> = () => {
|
||||
|
||||
useEffect(() => {
|
||||
memoService
|
||||
.fetchAllMemos()
|
||||
.fetchMemos()
|
||||
.then(() => {
|
||||
setFetchStatus(false);
|
||||
memoService.updateTagsState();
|
||||
// do nth
|
||||
})
|
||||
.catch(() => {
|
||||
toastHelper.error("😭 Fetching failed, please try again later.");
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
wrapperElement.current?.scrollTo({ top: 0 });
|
||||
wrapperElement.current?.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<div className={`memo-list-container ${isFetching ? "" : "completed"}`} ref={wrapperElement}>
|
||||
<Only when={isFetching}>
|
||||
<div className="status-text-container fetching-tip">
|
||||
<p className="status-text">{t("memo-list.fetching-data")}</p>
|
||||
</div>
|
||||
</Only>
|
||||
{sortedMemos.map((memo) => (
|
||||
<Memo key={`${memo.id}-${memo.updatedTs}`} memo={memo} />
|
||||
<Memo key={`${memo.id}-${memo.createdTs}-${memo.updatedTs}`} memo={memo} />
|
||||
))}
|
||||
<div className="status-text-container">
|
||||
<p className="status-text">
|
||||
{isFetching ? "Fetching data..." : sortedMemos.length === 0 ? "No memos 🌃" : showMemoFilter ? "" : "All memos are ready 🎉"}
|
||||
</p>
|
||||
</div>
|
||||
<Only when={!isFetching}>
|
||||
<div className="status-text-container">
|
||||
<p className="status-text">{sortedMemos.length === 0 ? "no memos 🌃" : showMemoFilter ? "" : "all memos are ready 🎉"}</p>
|
||||
</div>
|
||||
</Only>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IMAGE_URL_REG } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import Image from "./Image";
|
||||
import "../less/memo-resources.less";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
memo: Memo;
|
||||
}
|
||||
|
||||
const MemoResources: React.FC<Props> = (props: Props) => {
|
||||
const { className, memo } = props;
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
|
||||
return (
|
||||
<div className="resource-wrapper">
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className={`images-wrapper ${className ?? ""}`}>
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
<Image className="memo-img" key={idx} imgUrl={imgUrl} />
|
||||
))}
|
||||
</div>
|
||||
</Only>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoResources;
|
||||
@@ -1,15 +1,14 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { memoService, shortcutService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import Icon from "./Icon";
|
||||
import SearchBar from "./SearchBar";
|
||||
import { toggleSiderbar } from "./Sidebar";
|
||||
import "../less/memos-header.less";
|
||||
|
||||
let prevRequestTimestamp = Date.now();
|
||||
|
||||
interface Props {}
|
||||
|
||||
const MemosHeader: React.FC<Props> = () => {
|
||||
const MemosHeader = () => {
|
||||
const query = useAppSelector((state) => state.location.query);
|
||||
const shortcuts = useAppSelector((state) => state.shortcut.shortcuts);
|
||||
const [titleText, setTitleText] = useState("MEMOS");
|
||||
@@ -28,9 +27,9 @@ const MemosHeader: React.FC<Props> = () => {
|
||||
|
||||
const handleTitleTextClick = useCallback(() => {
|
||||
const now = Date.now();
|
||||
if (now - prevRequestTimestamp > 10 * 1000) {
|
||||
if (now - prevRequestTimestamp > 1 * 1000) {
|
||||
prevRequestTimestamp = now;
|
||||
memoService.fetchAllMemos().catch(() => {
|
||||
memoService.fetchMemos().catch(() => {
|
||||
// do nth
|
||||
});
|
||||
}
|
||||
@@ -40,7 +39,7 @@ const MemosHeader: React.FC<Props> = () => {
|
||||
<div className="section-header-container memos-header-container">
|
||||
<div className="title-container">
|
||||
<div className="action-btn" onClick={toggleSiderbar}>
|
||||
<i className="fa-solid fa-bars icon-img"></i>
|
||||
<Icon.Menu className="icon-img" />
|
||||
</div>
|
||||
<span className="title-text" onClick={handleTitleTextClick}>
|
||||
{titleText}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as api from "../helpers/api";
|
||||
import { locationService, userService } from "../services";
|
||||
import toastHelper from "./Toast";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import showAboutSiteDialog from "./AboutSiteDialog";
|
||||
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
||||
import showResourcesDialog from "./ResourcesDialog";
|
||||
import "../less/menu-btns-popup.less";
|
||||
|
||||
interface Props {
|
||||
@@ -13,6 +14,7 @@ interface Props {
|
||||
|
||||
const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
||||
const { shownStatus, setShownStatus } = props;
|
||||
const { t } = useI18n();
|
||||
const popupElRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,18 +32,12 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
||||
}
|
||||
}, [shownStatus]);
|
||||
|
||||
const handlePingBtnClick = () => {
|
||||
api
|
||||
.getSystemStatus()
|
||||
.then(({ data }) => {
|
||||
const {
|
||||
data: { profile },
|
||||
} = data;
|
||||
toastHelper.info(JSON.stringify(profile, null, 4));
|
||||
})
|
||||
.catch((error) => {
|
||||
toastHelper.error("Failed to ping\n" + JSON.stringify(error, null, 4));
|
||||
});
|
||||
const handleResourcesBtnClick = () => {
|
||||
showResourcesDialog();
|
||||
};
|
||||
|
||||
const handleArchivedBtnClick = () => {
|
||||
showArchivedMemoDialog();
|
||||
};
|
||||
|
||||
const handleAboutBtnClick = () => {
|
||||
@@ -49,24 +45,33 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
const handleSignOutBtnClick = async () => {
|
||||
userService.doSignOut().catch(() => {
|
||||
// do nth
|
||||
});
|
||||
locationService.replaceHistory("/signin");
|
||||
window.location.reload();
|
||||
userService
|
||||
.doSignOut()
|
||||
.then(() => {
|
||||
locationService.replaceHistory("/auth");
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(() => {
|
||||
// do nth
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`menu-btns-popup ${shownStatus ? "" : "hidden"}`} ref={popupElRef}>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button className="btn action-btn" onClick={handleResourcesBtnClick}>
|
||||
<span className="icon">🌄</span> {t("sidebar.resources")}
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handleArchivedBtnClick}>
|
||||
<span className="icon">🗂</span> {t("sidebar.archived")}
|
||||
</button>
|
||||
</Only>
|
||||
<button className="btn action-btn" onClick={handleAboutBtnClick}>
|
||||
<span className="icon">🤠</span> About
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handlePingBtnClick}>
|
||||
<span className="icon">🎯</span> Ping
|
||||
<span className="icon">🤠</span> {t("common.about")}
|
||||
</button>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button className="btn action-btn" onClick={handleSignOutBtnClick}>
|
||||
<span className="icon">👋</span> Sign out
|
||||
<span className="icon">👋</span> {t("common.sign-out")}
|
||||
</button>
|
||||
</Only>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { showDialog } from "./Dialog";
|
||||
import * as utils from "../helpers/utils";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import "../less/preview-image-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
@@ -18,25 +19,29 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrl }: Props) => {
|
||||
a.click();
|
||||
};
|
||||
|
||||
const handleImgContainerClick = () => {
|
||||
destroy();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="btns-container">
|
||||
<button className="btn" onClick={handleCloseBtnClick}>
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
<Icon.X className="icon-img" />
|
||||
</button>
|
||||
<button className="btn" onClick={handleDownloadBtnClick}>
|
||||
<i className="fa-solid fa-download icon-img"></i>
|
||||
<Icon.Download className="icon-img" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="img-container">
|
||||
<img src={imgUrl} />
|
||||
<div className="img-container" onClick={handleImgContainerClick}>
|
||||
<img onClick={(e) => e.stopPropagation()} src={imgUrl} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function showPreviewImageDialog(imgUrl: string): void {
|
||||
showDialog(
|
||||
generateDialog(
|
||||
{
|
||||
className: "preview-image-dialog",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import * as utils from "../helpers/utils";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { resourceService } from "../services";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||
import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
import "../less/resources-dialog.less";
|
||||
|
||||
type Props = DialogProps;
|
||||
|
||||
interface State {
|
||||
resources: Resource[];
|
||||
isUploadingResource: boolean;
|
||||
}
|
||||
|
||||
const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
||||
const { destroy } = props;
|
||||
const { t } = useI18n();
|
||||
const loadingState = useLoading();
|
||||
const [state, setState] = useState<State>({
|
||||
resources: [],
|
||||
isUploadingResource: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchResources()
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
})
|
||||
.finally(() => {
|
||||
loadingState.setFinish();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchResources = async () => {
|
||||
const data = await resourceService.getResourceList();
|
||||
setState({
|
||||
...state,
|
||||
resources: data,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUploadFileBtnClick = async () => {
|
||||
if (state.isUploadingResource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputEl = document.createElement("input");
|
||||
inputEl.style.position = "fixed";
|
||||
inputEl.style.top = "-100vh";
|
||||
inputEl.style.left = "-100vw";
|
||||
document.body.appendChild(inputEl);
|
||||
inputEl.type = "file";
|
||||
inputEl.multiple = true;
|
||||
inputEl.accept = "*";
|
||||
inputEl.onchange = async () => {
|
||||
if (!inputEl.files || inputEl.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState({
|
||||
...state,
|
||||
isUploadingResource: true,
|
||||
});
|
||||
|
||||
for (const file of inputEl.files) {
|
||||
try {
|
||||
await resourceService.upload(file);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
} finally {
|
||||
setState({
|
||||
...state,
|
||||
isUploadingResource: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.body.removeChild(inputEl);
|
||||
await fetchResources();
|
||||
};
|
||||
inputEl.click();
|
||||
};
|
||||
|
||||
const handlPreviewBtnClick = (resource: Resource) => {
|
||||
const resourceUrl = `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
|
||||
if (resource.type.startsWith("image")) {
|
||||
showPreviewImageDialog(resourceUrl);
|
||||
} else {
|
||||
window.open(resourceUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyResourceLinkBtnClick = (resource: Resource) => {
|
||||
utils.copyTextToClipboard(`${window.location.origin}/o/r/${resource.id}/${resource.filename}`);
|
||||
toastHelper.success("Succeed to copy resource link to clipboard");
|
||||
};
|
||||
|
||||
const handleDeleteResourceBtnClick = (resource: Resource) => {
|
||||
showCommonDialog({
|
||||
title: `Delete Resource`,
|
||||
content: `Are you sure to delete this resource? THIS ACTION IS IRREVERSIABLE.❗️`,
|
||||
style: "warning",
|
||||
onConfirm: async () => {
|
||||
await resourceService.deleteResourceById(resource.id);
|
||||
await fetchResources();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">
|
||||
<span className="icon-text">🌄</span>
|
||||
{t("sidebar.resources")}
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<Icon.X className="icon-img" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<div className="tip-text-container">(👨💻WIP) View your static resources in memos. e.g. images</div>
|
||||
<div className="upload-resource-container" onClick={() => handleUploadFileBtnClick()}>
|
||||
<div className="upload-resource-btn">
|
||||
<Icon.File className="icon-img" />
|
||||
<span>Upload</span>
|
||||
</div>
|
||||
</div>
|
||||
{loadingState.isLoading ? (
|
||||
<div className="loading-text-container">
|
||||
<p className="tip-text">fetching data...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="resource-table-container">
|
||||
<div className="fields-container">
|
||||
<span className="field-text">ID</span>
|
||||
<span className="field-text name-text">NAME</span>
|
||||
<span className="field-text">TYPE</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{state.resources.length === 0 ? (
|
||||
<p className="tip-text">No resource.</p>
|
||||
) : (
|
||||
state.resources.map((resource) => (
|
||||
<div key={resource.id} className="resource-container">
|
||||
<span className="field-text">{resource.id}</span>
|
||||
<span className="field-text name-text">{resource.filename}</span>
|
||||
<span className="field-text">{resource.type}</span>
|
||||
<div className="buttons-container">
|
||||
<Dropdown className="actions-dropdown">
|
||||
<button onClick={() => handlPreviewBtnClick(resource)}>Preview</button>
|
||||
<button onClick={() => handleCopyResourceLinkBtnClick(resource)}>Copy Link</button>
|
||||
<button className="delete-btn" onClick={() => handleDeleteResourceBtnClick(resource)}>
|
||||
Delete
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function showResourcesDialog() {
|
||||
generateDialog(
|
||||
{
|
||||
className: "resources-dialog",
|
||||
},
|
||||
ResourcesDialog,
|
||||
{}
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { locationService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import { memoSpecialTypes } from "../helpers/filter";
|
||||
import Icon from "./Icon";
|
||||
import "../less/search-bar.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SearchBar: React.FC<Props> = () => {
|
||||
const SearchBar = () => {
|
||||
const memoType = useAppSelector((state) => state.location.query?.type);
|
||||
|
||||
const handleMemoTypeItemClick = (type: MemoSpecType | undefined) => {
|
||||
@@ -24,7 +23,7 @@ const SearchBar: React.FC<Props> = () => {
|
||||
return (
|
||||
<div className="search-bar-container">
|
||||
<div className="search-bar-inputer">
|
||||
<i className="fa-solid fa-magnifying-glass fa-sm icon-img"></i>
|
||||
<Icon.Search className="icon-img" />
|
||||
<input className="text-input" type="text" placeholder="" onChange={handleTextQueryInput} />
|
||||
</div>
|
||||
<div className="quickly-action-wrapper">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user