diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 6035c7cb9d2e9..5f8d7ea601259 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -1,11 +1,11 @@
name: Bug Report
-description: Create a report to help us improve
+description: If something isn't working as expected
labels: [bug]
body:
- type: markdown
attributes:
value: |
- If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead.
+ Before submitting a bug report, please check if the issue is already present in the issues. If it is, please add a reaction to the issue. If it isn't, please fill out the form below.
- type: textarea
attributes:
label: Describe the bug
@@ -24,8 +24,15 @@ body:
3. See error
validations:
required: true
+ - type: input
+ attributes:
+ label: The version of Memos you're using
+ description: |
+ Provide the version of Memos you're using.
+ validations:
+ required: true
- type: textarea
attributes:
label: Screenshots or additional context
description: |
- Add screenshots or any other context about the problem.
+ If applicable, add screenshots to help explain your problem. And add any other context about the problem here. Such as the device you're using, etc.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index e9298d19a669a..6a312f56de644 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -1,28 +1,36 @@
name: Feature Request
-description: Suggest an idea for this project
+description: If you have a suggestion for a new feature
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
- Thanks for taking the time to suggest an idea for memos!
+ Before submitting a feature request, please check if the issue is already present in the issues. If it is, please add a reaction to the issue. If it isn't, please fill out the form below.
- type: textarea
attributes:
- label: Is your feature request related to a problem?
+ label: Describe the solution you'd like
description: |
- A clear and concise description of what the problem is.
+ A clear and concise description of what you want to happen.
placeholder: |
- I'm always frustrated when [...]
+ It would be great if [...]
validations:
required: true
- - type: textarea
+ - type: dropdown
attributes:
- label: Describe the solution you'd like
- description: |
- A clear and concise description of what you want to happen.
+ label: Type of feature
+ description: What type of feature is this?
+ options:
+ - User Interface (UI)
+ - User Experience (UX)
+ - API
+ - Documentation
+ - Integrations
+ - Other
+ default: 0
validations:
required: true
- type: textarea
attributes:
label: Additional context
- description: Add any other context or screenshots about the feature request.
+ description: |
+ What are you trying to do? Why is this important to you?
diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml
index 01a50d6f4feda..0573d9c3a9e3b 100644
--- a/.github/workflows/backend-tests.yml
+++ b/.github/workflows/backend-tests.yml
@@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
- go-version: 1.21
+ go-version: 1.22
check-latest: true
cache: true
- name: Verify go.mod is tidy
run: |
- go mod tidy -go=1.21
+ go mod tidy -go=1.22
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
@@ -37,9 +37,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
- go-version: 1.21
+ go-version: 1.22
check-latest: true
cache: true
- name: Run all tests
diff --git a/.github/workflows/build-and-push-release-image.yml b/.github/workflows/build-and-push-release-image.yml
index 4fba97afd5bb1..ddb04ca2acaa5 100644
--- a/.github/workflows/build-and-push-release-image.yml
+++ b/.github/workflows/build-and-push-release-image.yml
@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@v3
- name: Extract build args
# Extract version from branch name
@@ -25,13 +25,13 @@ jobs:
echo "VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
- name: Login to Docker Hub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
username: neosmemo
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -39,26 +39,25 @@ jobs:
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v3
with:
install: true
version: v0.9.1
- name: Docker meta
id: meta
- uses: docker/metadata-action@v4
+ uses: docker/metadata-action@v5
with:
images: |
neosmemo/memos
ghcr.io/usememos/memos
tags: |
- type=raw,value=latest
type=semver,pattern={{version}},value=${{ env.VERSION }}
type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }}
- name: Build and Push
id: docker_build
- uses: docker/build-push-action@v3
+ uses: docker/build-push-action@v5
with:
context: ./
file: ./Dockerfile
diff --git a/.github/workflows/build-and-push-stable-image.yml b/.github/workflows/build-and-push-stable-image.yml
new file mode 100644
index 0000000000000..3c399563790e6
--- /dev/null
+++ b/.github/workflows/build-and-push-stable-image.yml
@@ -0,0 +1,61 @@
+name: build-and-push-stable-image
+
+on:
+ push:
+ branches:
+ - "stable"
+
+jobs:
+ build-and-push-release-image:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: neosmemo
+ password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ github.token }}
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v3
+ with:
+ install: true
+ version: v0.9.1
+
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ neosmemo/memos
+ ghcr.io/usememos/memos
+ tags: |
+ type=raw,value=stable
+ flavor: |
+ latest=true
+
+ - name: Build and Push
+ id: docker_build
+ uses: docker/build-push-action@v5
+ with:
+ context: ./
+ file: ./Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/build-and-push-test-image.yml b/.github/workflows/build-and-push-test-image.yml
index 23c0e9e106de1..419dec832fa04 100644
--- a/.github/workflows/build-and-push-test-image.yml
+++ b/.github/workflows/build-and-push-test-image.yml
@@ -14,16 +14,16 @@ jobs:
- uses: actions/checkout@v4
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@v3
- name: Login to Docker Hub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
username: neosmemo
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -31,14 +31,14 @@ jobs:
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v3
with:
install: true
version: v0.9.1
- name: Docker meta
id: meta
- uses: docker/metadata-action@v4
+ uses: docker/metadata-action@v5
with:
images: |
neosmemo/memos
@@ -50,7 +50,7 @@ jobs:
- name: Build and Push
id: docker_build
- uses: docker/build-push-action@v3
+ uses: docker/build-push-action@v5
with:
context: ./
file: ./Dockerfile
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index f5d57eaed2ddd..0000000000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
- push:
- branches: [main]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [main]
- paths:
- - "go.mod"
- - "go.sum"
- - "**.go"
- - "proto/**"
- - "web/**"
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- 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@v4
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- 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@v2
-
- # ℹ️ 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
-
- #- run: |
- # make bootstrap
- # make release
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 0000000000000..5d4267891c272
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,80 @@
+name: Release Docker Image
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ on-success:
+ needs: publish
+ runs-on: ubuntu-latest
+ container: whatwewant/zmicro:v1
+ if: ${{ always() && needs.publish.result == 'success' }}
+ steps:
+ - name: Notification Feishu
+ uses: whatwewant/action-robot-feishu@v0.0.13
+ with:
+ url: ${{ secrets.DOCKER_VERSION_FEISHU_BOT_WEBHOOK_URL }}
+ title: '✅ Docker 发布:${{ github.repository }}'
+ text: |
+ 分支: ${{ github.ref }}
+ 提交信息: ${{ github.event.head_commit.message }}
+ 提交人: ${{ github.actor }}
+ 状态: 构建成功(https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
+
+ on-failure:
+ needs: publish
+ runs-on: ubuntu-latest
+ container: whatwewant/zmicro:v1
+ if: ${{ always() && needs.publish.result == 'failure' }}
+ steps:
+ - name: Notification Feishu
+ uses: whatwewant/action-robot-feishu@v0.0.13
+ with:
+ url: ${{ secrets.DOCKER_VERSION_FEISHU_BOT_WEBHOOK_URL }}
+ title: '❌ Docker 发布:${{ github.repository }}'
+ text: |
+ 分支: ${{ github.ref }}
+ 提交信息: ${{ github.event.head_commit.message }}
+ 提交人: ${{ github.actor }}
+ 状态: 构建失败(https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
+
+ publish:
+ runs-on: ubuntu-latest
+ container: whatwewant/zmicro:v1
+
+ steps:
+ - name: Get Current Date
+ id: date
+ run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
+
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: '0'
+
+ - name: Docker Meta
+ id: meta
+ uses: zmicro-design/action-docker-image-meta@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+
+ - name: Show Docker Tags
+ run: |
+ echo "Docker Tags: ${{ steps.meta.outputs.tags }}"
+
+ - name: Build and push
+ uses: zmicro-design/action-docker-build@v1
+ with:
+ build-args: |
+ VERSION=${{ steps.meta.outputs.version }}
+ context: .
+ push: ${{ github.event_name != 'pull_request' }}
+ cache-from: type=registry,ref=${{ steps.meta.outputs.name }}:buildcache
+ cache-to: type=registry,ref=${{ steps.meta.outputs.name }}:buildcache,mode=max
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ platforms: linux/amd64
+ #
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml
index 3b42dd860980e..af4f5dcb8c3ed 100644
--- a/.github/workflows/frontend-tests.yml
+++ b/.github/workflows/frontend-tests.yml
@@ -15,18 +15,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: pnpm/action-setup@v2.2.4
+ - uses: pnpm/action-setup@v2.4.0
with:
version: 8
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
- node-version: "18"
+ node-version: "20"
cache: pnpm
cache-dependency-path: "web/pnpm-lock.yaml"
- run: pnpm install
working-directory: web
- - run: pnpm type-gen
- working-directory: web
- name: Run eslint check
run: pnpm lint
working-directory: web
@@ -35,18 +33,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: pnpm/action-setup@v2.2.4
+ - uses: pnpm/action-setup@v2.4.0
with:
version: 8
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
- node-version: "18"
+ node-version: "20"
cache: pnpm
cache-dependency-path: "web/pnpm-lock.yaml"
- run: pnpm install
working-directory: web
- - run: pnpm type-gen
- working-directory: web
- name: Run frontend build
run: pnpm build
working-directory: web
diff --git a/.github/workflows/issue-translator.yml b/.github/workflows/issue-translator.yml
deleted file mode 100644
index f458c5268b7a5..0000000000000
--- a/.github/workflows/issue-translator.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: 'issue-translator'
-on:
- issue_comment:
- types: [created]
- issues:
- types: [opened]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: usthe/issues-translate-action@v2.7
- with:
- IS_MODIFY_TITLE: false
- # not require, default false, . Decide whether to modify the issue title
- # if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
- CUSTOM_BOT_NOTE: Issue is not in English. It has been translated automatically.
- # not require. Customize the translation robot prefix message.
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
new file mode 100644
index 0000000000000..f787269dbf50d
--- /dev/null
+++ b/.github/workflows/stale.yml
@@ -0,0 +1,17 @@
+name: Close Stale Issues
+
+on:
+ schedule:
+ - cron: "0 */8 * * *"
+
+jobs:
+ stale:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+
+ steps:
+ - uses: actions/stale@v9.0.0
+ with:
+ days-before-issue-stale: 14
+ days-before-issue-close: 7
diff --git a/.gitignore b/.gitignore
index 240621df1419b..17f64ee21ad8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@ tmp
# Frontend asset
web/dist
-server/dist
+server/frontend/dist
# build folder
build
@@ -16,4 +16,9 @@ build
# Jetbrains
.idea
+# Docker Compose Environment File
+.env
+
bin/air
+
+dev-dist
diff --git a/.golangci.yaml b/.golangci.yaml
index 0c1ba43265965..31aac66c2df25 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -67,6 +67,14 @@ linters-settings:
disabled: true
- name: early-return
disabled: true
+ - name: use-any
+ disabled: true
+ - name: exported
+ disabled: true
+ - name: unhandled-error
+ disabled: true
+ - name: if-return
+ disabled: true
gocritic:
disabled-checks:
- ifElseChain
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
deleted file mode 100644
index 12a59cb012e3e..0000000000000
--- a/.vscode/extensions.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "recommendations": ["golang.go"]
-}
diff --git a/.vscode/project.code-workspace b/.vscode/project.code-workspace
deleted file mode 100644
index b27ee896d466d..0000000000000
--- a/.vscode/project.code-workspace
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "folders": [
- {
- "name": "server",
- "path": "../"
- },
- {
- "name": "web",
- "path": "../web"
- }
- ]
-}
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index bf4a44ad73aff..0000000000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "json.schemaDownload.enable":true,
- "go.lintOnSave": "workspace",
- "go.lintTool": "golangci-lint",
-}
diff --git a/Dockerfile b/Dockerfile
index 8286ca0f67d30..1f9fe8ec45102 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,31 +1,35 @@
# Build frontend dist.
-FROM node:18-alpine AS frontend
+FROM whatwewant/builder-node:v18-1 AS frontend
WORKDIR /frontend-build
COPY . .
WORKDIR /frontend-build/web
-RUN corepack enable && pnpm i --frozen-lockfile && pnpm type-gen
+RUN corepack enable && pnpm i --frozen-lockfile
RUN pnpm build
# Build backend exec file.
-FROM golang:1.21-alpine AS backend
+FROM golang:1.22-alpine AS backend
WORKDIR /backend-build
+COPY go.mod .
+COPY go.sum .
+RUN go mod download
+
COPY . .
-COPY --from=frontend /frontend-build/web/dist ./server/dist
-RUN CGO_ENABLED=0 go build -o memos ./main.go
+RUN CGO_ENABLED=0 go build -o memos ./bin/memos/main.go
# Make workspace with above generated files.
-FROM alpine:latest AS monolithic
+FROM whatwewant/alpine:v3.17-1 AS monolithic
WORKDIR /usr/local/memos
RUN apk add --no-cache tzdata
ENV TZ="UTC"
+COPY --from=frontend /frontend-build/web/dist /usr/local/memos/dist
COPY --from=backend /backend-build/memos /usr/local/memos/
EXPOSE 5230
diff --git a/README.md b/README.md
index 20f3524607d59..3136668381498 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,4 @@
-# memos
-
-
+
A privacy-first, lightweight note-taking service. Easily capture and share your great thoughts.
@@ -10,13 +8,11 @@ A privacy-first, lightweight note-taking service. Easily capture and share your
Live Demo
+
## Star history
[](https://star-history.com/#usememos/memos&Date)
+
+## Other projects
+
+- [**Slash**](https://github.com/yourselfhosted/slash): An open source, self-hosted bookmarks and link sharing platform. Save and share your links very easily.
+- [**Gomark**](https://github.com/yourselfhosted/gomark): A markdown parser written in Go for Memos. And its [WebAssembly version](https://github.com/yourselfhosted/gomark-wasm) is also available.
diff --git a/SECURITY.md b/SECURITY.md
index af97c79abca7d..48ab17ab2f976 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,4 +4,4 @@
Report security bugs via GitHub [issues](https://github.com/usememos/memos/issues).
-For more information, please contact [stevenlgtm@gmail.com](stevenlgtm@gmail.com).
+For more information, please contact [usememos@gmail.com](usememos@gmail.com).
diff --git a/api/v1/rss.go b/api/v1/rss.go
deleted file mode 100644
index 8d8da824d9641..0000000000000
--- a/api/v1/rss.go
+++ /dev/null
@@ -1,211 +0,0 @@
-package v1
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "strconv"
- "strings"
- "time"
-
- "github.com/gorilla/feeds"
- "github.com/labstack/echo/v4"
- "github.com/pkg/errors"
- "github.com/yuin/goldmark"
-
- "github.com/usememos/memos/internal/util"
- "github.com/usememos/memos/store"
-)
-
-const maxRSSItemCount = 100
-const maxRSSItemTitleLength = 100
-
-func (s *APIV1Service) registerRSSRoutes(g *echo.Group) {
- g.GET("/explore/rss.xml", s.GetExploreRSS)
- g.GET("/u/:id/rss.xml", s.GetUserRSS)
-}
-
-// GetExploreRSS godoc
-//
-// @Summary Get RSS
-// @Tags rss
-// @Produce xml
-// @Success 200 {object} nil "RSS"
-// @Failure 500 {object} nil "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
-// @Router /explore/rss.xml [GET]
-func (s *APIV1Service) GetExploreRSS(c echo.Context) error {
- ctx := c.Request().Context()
- systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
- }
-
- normalStatus := store.Normal
- memoFind := store.FindMemo{
- RowStatus: &normalStatus,
- VisibilityList: []store.Visibility{store.Public},
- }
- memoList, err := s.Store.ListMemos(ctx, &memoFind)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
- }
-
- baseURL := c.Scheme() + "://" + c.Request().Host
- rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, systemCustomizedProfile)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
- }
- c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
- return c.String(http.StatusOK, rss)
-}
-
-// GetUserRSS godoc
-//
-// @Summary Get RSS for a user
-// @Tags rss
-// @Produce xml
-// @Param id path int true "User ID"
-// @Success 200 {object} nil "RSS"
-// @Failure 400 {object} nil "User id is not a number"
-// @Failure 500 {object} nil "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
-// @Router /u/{id}/rss.xml [GET]
-func (s *APIV1Service) GetUserRSS(c echo.Context) error {
- ctx := c.Request().Context()
- id, err := util.ConvertStringToInt32(c.Param("id"))
- if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "User id is not a number").SetInternal(err)
- }
-
- systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
- }
-
- normalStatus := store.Normal
- memoFind := store.FindMemo{
- CreatorID: &id,
- RowStatus: &normalStatus,
- VisibilityList: []store.Visibility{store.Public},
- }
- memoList, err := s.Store.ListMemos(ctx, &memoFind)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
- }
-
- baseURL := c.Scheme() + "://" + c.Request().Host
- rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, systemCustomizedProfile)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
- }
- c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
- return c.String(http.StatusOK, rss)
-}
-
-func (s *APIV1Service) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string, profile *CustomizedProfile) (string, error) {
- feed := &feeds.Feed{
- Title: profile.Name,
- Link: &feeds.Link{Href: baseURL},
- Description: profile.Description,
- Created: time.Now(),
- }
-
- var itemCountLimit = util.Min(len(memoList), maxRSSItemCount)
- feed.Items = make([]*feeds.Item, itemCountLimit)
- for i := 0; i < itemCountLimit; i++ {
- memo := memoList[i]
- feed.Items[i] = &feeds.Item{
- Title: getRSSItemTitle(memo.Content),
- Link: &feeds.Link{Href: baseURL + "/m/" + fmt.Sprintf("%d", memo.ID)},
- Description: getRSSItemDescription(memo.Content),
- Created: time.Unix(memo.CreatedTs, 0),
- Enclosure: &feeds.Enclosure{Url: baseURL + "/m/" + fmt.Sprintf("%d", memo.ID) + "/image"},
- }
- if len(memo.ResourceIDList) > 0 {
- resourceID := memo.ResourceIDList[0]
- resource, err := s.Store.GetResource(ctx, &store.FindResource{
- ID: &resourceID,
- })
- if err != nil {
- return "", err
- }
- if resource == nil {
- return "", errors.Errorf("Resource not found: %d", resourceID)
- }
- enclosure := feeds.Enclosure{}
- if resource.ExternalLink != "" {
- enclosure.Url = resource.ExternalLink
- } else {
- enclosure.Url = baseURL + "/o/r/" + fmt.Sprintf("%d", resource.ID)
- }
- enclosure.Length = strconv.Itoa(int(resource.Size))
- enclosure.Type = resource.Type
- feed.Items[i].Enclosure = &enclosure
- }
- }
-
- rss, err := feed.ToRss()
- if err != nil {
- return "", err
- }
- return rss, nil
-}
-
-func (s *APIV1Service) getSystemCustomizedProfile(ctx context.Context) (*CustomizedProfile, error) {
- systemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
- Name: SystemSettingCustomizedProfileName.String(),
- })
- if err != nil {
- return nil, err
- }
- customizedProfile := &CustomizedProfile{
- Name: "memos",
- LogoURL: "",
- Description: "",
- Locale: "en",
- Appearance: "system",
- ExternalURL: "",
- }
- if systemSetting != nil {
- if err := json.Unmarshal([]byte(systemSetting.Value), customizedProfile); err != nil {
- return nil, err
- }
- }
- return customizedProfile, nil
-}
-
-func getRSSItemTitle(content string) string {
- var title string
- if isTitleDefined(content) {
- title = strings.Split(content, "\n")[0][2:]
- } else {
- title = strings.Split(content, "\n")[0]
- var titleLengthLimit = util.Min(len(title), maxRSSItemTitleLength)
- if titleLengthLimit < len(title) {
- title = title[:titleLengthLimit] + "..."
- }
- }
- return title
-}
-
-func getRSSItemDescription(content string) string {
- var description string
- if isTitleDefined(content) {
- var firstLineEnd = strings.Index(content, "\n")
- description = strings.Trim(content[firstLineEnd+1:], " ")
- } else {
- description = content
- }
-
- // TODO: use our `./plugin/gomark` parser to handle markdown-like content.
- var buf bytes.Buffer
- if err := goldmark.Convert([]byte(description), &buf); err != nil {
- panic(err)
- }
- return buf.String()
-}
-
-func isTitleDefined(content string) bool {
- return strings.HasPrefix(content, "# ")
-}
diff --git a/api/v1/user_setting.go b/api/v1/user_setting.go
deleted file mode 100644
index 52ae45803cbb6..0000000000000
--- a/api/v1/user_setting.go
+++ /dev/null
@@ -1,174 +0,0 @@
-package v1
-
-import (
- "encoding/json"
- "net/http"
-
- "github.com/labstack/echo/v4"
- "github.com/pkg/errors"
- "golang.org/x/exp/slices"
-
- "github.com/usememos/memos/store"
-)
-
-type UserSettingKey string
-
-const (
- // UserSettingLocaleKey is the key type for user locale.
- UserSettingLocaleKey UserSettingKey = "locale"
- // UserSettingAppearanceKey is the key type for user appearance.
- UserSettingAppearanceKey UserSettingKey = "appearance"
- // UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
- UserSettingMemoVisibilityKey UserSettingKey = "memo-visibility"
- // UserSettingTelegramUserIDKey is the key type for telegram UserID of memos user.
- UserSettingTelegramUserIDKey UserSettingKey = "telegram-user-id"
-)
-
-// String returns the string format of UserSettingKey type.
-func (key UserSettingKey) String() string {
- switch key {
- case UserSettingLocaleKey:
- return "locale"
- case UserSettingAppearanceKey:
- return "appearance"
- case UserSettingMemoVisibilityKey:
- return "memo-visibility"
- case UserSettingTelegramUserIDKey:
- return "telegram-user-id"
- }
- return ""
-}
-
-var (
- UserSettingLocaleValue = []string{
- "ar",
- "de",
- "en",
- "es",
- "fr",
- "hi",
- "hr",
- "it",
- "ja",
- "ko",
- "nl",
- "pl",
- "pt-BR",
- "ru",
- "sl",
- "sv",
- "tr",
- "uk",
- "vi",
- "zh-Hans",
- "zh-Hant",
- }
- UserSettingAppearanceValue = []string{"system", "light", "dark"}
- UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public}
-)
-
-type UserSetting struct {
- UserID int32 `json:"userId"`
- Key UserSettingKey `json:"key"`
- Value string `json:"value"`
-}
-
-type UpsertUserSettingRequest struct {
- UserID int32 `json:"-"`
- Key UserSettingKey `json:"key"`
- Value string `json:"value"`
-}
-
-func (s *APIV1Service) registerUserSettingRoutes(g *echo.Group) {
- g.POST("/user/setting", s.UpsertUserSetting)
-}
-
-// UpsertUserSetting godoc
-//
-// @Summary Upsert user setting
-// @Tags user-setting
-// @Accept json
-// @Produce json
-// @Param body body UpsertUserSettingRequest true "Request object."
-// @Success 200 {object} store.UserSetting "Created user setting"
-// @Failure 400 {object} nil "Malformatted post user setting upsert request | Invalid user setting format"
-// @Failure 401 {object} nil "Missing auth session"
-// @Failure 500 {object} nil "Failed to upsert user setting"
-// @Router /api/v1/user/setting [POST]
-func (s *APIV1Service) UpsertUserSetting(c echo.Context) error {
- ctx := c.Request().Context()
- userID, ok := c.Get(userIDContextKey).(int32)
- if !ok {
- return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
- }
-
- userSettingUpsert := &UpsertUserSettingRequest{}
- 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, &store.UserSetting{
- UserID: userID,
- Key: userSettingUpsert.Key.String(),
- Value: userSettingUpsert.Value,
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err)
- }
-
- userSettingMessage := convertUserSettingFromStore(userSetting)
- return c.JSON(http.StatusOK, userSettingMessage)
-}
-
-func (upsert UpsertUserSettingRequest) Validate() error {
- if upsert.Key == UserSettingLocaleKey {
- localeValue := "en"
- err := json.Unmarshal([]byte(upsert.Value), &localeValue)
- if err != nil {
- return errors.New("failed to unmarshal user setting locale value")
- }
- if !slices.Contains(UserSettingLocaleValue, localeValue) {
- return errors.New("invalid user setting locale value")
- }
- } else if upsert.Key == UserSettingAppearanceKey {
- appearanceValue := "system"
- err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
- if err != nil {
- return errors.New("failed to unmarshal user setting appearance value")
- }
- if !slices.Contains(UserSettingAppearanceValue, appearanceValue) {
- return errors.New("invalid user setting appearance value")
- }
- } else if upsert.Key == UserSettingMemoVisibilityKey {
- memoVisibilityValue := Private
- err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue)
- if err != nil {
- return errors.New("failed to unmarshal user setting memo visibility value")
- }
- if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) {
- return errors.New("invalid user setting memo visibility value")
- }
- } else if upsert.Key == UserSettingTelegramUserIDKey {
- var key string
- err := json.Unmarshal([]byte(upsert.Value), &key)
- if err != nil {
- return errors.New("invalid user setting telegram user id value")
- }
- } else {
- return errors.New("invalid user setting key")
- }
-
- return nil
-}
-
-func convertUserSettingFromStore(userSetting *store.UserSetting) *UserSetting {
- return &UserSetting{
- UserID: userSetting.UserID,
- Key: UserSettingKey(userSetting.Key),
- Value: userSetting.Value,
- }
-}
diff --git a/api/v2/acl_config.go b/api/v2/acl_config.go
deleted file mode 100644
index 489bc50a0267e..0000000000000
--- a/api/v2/acl_config.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package v2
-
-import "strings"
-
-var authenticationAllowlistMethods = map[string]bool{
- "/memos.api.v2.SystemService/GetSystemInfo": true,
- "/memos.api.v2.UserService/GetUser": true,
- "/memos.api.v2.MemoService/ListMemos": true,
-}
-
-// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication.
-func isUnauthorizeAllowedMethod(fullMethodName string) bool {
- if strings.HasPrefix(fullMethodName, "/grpc.reflection") {
- return true
- }
- return authenticationAllowlistMethods[fullMethodName]
-}
-
-var allowedMethodsOnlyForAdmin = map[string]bool{
- "/memos.api.v2.UserService/CreateUser": true,
-}
-
-// isOnlyForAdminAllowedMethod returns true if the method is allowed to be called only by admin.
-func isOnlyForAdminAllowedMethod(methodName string) bool {
- return allowedMethodsOnlyForAdmin[methodName]
-}
diff --git a/api/v2/memo_service.go b/api/v2/memo_service.go
deleted file mode 100644
index 2b2bac5aa8f71..0000000000000
--- a/api/v2/memo_service.go
+++ /dev/null
@@ -1,266 +0,0 @@
-package v2
-
-import (
- "context"
-
- "github.com/google/cel-go/cel"
- "github.com/pkg/errors"
- v1alpha1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/status"
-
- apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
- "github.com/usememos/memos/store"
-)
-
-func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMemoRequest) (*apiv2pb.CreateMemoResponse, error) {
- user, err := getCurrentUser(ctx, s.Store)
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to get user")
- }
- if user == nil {
- return nil, status.Errorf(codes.PermissionDenied, "permission denied")
- }
-
- create := &store.Memo{
- CreatorID: user.ID,
- Content: request.Content,
- Visibility: store.Visibility(request.Visibility.String()),
- }
- memo, err := s.Store.CreateMemo(ctx, create)
- if err != nil {
- return nil, err
- }
-
- response := &apiv2pb.CreateMemoResponse{
- Memo: convertMemoFromStore(memo),
- }
- return response, nil
-}
-
-func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemosRequest) (*apiv2pb.ListMemosResponse, error) {
- memoFind := &store.FindMemo{}
- if request.Filter != "" {
- filter, err := parseListMemosFilter(request.Filter)
- if err != nil {
- return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
- }
- if filter.Visibility != nil {
- memoFind.VisibilityList = []store.Visibility{*filter.Visibility}
- }
- if filter.CreatedTsBefore != nil {
- memoFind.CreatedTsBefore = filter.CreatedTsBefore
- }
- if filter.CreatedTsAfter != nil {
- memoFind.CreatedTsAfter = filter.CreatedTsAfter
- }
- }
- user, _ := getCurrentUser(ctx, s.Store)
- // If the user is not authenticated, only public memos are visible.
- if user == nil {
- memoFind.VisibilityList = []store.Visibility{store.Public}
- }
-
- if request.CreatorId != nil {
- memoFind.CreatorID = request.CreatorId
- }
-
- // Remove the private memos from the list if the user is not the creator.
- if user != nil && request.CreatorId != nil && *request.CreatorId != user.ID {
- var filteredVisibility []store.Visibility
- for _, v := range memoFind.VisibilityList {
- if v != store.Private {
- filteredVisibility = append(filteredVisibility, v)
- }
- }
- memoFind.VisibilityList = filteredVisibility
- }
-
- if request.PageSize != 0 {
- offset := int(request.Page * request.PageSize)
- limit := int(request.PageSize)
- memoFind.Offset = &offset
- memoFind.Limit = &limit
- }
- memos, err := s.Store.ListMemos(ctx, memoFind)
- if err != nil {
- return nil, err
- }
-
- memoMessages := make([]*apiv2pb.Memo, len(memos))
- for i, memo := range memos {
- memoMessages[i] = convertMemoFromStore(memo)
- }
-
- response := &apiv2pb.ListMemosResponse{
- Memos: memoMessages,
- }
- return response, nil
-}
-
-func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv2pb.GetMemoRequest) (*apiv2pb.GetMemoResponse, error) {
- memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
- ID: &request.Id,
- })
- if err != nil {
- return nil, err
- }
- if memo == nil {
- return nil, status.Errorf(codes.NotFound, "memo not found")
- }
- if memo.Visibility != store.Public {
- user, err := getCurrentUser(ctx, s.Store)
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to get user")
- }
- if user == nil {
- return nil, status.Errorf(codes.PermissionDenied, "permission denied")
- }
- if memo.Visibility == store.Private && memo.CreatorID != user.ID {
- return nil, status.Errorf(codes.PermissionDenied, "permission denied")
- }
- }
-
- response := &apiv2pb.GetMemoResponse{
- Memo: convertMemoFromStore(memo),
- }
- return response, nil
-}
-
-func (s *APIV2Service) CreateMemoComment(ctx context.Context, request *apiv2pb.CreateMemoCommentRequest) (*apiv2pb.CreateMemoCommentResponse, error) {
- // Create the comment memo first.
- createMemoResponse, err := s.CreateMemo(ctx, request.Create)
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to create memo")
- }
-
- // Build the relation between the comment memo and the original memo.
- memo := createMemoResponse.Memo
- _, err = s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
- MemoID: memo.Id,
- RelatedMemoID: request.Id,
- Type: store.MemoRelationComment,
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to create memo relation")
- }
-
- response := &apiv2pb.CreateMemoCommentResponse{
- Memo: memo,
- }
- return response, nil
-}
-
-func (s *APIV2Service) ListMemoComments(ctx context.Context, request *apiv2pb.ListMemoCommentsRequest) (*apiv2pb.ListMemoCommentsResponse, error) {
- memoRelationComment := store.MemoRelationComment
- memoRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
- RelatedMemoID: &request.Id,
- Type: &memoRelationComment,
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to list memo relations")
- }
-
- var memos []*apiv2pb.Memo
- for _, memoRelation := range memoRelations {
- memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
- ID: &memoRelation.MemoID,
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to get memo")
- }
- if memo != nil {
- memos = append(memos, convertMemoFromStore(memo))
- }
- }
-
- response := &apiv2pb.ListMemoCommentsResponse{
- Memos: memos,
- }
- return response, nil
-}
-
-// ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter.
-var ListMemosFilterCELAttributes = []cel.EnvOption{
- cel.Variable("visibility", cel.StringType),
- cel.Variable("created_ts_before", cel.IntType),
- cel.Variable("created_ts_after", cel.IntType),
-}
-
-type ListMemosFilter struct {
- Visibility *store.Visibility
- CreatedTsBefore *int64
- CreatedTsAfter *int64
-}
-
-func parseListMemosFilter(expression string) (*ListMemosFilter, error) {
- e, err := cel.NewEnv(ListMemosFilterCELAttributes...)
- if err != nil {
- return nil, err
- }
- ast, issues := e.Compile(expression)
- if issues != nil {
- return nil, errors.Errorf("found issue %v", issues)
- }
- filter := &ListMemosFilter{}
- expr, err := cel.AstToParsedExpr(ast)
- if err != nil {
- return nil, err
- }
- callExpr := expr.GetExpr().GetCallExpr()
- findField(callExpr, filter)
- return filter, nil
-}
-
-func findField(callExpr *v1alpha1.Expr_Call, filter *ListMemosFilter) {
- if len(callExpr.Args) == 2 {
- idExpr := callExpr.Args[0].GetIdentExpr()
- if idExpr != nil {
- if idExpr.Name == "visibility" {
- visibility := store.Visibility(callExpr.Args[1].GetConstExpr().GetStringValue())
- filter.Visibility = &visibility
- }
- if idExpr.Name == "created_ts_before" {
- createdTsBefore := callExpr.Args[1].GetConstExpr().GetInt64Value()
- filter.CreatedTsBefore = &createdTsBefore
- }
- if idExpr.Name == "created_ts_after" {
- createdTsAfter := callExpr.Args[1].GetConstExpr().GetInt64Value()
- filter.CreatedTsAfter = &createdTsAfter
- }
- return
- }
- }
- for _, arg := range callExpr.Args {
- callExpr := arg.GetCallExpr()
- if callExpr != nil {
- findField(callExpr, filter)
- }
- }
-}
-
-func convertMemoFromStore(memo *store.Memo) *apiv2pb.Memo {
- return &apiv2pb.Memo{
- Id: int32(memo.ID),
- RowStatus: convertRowStatusFromStore(memo.RowStatus),
- CreatedTs: memo.CreatedTs,
- UpdatedTs: memo.UpdatedTs,
- CreatorId: int32(memo.CreatorID),
- Content: memo.Content,
- Visibility: convertVisibilityFromStore(memo.Visibility),
- Pinned: memo.Pinned,
- }
-}
-
-func convertVisibilityFromStore(visibility store.Visibility) apiv2pb.Visibility {
- switch visibility {
- case store.Private:
- return apiv2pb.Visibility_PRIVATE
- case store.Protected:
- return apiv2pb.Visibility_PROTECTED
- case store.Public:
- return apiv2pb.Visibility_PUBLIC
- default:
- return apiv2pb.Visibility_VISIBILITY_UNSPECIFIED
- }
-}
diff --git a/api/v2/system_service.go b/api/v2/system_service.go
deleted file mode 100644
index af37a22c734c8..0000000000000
--- a/api/v2/system_service.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package v2
-
-import (
- "context"
- "strconv"
-
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/status"
-
- apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
- "github.com/usememos/memos/store"
-)
-
-func (s *APIV2Service) GetSystemInfo(ctx context.Context, _ *apiv2pb.GetSystemInfoRequest) (*apiv2pb.GetSystemInfoResponse, error) {
- defaultSystemInfo := &apiv2pb.SystemInfo{}
-
- // Get the database size if the user is a host.
- currentUser, err := getCurrentUser(ctx, s.Store)
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
- }
- if currentUser != nil && currentUser.Role == store.RoleHost {
- size, err := s.Store.GetCurrentDBSize(ctx)
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to get db size: %v", err)
- }
- defaultSystemInfo.DbSize = size
- }
-
- response := &apiv2pb.GetSystemInfoResponse{
- SystemInfo: defaultSystemInfo,
- }
- return response, nil
-}
-
-func (s *APIV2Service) UpdateSystemInfo(ctx context.Context, request *apiv2pb.UpdateSystemInfoRequest) (*apiv2pb.UpdateSystemInfoResponse, error) {
- user, err := getCurrentUser(ctx, s.Store)
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
- }
- if user.Role != store.RoleHost {
- return nil, status.Errorf(codes.PermissionDenied, "permission denied")
- }
- if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
- return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
- }
-
- // Update system settings.
- for _, path := range request.UpdateMask.Paths {
- if path == "allow_registration" {
- _, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
- Name: "allow-signup",
- Value: strconv.FormatBool(request.SystemInfo.AllowRegistration),
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to update allow_registration system setting: %v", err)
- }
- } else if path == "disable_password_login" {
- _, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
- Name: "disable-password-login",
- Value: strconv.FormatBool(request.SystemInfo.DisablePasswordLogin),
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to update disable_password_login system setting: %v", err)
- }
- } else if path == "additional_script" {
- _, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
- Name: "additional-script",
- Value: request.SystemInfo.AdditionalScript,
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to update additional_script system setting: %v", err)
- }
- } else if path == "additional_style" {
- _, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
- Name: "additional-style",
- Value: request.SystemInfo.AdditionalStyle,
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to update additional_style system setting: %v", err)
- }
- }
- }
-
- systemInfo, err := s.GetSystemInfo(ctx, &apiv2pb.GetSystemInfoRequest{})
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to get system info: %v", err)
- }
- return &apiv2pb.UpdateSystemInfoResponse{
- SystemInfo: systemInfo.SystemInfo,
- }, nil
-}
diff --git a/api/v2/tag_service.go b/api/v2/tag_service.go
deleted file mode 100644
index f1cd8561a6e1b..0000000000000
--- a/api/v2/tag_service.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package v2
-
-import (
- "context"
-
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/status"
-
- apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
- "github.com/usememos/memos/store"
-)
-
-func (s *APIV2Service) UpsertTag(ctx context.Context, request *apiv2pb.UpsertTagRequest) (*apiv2pb.UpsertTagResponse, error) {
- user, err := getCurrentUser(ctx, s.Store)
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to get user")
- }
-
- tag, err := s.Store.UpsertTag(ctx, &store.Tag{
- Name: request.Name,
- CreatorID: user.ID,
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err)
- }
-
- return &apiv2pb.UpsertTagResponse{
- Tag: convertTagFromStore(tag),
- }, nil
-}
-
-func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRequest) (*apiv2pb.ListTagsResponse, error) {
- tags, err := s.Store.ListTags(ctx, &store.FindTag{
- CreatorID: request.CreatorId,
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
- }
-
- response := &apiv2pb.ListTagsResponse{}
- for _, tag := range tags {
- response.Tags = append(response.Tags, convertTagFromStore(tag))
- }
- return response, nil
-}
-
-func (s *APIV2Service) DeleteTag(ctx context.Context, request *apiv2pb.DeleteTagRequest) (*apiv2pb.DeleteTagResponse, error) {
- err := s.Store.DeleteTag(ctx, &store.DeleteTag{
- Name: request.Tag.Name,
- CreatorID: request.Tag.CreatorId,
- })
- if err != nil {
- return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err)
- }
-
- return &apiv2pb.DeleteTagResponse{}, nil
-}
-
-func convertTagFromStore(tag *store.Tag) *apiv2pb.Tag {
- return &apiv2pb.Tag{
- Name: tag.Name,
- CreatorId: int32(tag.CreatorID),
- }
-}
diff --git a/cmd/memos.go b/bin/memos/main.go
similarity index 71%
rename from cmd/memos.go
rename to bin/memos/main.go
index c391d35e26c3d..9669d8188f661 100644
--- a/cmd/memos.go
+++ b/bin/memos/main.go
@@ -1,8 +1,9 @@
-package cmd
+package main
import (
"context"
"fmt"
+ "log/slog"
"net/http"
"os"
"os/signal"
@@ -10,12 +11,10 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
- "go.uber.org/zap"
- "github.com/usememos/memos/internal/log"
+ "github.com/usememos/memos/internal/jobs"
"github.com/usememos/memos/server"
_profile "github.com/usememos/memos/server/profile"
- "github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store/db"
)
@@ -32,14 +31,14 @@ const (
)
var (
- profile *_profile.Profile
- mode string
- addr string
- port int
- data string
- driver string
- dsn string
- enableMetric bool
+ profile *_profile.Profile
+ mode string
+ addr string
+ port int
+ data string
+ driver string
+ dsn string
+ serveFrontend bool
rootCmd = &cobra.Command{
Use: "memos",
@@ -49,29 +48,27 @@ var (
dbDriver, err := db.NewDBDriver(profile)
if err != nil {
cancel()
- log.Error("failed to create db driver", zap.Error(err))
+ slog.Error("failed to create db driver", err)
return
}
if err := dbDriver.Migrate(ctx); err != nil {
cancel()
- log.Error("failed to migrate db", zap.Error(err))
+ slog.Error("failed to migrate database", err)
return
}
- store := store.New(dbDriver, profile)
- s, err := server.NewServer(ctx, profile, store)
- if err != nil {
+ storeInstance := store.New(dbDriver, profile)
+ if err := storeInstance.MigrateManually(ctx); err != nil {
cancel()
- log.Error("failed to create server", zap.Error(err))
+ slog.Error("failed to migrate manually", err)
return
}
- if profile.Metric {
- println("metric collection is enabled")
- // nolint
- metric.NewMetricClient(s.ID, *profile)
- } else {
- println("metric collection is disabled")
+ s, err := server.NewServer(ctx, profile, storeInstance)
+ if err != nil {
+ cancel()
+ slog.Error("failed to create server", err)
+ return
}
c := make(chan os.Signal, 1)
@@ -80,17 +77,19 @@ var (
// which is taken as the graceful shutdown signal for many systems, eg., Kubernetes, Gunicorn.
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
- sig := <-c
- log.Info(fmt.Sprintf("%s received.\n", sig.String()))
+ <-c
s.Shutdown(ctx)
cancel()
}()
printGreetings()
+ // update (pre-sign) object storage links if applicable
+ go jobs.RunPreSignLinks(ctx, storeInstance)
+
if err := s.Start(ctx); err != nil {
if err != http.ErrServerClosed {
- log.Error("failed to start server", zap.Error(err))
+ slog.Error("failed to start server", err)
cancel()
}
}
@@ -102,7 +101,6 @@ var (
)
func Execute() error {
- defer log.Sync()
return rootCmd.Execute()
}
@@ -115,7 +113,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&data, "data", "d", "", "data directory")
rootCmd.PersistentFlags().StringVarP(&driver, "driver", "", "", "database driver")
rootCmd.PersistentFlags().StringVarP(&dsn, "dsn", "", "", "database source name(aka. DSN)")
- rootCmd.PersistentFlags().BoolVarP(&enableMetric, "metric", "", true, "allow metric collection")
+ rootCmd.PersistentFlags().BoolVarP(&serveFrontend, "frontend", "", true, "serve frontend files")
err := viper.BindPFlag("mode", rootCmd.PersistentFlags().Lookup("mode"))
if err != nil {
@@ -141,7 +139,7 @@ func init() {
if err != nil {
panic(err)
}
- err = viper.BindPFlag("metric", rootCmd.PersistentFlags().Lookup("metric"))
+ err = viper.BindPFlag("frontend", rootCmd.PersistentFlags().Lookup("frontend"))
if err != nil {
panic(err)
}
@@ -150,7 +148,7 @@ func init() {
viper.SetDefault("driver", "sqlite")
viper.SetDefault("addr", "")
viper.SetDefault("port", 8081)
- viper.SetDefault("metric", true)
+ viper.SetDefault("frontend", true)
viper.SetEnvPrefix("memos")
}
@@ -163,17 +161,18 @@ func initConfig() {
return
}
- println("---")
- println("Server profile")
- println("data:", profile.Data)
- println("dsn:", profile.DSN)
- println("addr:", profile.Addr)
- println("port:", profile.Port)
- println("mode:", profile.Mode)
- println("driver:", profile.Driver)
- println("version:", profile.Version)
- println("metric:", profile.Metric)
- println("---")
+ fmt.Printf(`---
+Server profile
+version: %s
+data: %s
+dsn: %s
+addr: %s
+port: %d
+mode: %s
+driver: %s
+frontend: %t
+---
+`, profile.Version, profile.Data, profile.DSN, profile.Addr, profile.Port, profile.Mode, profile.Driver, profile.Frontend)
}
func printGreetings() {
@@ -183,9 +182,17 @@ func printGreetings() {
} else {
fmt.Printf("Version %s has been started on address '%s' and port %d\n", profile.Version, profile.Addr, profile.Port)
}
- println("---")
- println("See more in:")
- fmt.Printf("👉Website: %s\n", "https://usememos.com")
- fmt.Printf("👉GitHub: %s\n", "https://github.com/usememos/memos")
- println("---")
+ fmt.Printf(`---
+See more in:
+👉Website: %s
+👉GitHub: %s
+---
+`, "https://usememos.com", "https://github.com/usememos/memos")
+}
+
+func main() {
+ err := Execute()
+ if err != nil {
+ panic(err)
+ }
}
diff --git a/cmd/copydb.go b/cmd/copydb.go
deleted file mode 100644
index fed5c1963ed7a..0000000000000
--- a/cmd/copydb.go
+++ /dev/null
@@ -1,383 +0,0 @@
-package cmd
-
-import (
- "context"
- "fmt"
- "strings"
-
- "github.com/pkg/errors"
- "github.com/spf13/cobra"
-
- _profile "github.com/usememos/memos/server/profile"
- "github.com/usememos/memos/store"
- "github.com/usememos/memos/store/db"
-)
-
-var (
- copydbCmdFlagFrom = "from"
- copydbCmd = &cobra.Command{
- Use: "copydb", // `copydb` is a shortened for 'copy database'
- Short: "Copy data between db drivers",
- Run: func(cmd *cobra.Command, _ []string) {
- s, err := cmd.Flags().GetString(copydbCmdFlagFrom)
- if err != nil {
- println("fail to get from driver DSN")
- println(err)
- return
- }
- ss := strings.Split(s, "://")
- if len(ss) != 2 {
- println("fail to parse from driver DSN, should be like 'sqlite://memos_prod.db' or 'mysql://user:pass@tcp(host)/memos'")
- return
- }
-
- fromProfile := &_profile.Profile{Driver: ss[0], DSN: ss[1]}
-
- err = copydb(fromProfile, profile)
- if err != nil {
- fmt.Printf("fail to copydb: %s\n", err)
- return
- }
-
- println("done")
- },
- }
-)
-
-func init() {
- copydbCmd.Flags().String(copydbCmdFlagFrom, "sqlite://memos_prod.db", "From driver DSN")
-
- rootCmd.AddCommand(copydbCmd)
-}
-
-func copydb(fromProfile, toProfile *_profile.Profile) error {
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- toDriver, err := db.NewDBDriver(toProfile)
- if err != nil {
- return errors.Wrap(err, "fail to create `to` driver")
- }
-
- if err := toDriver.Migrate(ctx); err != nil {
- return errors.Wrap(err, "fail to migrate db")
- }
-
- fromDriver, err := db.NewDBDriver(fromProfile)
- if err != nil {
- return errors.Wrap(err, "fail to create `from` driver")
- }
-
- // Register here if any table is added
- copyMap := map[string]func(context.Context, store.Driver, store.Driver) error{
- "activity": copyActivity,
- "idp": copyIdp,
- "memo": copyMemo,
- "memo_organizer": copyMemoOrganizer,
- "memo_relation": copyMemoRelation,
- "resource": copyResource,
- "storage": copyStorage,
- "system_setting": copySystemSettings,
- "tag": copyTag,
- "user": copyUser,
- "user_setting": copyUserSettings,
- }
-
- toDb := toDriver.GetDB()
- for table := range copyMap {
- println("Checking " + table + "...")
- var cnt int
- err := toDb.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+table).Scan(&cnt)
- if err != nil {
- return errors.Wrapf(err, "fail to check '%s'", table)
- }
- if cnt > 0 {
- return errors.Errorf("table '%s' is not empty", table)
- }
- }
-
- for _, f := range copyMap {
- err = f(ctx, fromDriver, toDriver)
- if err != nil {
- return errors.Wrap(err, "fail to copy data")
- }
- }
-
- return nil
-}
-
-func copyActivity(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying Activity...")
- list, err := fromDriver.ListActivities(ctx, &store.FindActivity{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.CreateActivity(ctx, &store.Activity{
- ID: item.ID,
- CreatorID: item.CreatorID,
- CreatedTs: item.CreatedTs,
- Level: item.Level,
- Type: item.Type,
- Payload: item.Payload,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
-
-func copyIdp(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying IdentityProvider...")
- list, err := fromDriver.ListIdentityProviders(ctx, &store.FindIdentityProvider{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.CreateIdentityProvider(ctx, &store.IdentityProvider{
- ID: item.ID,
- Name: item.Name,
- Type: item.Type,
- IdentifierFilter: item.IdentifierFilter,
- Config: item.Config,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
-
-func copyMemo(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying Memo...")
- list, err := fromDriver.ListMemos(ctx, &store.FindMemo{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.CreateMemo(ctx, &store.Memo{
- ID: item.ID,
- CreatorID: item.CreatorID,
- CreatedTs: item.CreatedTs,
- UpdatedTs: item.UpdatedTs,
- RowStatus: item.RowStatus,
- Content: item.Content,
- Visibility: item.Visibility,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
-
-func copyMemoOrganizer(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying MemoOrganizer...")
- list, err := fromDriver.ListMemoOrganizer(ctx, &store.FindMemoOrganizer{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.UpsertMemoOrganizer(ctx, &store.MemoOrganizer{
- MemoID: item.MemoID,
- UserID: item.UserID,
- Pinned: item.Pinned,
- })
- if err != nil {
- return err
- }
- }
- println("\tDONE")
- return nil
-}
-
-func copyMemoRelation(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying MemoRelation...")
- list, err := fromDriver.ListMemoRelations(ctx, &store.FindMemoRelation{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.UpsertMemoRelation(ctx, &store.MemoRelation{
- MemoID: item.MemoID,
- RelatedMemoID: item.RelatedMemoID,
- Type: item.Type,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
-
-func copyResource(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying Resource...")
- list, err := fromDriver.ListResources(ctx, &store.FindResource{GetBlob: true})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.CreateResource(ctx, &store.Resource{
- ID: item.ID,
- CreatorID: item.CreatorID,
- CreatedTs: item.CreatedTs,
- UpdatedTs: item.UpdatedTs,
- Filename: item.Filename,
- Blob: item.Blob,
- ExternalLink: item.ExternalLink,
- Type: item.Type,
- Size: item.Size,
- InternalPath: item.InternalPath,
- MemoID: item.MemoID,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
-
-func copyStorage(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying Storage...")
- list, err := fromDriver.ListStorages(ctx, &store.FindStorage{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.CreateStorage(ctx, &store.Storage{
- ID: item.ID,
- Name: item.Name,
- Type: item.Type,
- Config: item.Config,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
-
-func copySystemSettings(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying SystemSettings...")
- list, err := fromDriver.ListSystemSettings(ctx, &store.FindSystemSetting{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.UpsertSystemSetting(ctx, &store.SystemSetting{
- Name: item.Name,
- Value: item.Value,
- Description: item.Description,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
-
-func copyTag(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying Tag...")
- list, err := fromDriver.ListTags(ctx, &store.FindTag{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.UpsertTag(ctx, &store.Tag{
- Name: item.Name,
- CreatorID: item.CreatorID,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
-
-func copyUser(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying User...")
- list, err := fromDriver.ListUsers(ctx, &store.FindUser{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.CreateUser(ctx, &store.User{
- ID: item.ID,
- CreatedTs: item.CreatedTs,
- UpdatedTs: item.UpdatedTs,
- RowStatus: item.RowStatus,
- Username: item.Username,
- Role: item.Role,
- Email: item.Email,
- Nickname: item.Nickname,
- PasswordHash: item.PasswordHash,
- AvatarURL: item.AvatarURL,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
-
-func copyUserSettings(ctx context.Context, fromDriver, toDriver store.Driver) error {
- println("Copying UserSettings...")
- list, err := fromDriver.ListUserSettings(ctx, &store.FindUserSetting{})
- if err != nil {
- return err
- }
-
- fmt.Printf("\tTotal %d records\n", len(list))
- for _, item := range list {
- _, err := toDriver.UpsertUserSetting(ctx, &store.UserSetting{
- Key: item.Key,
- Value: item.Value,
- UserID: item.UserID,
- })
- if err != nil {
- return err
- }
- }
-
- println("\tDONE")
- return nil
-}
diff --git a/cmd/mvrss.go b/cmd/mvrss.go
deleted file mode 100644
index 402b42cb0e4c9..0000000000000
--- a/cmd/mvrss.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package cmd
-
-import (
- "context"
- "fmt"
- "os"
- "time"
-
- "github.com/spf13/cobra"
-
- "github.com/usememos/memos/store"
- "github.com/usememos/memos/store/db/sqlite"
-)
-
-var (
- mvrssCmdFlagFrom = "from"
- mvrssCmdFlagTo = "to"
- mvrssCmd = &cobra.Command{
- Use: "mvrss", // `mvrss` is a shortened for 'means move resource'
- Short: "Move resource between storage",
- Run: func(cmd *cobra.Command, _ []string) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- from, err := cmd.Flags().GetString(mvrssCmdFlagFrom)
- if err != nil {
- fmt.Printf("failed to get from storage, error: %+v\n", err)
- return
- }
-
- to, err := cmd.Flags().GetString(mvrssCmdFlagTo)
- if err != nil {
- fmt.Printf("failed to get to storage, error: %+v\n", err)
- return
- }
-
- if from != "local" || to != "db" {
- fmt.Printf("only local=>db be supported currently\n")
- return
- }
-
- driver, err := sqlite.NewDB(profile)
- if err != nil {
- fmt.Printf("failed to create db driver, error: %+v\n", err)
- return
- }
- if err := driver.Migrate(ctx); err != nil {
- fmt.Printf("failed to migrate db, error: %+v\n", err)
- return
- }
-
- s := store.New(driver, profile)
- resources, err := s.ListResources(ctx, &store.FindResource{})
- if err != nil {
- fmt.Printf("failed to list resources, error: %+v\n", err)
- return
- }
-
- var emptyString string
- for _, res := range resources {
- if res.InternalPath == "" {
- continue
- }
-
- buf, err := os.ReadFile(res.InternalPath)
- if err != nil {
- fmt.Printf("Resource %5d failed to read file: %s\n", res.ID, err)
- continue
- }
-
- if len(buf) != int(res.Size) {
- fmt.Printf("Resource %5d size of file %d != %d\n", res.ID, len(buf), res.Size)
- continue
- }
-
- update := store.UpdateResource{
- ID: res.ID,
- Blob: buf,
- InternalPath: &emptyString,
- }
- _, err = s.UpdateResource(ctx, &update)
- if err != nil {
- fmt.Printf("Resource %5d failed to update: %s\n", res.ID, err)
- continue
- }
-
- fmt.Printf("Resource %5d copy %12d bytes from %s\n", res.ID, len(buf), res.InternalPath)
- }
- println("done")
- },
- }
-)
-
-func init() {
- mvrssCmd.Flags().String(mvrssCmdFlagFrom, "local", "From storage")
- mvrssCmd.Flags().String(mvrssCmdFlagTo, "db", "To Storage")
-
- rootCmd.AddCommand(mvrssCmd)
-}
diff --git a/cmd/setup.go b/cmd/setup.go
deleted file mode 100644
index 3683b46c911c1..0000000000000
--- a/cmd/setup.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package cmd
-
-import (
- "context"
- "fmt"
- "time"
-
- "github.com/pkg/errors"
- "github.com/spf13/cobra"
- "golang.org/x/crypto/bcrypt"
-
- "github.com/usememos/memos/internal/util"
- "github.com/usememos/memos/store"
- "github.com/usememos/memos/store/db/sqlite"
-)
-
-var (
- setupCmdFlagHostUsername = "host-username"
- setupCmdFlagHostPassword = "host-password"
- setupCmd = &cobra.Command{
- Use: "setup",
- Short: "Make initial setup for memos",
- Run: func(cmd *cobra.Command, _ []string) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- hostUsername, err := cmd.Flags().GetString(setupCmdFlagHostUsername)
- if err != nil {
- fmt.Printf("failed to get owner username, error: %+v\n", err)
- return
- }
-
- hostPassword, err := cmd.Flags().GetString(setupCmdFlagHostPassword)
- if err != nil {
- fmt.Printf("failed to get owner password, error: %+v\n", err)
- return
- }
-
- driver, err := sqlite.NewDB(profile)
- if err != nil {
- fmt.Printf("failed to create db driver, error: %+v\n", err)
- return
- }
- if err := driver.Migrate(ctx); err != nil {
- fmt.Printf("failed to migrate db, error: %+v\n", err)
- return
- }
-
- store := store.New(driver, profile)
- if err := ExecuteSetup(ctx, store, hostUsername, hostPassword); err != nil {
- fmt.Printf("failed to setup, error: %+v\n", err)
- return
- }
- },
- }
-)
-
-func init() {
- setupCmd.Flags().String(setupCmdFlagHostUsername, "", "Owner username")
- setupCmd.Flags().String(setupCmdFlagHostPassword, "", "Owner password")
-
- rootCmd.AddCommand(setupCmd)
-}
-
-func ExecuteSetup(ctx context.Context, store *store.Store, hostUsername, hostPassword string) error {
- s := setupService{store: store}
- return s.Setup(ctx, hostUsername, hostPassword)
-}
-
-type setupService struct {
- store *store.Store
-}
-
-func (s setupService) Setup(ctx context.Context, hostUsername, hostPassword string) error {
- if err := s.makeSureHostUserNotExists(ctx); err != nil {
- return err
- }
-
- if err := s.createUser(ctx, hostUsername, hostPassword); err != nil {
- return errors.Wrap(err, "create user")
- }
- return nil
-}
-
-func (s setupService) makeSureHostUserNotExists(ctx context.Context) error {
- hostUserType := store.RoleHost
- existedHostUsers, err := s.store.ListUsers(ctx, &store.FindUser{Role: &hostUserType})
- if err != nil {
- return errors.Wrap(err, "find user list")
- }
-
- if len(existedHostUsers) != 0 {
- return errors.New("host user already exists")
- }
-
- return nil
-}
-
-func (s setupService) createUser(ctx context.Context, hostUsername, hostPassword string) error {
- userCreate := &store.User{
- Username: hostUsername,
- // The new signup user should be normal user by default.
- Role: store.RoleHost,
- Nickname: hostUsername,
- }
-
- if len(userCreate.Username) < 3 {
- return errors.New("username is too short, minimum length is 3")
- }
- if len(userCreate.Username) > 32 {
- return errors.New("username is too long, maximum length is 32")
- }
- if len(hostPassword) < 3 {
- return errors.New("password is too short, minimum length is 3")
- }
- if len(hostPassword) > 512 {
- return errors.New("password is too long, maximum length is 512")
- }
- if len(userCreate.Nickname) > 64 {
- return errors.New("nickname is too long, maximum length is 64")
- }
- if userCreate.Email != "" {
- if len(userCreate.Email) > 256 {
- return errors.New("email is too long, maximum length is 256")
- }
- if !util.ValidateEmail(userCreate.Email) {
- return errors.New("invalid email format")
- }
- }
-
- passwordHash, err := bcrypt.GenerateFromPassword([]byte(hostPassword), bcrypt.DefaultCost)
- if err != nil {
- return errors.Wrap(err, "failed to hash password")
- }
-
- userCreate.PasswordHash = string(passwordHash)
- if _, err := s.store.CreateUser(ctx, userCreate); err != nil {
- return errors.Wrap(err, "failed to create user")
- }
-
- return nil
-}
diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml
deleted file mode 100644
index 2c1e07d978965..0000000000000
--- a/docker-compose.dev.yaml
+++ /dev/null
@@ -1,70 +0,0 @@
-services:
- db:
- image: mysql
- volumes:
- - ./.air/mysql:/var/lib/mysql
- api:
- image: cosmtrek/air
- working_dir: /work
- command: ["-c", "./scripts/.air.toml"]
- environment:
- - "MEMOS_DSN=root@tcp(db)/memos"
- - "MEMOS_DRIVER=mysql"
- volumes:
- - .:/work/
- - .air/go-build:/root/.cache/go-build
- - $HOME/go/pkg/:/go/pkg/ # Cache for go mod shared with the host
- web:
- image: node:18-alpine
- working_dir: /work
- depends_on: ["api"]
- ports: ["3001:3001"]
- environment: ["DEV_PROXY_SERVER=http://api:8081/"]
- entrypoint: ["/bin/sh", "-c"]
- command: ["corepack enable && pnpm install && pnpm dev"]
- volumes:
- - ./web:/work
- - ./.air/node_modules/:/work/node_modules/ # Cache for Node Modules
-
- # Services below are used for developers to run once
- #
- # You can just run `docker compose run --rm SERVICE_NAME` to use
- # For example:
- # To regenerate typescript code of gRPC proto
- # Just run `docker compose run --rm buf`
- #
- # All of theses services belongs to profile 'tools'
- # This will prevent to launch by normally `docker compose up` unexpectly
-
- # Generate typescript code of gRPC proto
- buf:
- profiles: ["tools"]
- image: bufbuild/buf
- working_dir: /work/proto
- command: generate
- volumes:
- - ./proto:/work/proto
- - ./web/src/types/:/work/web/src/types/
-
- # Do golang static code check before create PR
- golangci-lint:
- profiles: ["tools"]
- image: golangci/golangci-lint:v1.54.2
- working_dir: /work/
- entrypoint: golangci-lint
- command: run -v
- volumes:
- - $HOME/go/pkg/:/go/pkg/ # Cache for go mod shared with the host
- - .air/go-build:/root/.cache/go-build
- - .:/work/
-
- # run npm
- npm:
- profiles: ["tools"]
- image: node:18-alpine
- working_dir: /work
- environment: ["NPM_CONFIG_UPDATE_NOTIFIER=false"]
- entrypoint: "npm"
- volumes:
- - ./web:/work
- - ./.air/node_modules/:/work/node_modules/
diff --git a/docs/api/v1.md b/docs/api/v1.md
deleted file mode 100644
index 0514b97548d65..0000000000000
--- a/docs/api/v1.md
+++ /dev/null
@@ -1,1607 +0,0 @@
-# memos API
-
-A privacy-first, lightweight note-taking service.
-
-## Version: 1.0
-
-**Contact information:**
-API Support
-No frontend embeded.
+ + + diff --git a/server/embed_frontend.go b/server/embed_frontend.go deleted file mode 100644 index 497e5216e9e08..0000000000000 --- a/server/embed_frontend.go +++ /dev/null @@ -1,52 +0,0 @@ -package server - -import ( - "embed" - "io/fs" - "net/http" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - - "github.com/usememos/memos/internal/util" -) - -//go:embed dist -var embeddedFiles embed.FS - -func getFileSystem(path string) http.FileSystem { - fs, err := fs.Sub(embeddedFiles, path) - if err != nil { - panic(err) - } - - return http.FS(fs) -} - -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{ - Skipper: defaultAPIRequestSkipper, - HTML5: true, - Filesystem: getFileSystem("dist"), - })) - - assetsGroup := e.Group("assets") - assetsGroup.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) - } - }) - assetsGroup.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - Skipper: defaultAPIRequestSkipper, - HTML5: true, - Filesystem: getFileSystem("dist/assets"), - })) -} - -func defaultAPIRequestSkipper(c echo.Context) bool { - path := c.Request().URL.Path - return util.HasPrefixes(path, "/api", "/memos.api.v2") -} diff --git a/server/integration/telegram.go b/server/integration/telegram.go index 902935b12e547..20f85ac897d45 100644 --- a/server/integration/telegram.go +++ b/server/integration/telegram.go @@ -5,13 +5,23 @@ import ( "context" "encoding/json" "fmt" + "path/filepath" + "slices" "strconv" + "time" "unicode/utf16" + "github.com/lithammer/shortuuid/v4" "github.com/pkg/errors" + "github.com/yourselfhosted/gomark/ast" + "github.com/yourselfhosted/gomark/parser" + "github.com/yourselfhosted/gomark/parser/tokenizer" - apiv1 "github.com/usememos/memos/api/v1" "github.com/usememos/memos/plugin/telegram" + "github.com/usememos/memos/plugin/webhook" + storepb "github.com/usememos/memos/proto/gen/store" + apiv1 "github.com/usememos/memos/server/route/api/v1" + apiv2 "github.com/usememos/memos/server/route/api/v2" "github.com/usememos/memos/store" ) @@ -24,7 +34,12 @@ func NewTelegramHandler(store *store.Store) *TelegramHandler { } func (t *TelegramHandler) BotToken(ctx context.Context) string { - return t.store.GetSystemSettingValueWithDefault(ctx, apiv1.SystemSettingTelegramBotTokenName.String(), "") + if setting, err := t.store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ + Name: apiv1.SystemSettingTelegramBotTokenName.String(), + }); err == nil && setting != nil { + return setting.Value + } + return "" } const ( @@ -38,61 +53,80 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot, return errors.Wrap(err, "Failed to SendReplyMessage") } + messageSenderID := strconv.FormatInt(message.From.ID, 10) var creatorID int32 userSettingList, err := t.store.ListUserSettings(ctx, &store.FindUserSetting{ - Key: apiv1.UserSettingTelegramUserIDKey.String(), + Key: storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID, }) if err != nil { return errors.Wrap(err, "Failed to find userSettingList") } for _, userSetting := range userSettingList { - var value string - if err := json.Unmarshal([]byte(userSetting.Value), &value); err != nil { - continue - } - - if value == strconv.FormatInt(message.From.ID, 10) { - creatorID = userSetting.UserID + if userSetting.GetTelegramUserId() == messageSenderID { + creatorID = userSetting.UserId } } + // If creatorID is not found, ask the user to set the telegram userid in UserSetting of memos. if creatorID == 0 { _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Please set your telegram userid %d in UserSetting of memos", message.From.ID), nil) return err } create := &store.Memo{ - CreatorID: creatorID, - Visibility: store.Private, + ResourceName: shortuuid.New(), + CreatorID: creatorID, + Visibility: store.Private, } - if message.Text != nil { create.Content = convertToMarkdown(*message.Text, message.Entities) } - if message.Caption != nil { create.Content = convertToMarkdown(*message.Caption, message.CaptionEntities) } - if message.ForwardFromChat != nil { create.Content += fmt.Sprintf("\n\n[Message link](%s)", message.GetMessageLink()) } - memoMessage, err := t.store.CreateMemo(ctx, create) if err != nil { _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Failed to CreateMemo: %s", err), nil) return err } - // create resources + // Dynamically upsert tags from memo content. + nodes, err := parser.Parse(tokenizer.Tokenize(create.Content)) + if err != nil { + return errors.Wrap(err, "Failed to parse content") + } + tags := []string{} + apiv2.TraverseASTNodes(nodes, func(node ast.Node) { + if tagNode, ok := node.(*ast.Tag); ok { + tag := tagNode.Content + if !slices.Contains(tags, tag) { + tags = append(tags, tag) + } + } + }) + for _, tag := range tags { + _, err := t.store.UpsertTag(ctx, &store.Tag{ + Name: tag, + CreatorID: creatorID, + }) + if err != nil { + return errors.Wrap(err, "Failed to upsert tag") + } + } + + // Create memo related resources. for _, attachment := range attachments { // Fill the common field of create create := store.Resource{ - CreatorID: creatorID, - Filename: attachment.FileName, - Type: attachment.GetMimeType(), - Size: attachment.FileSize, - MemoID: &memoMessage.ID, + ResourceName: shortuuid.New(), + CreatorID: creatorID, + Filename: filepath.Base(attachment.FileName), + Type: attachment.GetMimeType(), + Size: attachment.FileSize, + MemoID: &memoMessage.ID, } err := apiv1.SaveResourceBlob(ctx, t.store, &create, bytes.NewReader(attachment.Data)) @@ -110,6 +144,7 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot, keyboard := generateKeyboardForMemoID(memoMessage.ID) _, err = bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Saved as %s Memo %d", memoMessage.Visibility, memoMessage.ID), keyboard) + _ = t.dispatchMemoRelatedWebhook(ctx, *memoMessage, "memos.memo.created") return err } @@ -121,6 +156,37 @@ func (t *TelegramHandler) CallbackQueryHandle(ctx context.Context, bot *telegram return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to parse callbackQuery.Data %s", callbackQuery.Data)) } + memo, err := t.store.GetMemo(ctx, &store.FindMemo{ + ID: &memoID, + }) + if err != nil { + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to call FindMemo %s", err)) + } + if memo == nil { + _, err = bot.EditMessage(ctx, callbackQuery.Message.Chat.ID, callbackQuery.Message.MessageID, fmt.Sprintf("Memo %d not found", memoID), nil) + if err != nil { + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to EditMessage %s", err)) + } + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Memo %d not found, possibly deleted elsewhere", memoID)) + } + + setting, err := t.store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ + Name: apiv1.SystemSettingDisablePublicMemosName.String(), + }) + if err != nil { + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to get workspace setting %s", err)) + } + if setting != nil && setting.Value != "" { + disablePublicMemo := false + err = json.Unmarshal([]byte(setting.Value), &disablePublicMemo) + if err != nil { + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to get workspace setting %s", err)) + } + if disablePublicMemo && visibility == store.Public { + return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to changing Memo %d to %s\n(workspace disallowed public memo)", memoID, visibility)) + } + } + update := store.UpdateMemo{ ID: memoID, Visibility: &visibility, @@ -136,7 +202,15 @@ func (t *TelegramHandler) CallbackQueryHandle(ctx context.Context, bot *telegram return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to EditMessage %s", err)) } - return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Success changing Memo %d to %s", memoID, visibility)) + err = bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Success changing Memo %d to %s", memoID, visibility)) + + memo, webhookErr := t.store.GetMemo(ctx, &store.FindMemo{ + ID: &memoID, + }) + if webhookErr == nil { + _ = t.dispatchMemoRelatedWebhook(ctx, *memo, "memos.memo.updated") + } + return err } func generateKeyboardForMemoID(id int32) [][]telegram.InlineKeyboardButton { @@ -184,6 +258,9 @@ func convertToMarkdown(text string, messageEntities []telegram.MessageEntity) st case telegram.TextLink: before = "[" after = fmt.Sprintf(`](%s)`, e.URL) + case telegram.Spoiler: + before = "||" + after = "||" } if before != "" { @@ -205,3 +282,78 @@ func convertToMarkdown(text string, messageEntities []telegram.MessageEntity) st return string(output) } + +func (t *TelegramHandler) dispatchMemoRelatedWebhook(ctx context.Context, memo store.Memo, activityType string) error { + webhooks, err := t.store.ListWebhooks(ctx, &store.FindWebhook{ + CreatorID: &memo.CreatorID, + }) + if err != nil { + return err + } + for _, hook := range webhooks { + payload := t.convertMemoToWebhookPayload(ctx, memo) + payload.ActivityType = activityType + payload.URL = hook.Url + err := webhook.Post(*payload) + if err != nil { + return errors.Wrap(err, "failed to post webhook") + } + } + return nil +} + +func (t *TelegramHandler) convertMemoToWebhookPayload(ctx context.Context, memo store.Memo) (payload *webhook.WebhookPayload) { + payload = &webhook.WebhookPayload{ + CreatorID: memo.CreatorID, + CreatedTs: time.Now().Unix(), + Memo: &webhook.Memo{ + ID: memo.ID, + CreatorID: memo.CreatorID, + CreatedTs: memo.CreatedTs, + UpdatedTs: memo.UpdatedTs, + Content: memo.Content, + Visibility: memo.Visibility.String(), + Pinned: memo.Pinned, + ResourceList: make([]*webhook.Resource, 0), + RelationList: make([]*webhook.MemoRelation, 0), + }, + } + + resourceList, err := t.store.ListResources(ctx, &store.FindResource{ + MemoID: &memo.ID, + }) + + if err != nil { + return payload + } + for _, resource := range resourceList { + payload.Memo.ResourceList = append(payload.Memo.ResourceList, &webhook.Resource{ + ID: resource.ID, + CreatorID: resource.CreatorID, + CreatedTs: resource.CreatedTs, + UpdatedTs: resource.UpdatedTs, + Filename: resource.Filename, + Type: resource.Type, + Size: resource.Size, + InternalPath: resource.InternalPath, + ExternalLink: resource.ExternalLink, + }) + } + + relationList, err := t.store.ListMemoRelations(ctx, &store.FindMemoRelation{ + MemoID: &memo.ID, + }) + + if err != nil { + return payload + } + + for _, relation := range relationList { + payload.Memo.RelationList = append(payload.Memo.RelationList, &webhook.MemoRelation{ + MemoID: relation.MemoID, + RelatedMemoID: relation.RelatedMemoID, + Type: string(relation.Type), + }) + } + return payload +} diff --git a/server/profile/profile.go b/server/profile/profile.go index 1c21e9df5fcf5..bd22193f8b7a2 100644 --- a/server/profile/profile.go +++ b/server/profile/profile.go @@ -30,8 +30,8 @@ type Profile struct { Driver string `json:"-"` // Version is the current version of server Version string `json:"version"` - // Metric indicate the metric collection is enabled or not - Metric bool `json:"-"` + // Frontend indicate the frontend is enabled or not + Frontend bool `json:"-"` } func (p *Profile) IsDev() bool { diff --git a/api/auth/auth.go b/server/route/api/auth/auth.go similarity index 98% rename from api/auth/auth.go rename to server/route/api/auth/auth.go index d846e6d8056eb..5a46d010551d6 100644 --- a/api/auth/auth.go +++ b/server/route/api/auth/auth.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) const ( diff --git a/api/v1/auth.go b/server/route/api/v1/auth.go similarity index 82% rename from api/v1/auth.go rename to server/route/api/v1/auth.go index ae15300b2b0a6..2912ffa744533 100644 --- a/api/v1/auth.go +++ b/server/route/api/v1/auth.go @@ -13,18 +13,14 @@ import ( "github.com/pkg/errors" "golang.org/x/crypto/bcrypt" - "github.com/usememos/memos/api/auth" "github.com/usememos/memos/internal/util" "github.com/usememos/memos/plugin/idp" "github.com/usememos/memos/plugin/idp/oauth2" storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/server/route/api/auth" "github.com/usememos/memos/store" ) -var ( - usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$") -) - type SignIn struct { Username string `json:"username"` Password string `json:"password"` @@ -64,25 +60,15 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) { // @Router /api/v1/auth/signin [POST] func (s *APIV1Service) SignIn(c echo.Context) error { ctx := c.Request().Context() - signin := &SignIn{} - - disablePasswordLoginSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{ - Name: SystemSettingDisablePasswordLoginName.String(), - }) + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err) } - if disablePasswordLoginSystemSetting != nil { - disablePasswordLogin := false - err = json.Unmarshal([]byte(disablePasswordLoginSystemSetting.Value), &disablePasswordLogin) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err) - } - if disablePasswordLogin { - return echo.NewHTTPError(http.StatusUnauthorized, "Password login is deactivated") - } + if workspaceGeneralSetting.DisallowPasswordLogin { + return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").SetInternal(err) } + signin := &SignIn{} if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err) } @@ -106,8 +92,11 @@ func (s *APIV1Service) SignIn(c echo.Context) error { } var expireAt time.Time + // Set cookie expiration to 100 years to make it persistent. + cookieExp := time.Now().AddDate(100, 0, 0) if !signin.Remember { expireAt = time.Now().Add(auth.AccessTokenDuration) + cookieExp = time.Now().Add(auth.CookieExpDuration) } accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, expireAt, []byte(s.Secret)) @@ -117,7 +106,6 @@ func (s *APIV1Service) SignIn(c echo.Context) error { if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err) } - cookieExp := time.Now().Add(auth.CookieExpDuration) setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp) userMessage := convertUserFromStore(user) return c.JSON(http.StatusOK, userMessage) @@ -188,21 +176,11 @@ func (s *APIV1Service) SignInSSO(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again") } if user == nil { - allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{ - Name: SystemSettingAllowSignUpName.String(), - }) + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err) } - - allowSignUpSettingValue := false - if allowSignUpSetting != nil { - err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err) - } - } - if !allowSignUpSettingValue { + if workspaceGeneralSetting.DisallowSignup { return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err) } @@ -252,33 +230,14 @@ func (s *APIV1Service) SignInSSO(c echo.Context) error { // @Success 200 {boolean} true "Sign-out success" // @Router /api/v1/auth/signout [POST] func (s *APIV1Service) SignOut(c echo.Context) error { - ctx := c.Request().Context() - accessToken := findAccessToken(c) + accessToken := FindAccessToken(c) userID, _ := getUserIDFromAccessToken(accessToken, s.Secret) - userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID) - // Auto remove the current access token from the user access tokens. - if err == nil && len(userAccessTokens) != 0 { - accessTokens := []*storepb.AccessTokensUserSetting_AccessToken{} - for _, userAccessToken := range userAccessTokens { - if accessToken != userAccessToken.AccessToken { - accessTokens = append(accessTokens, userAccessToken) - } - } - if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{ - UserId: userID, - Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS, - Value: &storepb.UserSetting_AccessTokens{ - AccessTokens: &storepb.AccessTokensUserSetting{ - AccessTokens: accessTokens, - }, - }, - }); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err) - } + err := removeAccessTokenAndCookies(c, s.Store, userID, accessToken) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to remove access token, err: %s", err)).SetInternal(err) } - removeAccessTokenAndCookies(c) return c.JSON(http.StatusOK, true) } @@ -310,7 +269,7 @@ func (s *APIV1Service) SignUp(c echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err) } - if !usernameMatcher.MatchString(strings.ToLower(signup.Username)) { + if !util.ResourceNameMatcher.MatchString(strings.ToLower(signup.Username)) { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err) } @@ -324,23 +283,16 @@ func (s *APIV1Service) SignUp(c echo.Context) error { // Change the default role to host if there is no host user. userCreate.Role = store.RoleHost } else { - allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{ - Name: SystemSettingAllowSignUpName.String(), - }) + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err) } - - allowSignUpSettingValue := false - if allowSignUpSetting != nil { - err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err) - } - } - if !allowSignUpSettingValue { + if workspaceGeneralSetting.DisallowSignup { return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err) } + if workspaceGeneralSetting.DisallowPasswordLogin { + return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").SetInternal(err) + } } passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost) @@ -376,7 +328,7 @@ func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store Description: "Account sign in", } userAccessTokens = append(userAccessTokens, &userAccessToken) - if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{ + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS, Value: &storepb.UserSetting_AccessTokens{ @@ -391,9 +343,15 @@ func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store } // removeAccessTokenAndCookies removes the jwt token from the cookies. -func removeAccessTokenAndCookies(c echo.Context) { +func removeAccessTokenAndCookies(c echo.Context, s *store.Store, userID int32, token string) error { + err := s.RemoveUserAccessToken(c.Request().Context(), userID, token) + if err != nil { + return err + } + cookieExp := time.Now().Add(-1 * time.Hour) setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp) + return nil } // setTokenCookie sets the token to the cookie. diff --git a/api/v1/common.go b/server/route/api/v1/common.go similarity index 100% rename from api/v1/common.go rename to server/route/api/v1/common.go diff --git a/api/v1/docs.go b/server/route/api/v1/docs.go similarity index 79% rename from api/v1/docs.go rename to server/route/api/v1/docs.go index 300abeb4cd405..6054a037a21f8 100644 --- a/api/v1/docs.go +++ b/server/route/api/v1/docs.go @@ -42,7 +42,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.SignIn" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.SignIn" } } ], @@ -87,7 +87,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.SSOSignIn" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.SSOSignIn" } } ], @@ -154,7 +154,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.SignUp" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.SignUp" } } ], @@ -199,7 +199,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/v1.IdentityProvider" + "$ref": "#/definitions/api_v1.IdentityProvider" } } }, @@ -226,7 +226,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateIdentityProviderRequest" + "$ref": "#/definitions/api_v1.CreateIdentityProviderRequest" } } ], @@ -354,7 +354,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpdateIdentityProviderRequest" + "$ref": "#/definitions/api_v1.UpdateIdentityProviderRequest" } } ], @@ -477,7 +477,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateMemoRequest" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.CreateMemoRequest" } } ], @@ -695,7 +695,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.PatchMemoRequest" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.PatchMemoRequest" } } ], @@ -747,7 +747,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpsertMemoOrganizerRequest" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest" } } ], @@ -838,7 +838,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpsertMemoRelationRequest" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest" } } ], @@ -889,7 +889,7 @@ const docTemplate = `{ { "enum": [ "REFERENCE", - "ADDITIONAL" + "COMMENT" ], "type": "string", "description": "Type of relation to remove", @@ -992,7 +992,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateResourceRequest" + "$ref": "#/definitions/api_v1.CreateResourceRequest" } } ], @@ -1116,7 +1116,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpdateResourceRequest" + "$ref": "#/definitions/api_v1.UpdateResourceRequest" } } ], @@ -1155,7 +1155,7 @@ const docTemplate = `{ "200": { "description": "System GetSystemStatus", "schema": { - "$ref": "#/definitions/v1.SystemStatus" + "$ref": "#/definitions/api_v1.SystemStatus" } }, "401": { @@ -1212,7 +1212,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateStorageRequest" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.CreateStorageRequest" } } ], @@ -1293,7 +1293,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpdateStorageRequest" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpdateStorageRequest" } } ], @@ -1331,7 +1331,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/v1.SystemSetting" + "$ref": "#/definitions/api_v1.SystemSetting" } } }, @@ -1361,17 +1361,11 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpsertSystemSettingRequest" + "$ref": "#/definitions/api_v1.UpsertSystemSettingRequest" } } ], "responses": { - "200": { - "description": "Created system setting", - "schema": { - "$ref": "#/definitions/store.SystemSetting" - } - }, "400": { "description": "Malformatted post system setting request | invalid system setting" }, @@ -1457,7 +1451,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpsertTagRequest" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertTagRequest" } } ], @@ -1499,7 +1493,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.DeleteTagRequest" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.DeleteTagRequest" } } ], @@ -1592,7 +1586,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateUserRequest" + "$ref": "#/definitions/api_v1.CreateUserRequest" } } ], @@ -1677,48 +1671,6 @@ const docTemplate = `{ } } }, - "/api/v1/user/setting": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "user-setting" - ], - "summary": "Upsert user setting", - "parameters": [ - { - "description": "Request object.", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.UpsertUserSettingRequest" - } - } - ], - "responses": { - "200": { - "description": "Created user setting", - "schema": { - "$ref": "#/definitions/github_com_usememos_memos_store.UserSetting" - } - }, - "400": { - "description": "Malformatted post user setting upsert request | Invalid user setting format" - }, - "401": { - "description": "Missing auth session" - }, - "500": { - "description": "Failed to upsert user setting" - } - } - } - }, "/api/v1/user/{id}": { "get": { "produces": [ @@ -1815,7 +1767,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpdateUserRequest" + "$ref": "#/definitions/api_v1.UpdateUserRequest" } } ], @@ -1841,32 +1793,13 @@ const docTemplate = `{ } } }, - "/explore/rss.xml": { - "get": { - "produces": [ - "text/xml" - ], - "tags": [ - "rss" - ], - "summary": "Get RSS", - "responses": { - "200": { - "description": "RSS" - }, - "500": { - "description": "Failed to get system customized profile | Failed to find memo list | Failed to generate rss" - } - } - } - }, "/o/get/GetImage": { "get": { "produces": [ "GetImage/*" ], "tags": [ - "get" + "image-url" ], "summary": "Get GetImage from URL", "parameters": [ @@ -1890,120 +1823,140 @@ const docTemplate = `{ } } } + } + }, + "definitions": { + "api_v1.CreateIdentityProviderRequest": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/api_v1.IdentityProviderConfig" + }, + "identifierFilter": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/api_v1.IdentityProviderType" + } + } }, - "/o/r/{resourceId}": { - "get": { - "description": "*Swagger UI may have problems displaying other file types than images", - "produces": [ - "application/octet-stream" - ], - "tags": [ - "resource" - ], - "summary": "Stream a resource", - "parameters": [ - { - "type": "integer", - "description": "Resource ID", - "name": "resourceId", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Thumbnail", - "name": "thumbnail", - "in": "query" + "api_v1.CreateMemoRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdTs": { + "type": "integer" + }, + "relationList": { + "type": "array", + "items": { + "$ref": "#/definitions/api_v1.UpsertMemoRelationRequest" } - ], - "responses": { - "200": { - "description": "Requested resource" - }, - "400": { - "description": "ID is not a number: %s | Failed to get resource visibility" - }, - "401": { - "description": "Resource visibility not match" - }, - "404": { - "description": "Resource not found: %d" - }, - "500": { - "description": "Failed to find resource by ID: %v | Failed to open the local resource: %s | Failed to read the local resource: %s" + }, + "resourceIdList": { + "description": "Related fields", + "type": "array", + "items": { + "type": "integer" } + }, + "visibility": { + "description": "Domain specific fields", + "allOf": [ + { + "$ref": "#/definitions/api_v1.Visibility" + } + ] } } }, - "/u/{id}/rss.xml": { - "get": { - "produces": [ - "text/xml" - ], - "tags": [ - "rss" - ], - "summary": "Get RSS for a user", - "parameters": [ - { - "type": "integer", - "description": "User ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "RSS" - }, - "400": { - "description": "User id is not a number" - }, - "500": { - "description": "Failed to get system customized profile | Failed to find memo list | Failed to generate rss" - } + "api_v1.CreateResourceRequest": { + "type": "object", + "properties": { + "externalLink": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "type": { + "type": "string" } } - } - }, - "definitions": { - "github_com_usememos_memos_store.UserSetting": { + }, + "api_v1.CreateStorageRequest": { "type": "object", "properties": { - "key": { + "config": { + "$ref": "#/definitions/api_v1.StorageConfig" + }, + "name": { "type": "string" }, - "userID": { - "type": "integer" + "type": { + "$ref": "#/definitions/api_v1.StorageType" + } + } + }, + "api_v1.CreateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" }, - "value": { + "nickname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/api_v1.Role" + }, + "username": { "type": "string" } } }, - "profile.Profile": { + "api_v1.CustomizedProfile": { "type": "object", "properties": { - "driver": { - "description": "Driver is the database driver\nsqlite, mysql", + "appearance": { + "description": "Appearance is the server default appearance.", "type": "string" }, - "dsn": { - "description": "DSN points to where Memos stores its own data", + "description": { + "description": "Description is the server description.", "type": "string" }, - "mode": { - "description": "Mode can be \"prod\" or \"dev\" or \"demo\"", + "locale": { + "description": "Locale is the server default locale.", "type": "string" }, - "version": { - "description": "Version is the current version of server", + "logoUrl": { + "description": "LogoURL is the url of logo image.", + "type": "string" + }, + "name": { + "description": "Name is the server name, default is ` + "`" + `memos` + "`" + `", "type": "string" } } }, - "store.FieldMapping": { + "api_v1.DeleteTagRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "api_v1.FieldMapping": { "type": "object", "properties": { "displayName": { @@ -2017,11 +1970,11 @@ const docTemplate = `{ } } }, - "store.IdentityProvider": { + "api_v1.IdentityProvider": { "type": "object", "properties": { "config": { - "$ref": "#/definitions/store.IdentityProviderConfig" + "$ref": "#/definitions/api_v1.IdentityProviderConfig" }, "id": { "type": "integer" @@ -2033,19 +1986,19 @@ const docTemplate = `{ "type": "string" }, "type": { - "$ref": "#/definitions/store.IdentityProviderType" + "$ref": "#/definitions/api_v1.IdentityProviderType" } } }, - "store.IdentityProviderConfig": { + "api_v1.IdentityProviderConfig": { "type": "object", "properties": { "oauth2Config": { - "$ref": "#/definitions/store.IdentityProviderOAuth2Config" + "$ref": "#/definitions/api_v1.IdentityProviderOAuth2Config" } } }, - "store.IdentityProviderOAuth2Config": { + "api_v1.IdentityProviderOAuth2Config": { "type": "object", "properties": { "authUrl": { @@ -2058,7 +2011,7 @@ const docTemplate = `{ "type": "string" }, "fieldMapping": { - "$ref": "#/definitions/store.FieldMapping" + "$ref": "#/definitions/api_v1.FieldMapping" }, "scopes": { "type": "array", @@ -2074,7 +2027,7 @@ const docTemplate = `{ } } }, - "store.IdentityProviderType": { + "api_v1.IdentityProviderType": { "type": "string", "enum": [ "OAUTH2" @@ -2083,7 +2036,18 @@ const docTemplate = `{ "IdentityProviderOAuth2Type" ] }, - "store.Memo": { + "api_v1.MemoRelationType": { + "type": "string", + "enum": [ + "REFERENCE", + "COMMENT" + ], + "x-enum-varnames": [ + "MemoRelationReference", + "MemoRelationComment" + ] + }, + "api_v1.PatchMemoRequest": { "type": "object", "properties": { "content": { @@ -2091,118 +2055,34 @@ const docTemplate = `{ "type": "string" }, "createdTs": { + "description": "Standard fields", "type": "integer" }, - "creatorID": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "parentID": { - "description": "Composed fields\nFor those comment memos, the parent ID is the memo ID of the memo being commented.\nIf the parent ID is nil, then this memo is not a comment.", - "type": "integer" - }, - "pinned": { - "type": "boolean" - }, "relationList": { "type": "array", "items": { - "$ref": "#/definitions/store.MemoRelation" + "$ref": "#/definitions/api_v1.UpsertMemoRelationRequest" } }, - "resourceIDList": { + "resourceIdList": { + "description": "Related fields", "type": "array", "items": { "type": "integer" } }, "rowStatus": { - "description": "Standard fields", - "allOf": [ - { - "$ref": "#/definitions/store.RowStatus" - } - ] + "$ref": "#/definitions/api_v1.RowStatus" }, "updatedTs": { "type": "integer" }, "visibility": { - "$ref": "#/definitions/store.Visibility" + "$ref": "#/definitions/api_v1.Visibility" } } }, - "store.MemoRelation": { - "type": "object", - "properties": { - "memoID": { - "type": "integer" - }, - "relatedMemoID": { - "type": "integer" - }, - "type": { - "$ref": "#/definitions/store.MemoRelationType" - } - } - }, - "store.MemoRelationType": { - "type": "string", - "enum": [ - "REFERENCE", - "COMMENT" - ], - "x-enum-varnames": [ - "MemoRelationReference", - "MemoRelationComment" - ] - }, - "store.Resource": { - "type": "object", - "properties": { - "blob": { - "type": "array", - "items": { - "type": "integer" - } - }, - "createdTs": { - "type": "integer" - }, - "creatorID": { - "description": "Standard fields", - "type": "integer" - }, - "externalLink": { - "type": "string" - }, - "filename": { - "description": "Domain specific fields", - "type": "string" - }, - "id": { - "type": "integer" - }, - "internalPath": { - "type": "string" - }, - "memoID": { - "type": "integer" - }, - "size": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "updatedTs": { - "type": "integer" - } - } - }, - "store.Role": { + "api_v1.Role": { "type": "string", "enum": [ "HOST", @@ -2215,7 +2095,7 @@ const docTemplate = `{ "RoleUser" ] }, - "store.RowStatus": { + "api_v1.RowStatus": { "type": "string", "enum": [ "NORMAL", @@ -2226,171 +2106,237 @@ const docTemplate = `{ "Archived" ] }, - "store.Storage": { + "api_v1.SSOSignIn": { "type": "object", "properties": { - "config": { + "code": { "type": "string" }, - "id": { + "identityProviderId": { "type": "integer" }, - "name": { - "type": "string" - }, - "type": { + "redirectUri": { "type": "string" } } }, - "store.SystemSetting": { + "api_v1.SignIn": { "type": "object", "properties": { - "description": { + "password": { "type": "string" }, - "name": { - "type": "string" + "remember": { + "type": "boolean" }, - "value": { + "username": { "type": "string" } } }, - "store.User": { + "api_v1.SignUp": { "type": "object", "properties": { - "avatarURL": { + "password": { "type": "string" }, - "createdTs": { - "type": "integer" - }, - "email": { + "username": { + "type": "string" + } + } + }, + "api_v1.StorageConfig": { + "type": "object", + "properties": { + "s3Config": { + "$ref": "#/definitions/api_v1.StorageS3Config" + } + } + }, + "api_v1.StorageS3Config": { + "type": "object", + "properties": { + "accessKey": { "type": "string" }, - "id": { - "type": "integer" + "bucket": { + "type": "string" }, - "nickname": { + "endPoint": { "type": "string" }, - "passwordHash": { + "path": { "type": "string" }, - "role": { - "$ref": "#/definitions/store.Role" + "presign": { + "type": "boolean" }, - "rowStatus": { - "description": "Standard fields", - "allOf": [ - { - "$ref": "#/definitions/store.RowStatus" - } - ] + "region": { + "type": "string" }, - "updatedTs": { - "type": "integer" + "secretKey": { + "type": "string" }, - "username": { - "description": "Domain specific fields", + "urlPrefix": { + "type": "string" + }, + "urlSuffix": { "type": "string" } } }, - "store.Visibility": { + "api_v1.StorageType": { "type": "string", "enum": [ - "PUBLIC", - "PROTECTED", - "PRIVATE" + "S3" ], "x-enum-varnames": [ - "Public", - "Protected", - "Private" + "StorageS3" ] }, - "v1.CreateIdentityProviderRequest": { + "api_v1.SystemSetting": { "type": "object", "properties": { - "config": { - "$ref": "#/definitions/v1.IdentityProviderConfig" - }, - "identifierFilter": { + "description": { "type": "string" }, "name": { - "type": "string" + "$ref": "#/definitions/api_v1.SystemSettingName" }, - "type": { - "$ref": "#/definitions/v1.IdentityProviderType" + "value": { + "description": "Value is a JSON string with basic value.", + "type": "string" } } }, - "v1.CreateMemoRequest": { + "api_v1.SystemSettingName": { + "type": "string", + "enum": [ + "server-id", + "secret-session", + "disable-public-memos", + "max-upload-size-mib", + "customized-profile", + "storage-service-id", + "local-storage-path", + "telegram-bot-token", + "memo-display-with-updated-ts" + ], + "x-enum-varnames": [ + "SystemSettingServerIDName", + "SystemSettingSecretSessionName", + "SystemSettingDisablePublicMemosName", + "SystemSettingMaxUploadSizeMiBName", + "SystemSettingCustomizedProfileName", + "SystemSettingStorageServiceIDName", + "SystemSettingLocalStoragePathName", + "SystemSettingTelegramBotTokenName", + "SystemSettingMemoDisplayWithUpdatedTsName" + ] + }, + "api_v1.SystemStatus": { "type": "object", "properties": { - "content": { + "additionalScript": { + "description": "Additional script.", "type": "string" }, - "createdTs": { - "type": "integer" - }, - "relationList": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.UpsertMemoRelationRequest" - } + "additionalStyle": { + "description": "Additional style.", + "type": "string" }, - "resourceIdList": { - "description": "Related fields", - "type": "array", - "items": { - "type": "integer" - } + "allowSignUp": { + "description": "System settings\nAllow sign up.", + "type": "boolean" }, - "visibility": { - "description": "Domain specific fields", + "customizedProfile": { + "description": "Customized server profile, including server name and external url.", "allOf": [ { - "$ref": "#/definitions/v1.Visibility" + "$ref": "#/definitions/api_v1.CustomizedProfile" } ] + }, + "dbSize": { + "type": "integer" + }, + "disablePasswordLogin": { + "description": "Disable password login.", + "type": "boolean" + }, + "disablePublicMemos": { + "description": "Disable public memos.", + "type": "boolean" + }, + "host": { + "$ref": "#/definitions/api_v1.User" + }, + "localStoragePath": { + "description": "Local storage path.", + "type": "string" + }, + "maxUploadSizeMiB": { + "description": "Max upload size.", + "type": "integer" + }, + "memoDisplayWithUpdatedTs": { + "description": "Memo display with updated timestamp.", + "type": "boolean" + }, + "profile": { + "$ref": "#/definitions/profile.Profile" + }, + "storageServiceId": { + "description": "Storage service ID.", + "type": "integer" } } }, - "v1.CreateResourceRequest": { + "api_v1.UpdateIdentityProviderRequest": { "type": "object", "properties": { - "externalLink": { + "config": { + "$ref": "#/definitions/api_v1.IdentityProviderConfig" + }, + "identifierFilter": { "type": "string" }, - "filename": { + "name": { "type": "string" }, "type": { + "$ref": "#/definitions/api_v1.IdentityProviderType" + } + } + }, + "api_v1.UpdateResourceRequest": { + "type": "object", + "properties": { + "filename": { "type": "string" } } }, - "v1.CreateStorageRequest": { + "api_v1.UpdateStorageRequest": { "type": "object", "properties": { "config": { - "$ref": "#/definitions/v1.StorageConfig" + "$ref": "#/definitions/api_v1.StorageConfig" }, "name": { "type": "string" }, "type": { - "$ref": "#/definitions/v1.StorageType" + "$ref": "#/definitions/api_v1.StorageType" } } }, - "v1.CreateUserRequest": { + "api_v1.UpdateUserRequest": { "type": "object", "properties": { + "avatarUrl": { + "type": "string" + }, "email": { "type": "string" }, @@ -2400,27 +2346,212 @@ const docTemplate = `{ "password": { "type": "string" }, - "role": { - "$ref": "#/definitions/v1.Role" + "rowStatus": { + "$ref": "#/definitions/api_v1.RowStatus" }, "username": { "type": "string" } } }, - "v1.CustomizedProfile": { + "api_v1.UpsertMemoOrganizerRequest": { "type": "object", "properties": { - "appearance": { - "description": "Appearance is the server default appearance.", - "type": "string" - }, - "description": { - "description": "Description is the server description.", - "type": "string" + "pinned": { + "type": "boolean" + } + } + }, + "api_v1.UpsertMemoRelationRequest": { + "type": "object", + "properties": { + "relatedMemoId": { + "type": "integer" }, - "externalUrl": { - "description": "ExternalURL is the external url of server. e.g. https://usermemos.com", + "type": { + "$ref": "#/definitions/api_v1.MemoRelationType" + } + } + }, + "api_v1.UpsertSystemSettingRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "$ref": "#/definitions/api_v1.SystemSettingName" + }, + "value": { + "type": "string" + } + } + }, + "api_v1.UpsertTagRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "api_v1.User": { + "type": "object", + "properties": { + "avatarUrl": { + "type": "string" + }, + "createdTs": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/api_v1.Role" + }, + "rowStatus": { + "description": "Standard fields", + "allOf": [ + { + "$ref": "#/definitions/api_v1.RowStatus" + } + ] + }, + "updatedTs": { + "type": "integer" + }, + "username": { + "description": "Domain specific fields", + "type": "string" + } + } + }, + "api_v1.Visibility": { + "type": "string", + "enum": [ + "PUBLIC", + "PROTECTED", + "PRIVATE" + ], + "x-enum-varnames": [ + "Public", + "Protected", + "Private" + ] + }, + "github_com_usememos_memos_api_v1.CreateIdentityProviderRequest": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig" + }, + "identifierFilter": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType" + } + } + }, + "github_com_usememos_memos_api_v1.CreateMemoRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdTs": { + "type": "integer" + }, + "relationList": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest" + } + }, + "resourceIdList": { + "description": "Related fields", + "type": "array", + "items": { + "type": "integer" + } + }, + "visibility": { + "description": "Domain specific fields", + "allOf": [ + { + "$ref": "#/definitions/github_com_usememos_memos_api_v1.Visibility" + } + ] + } + } + }, + "github_com_usememos_memos_api_v1.CreateResourceRequest": { + "type": "object", + "properties": { + "externalLink": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "github_com_usememos_memos_api_v1.CreateStorageRequest": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageConfig" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageType" + } + } + }, + "github_com_usememos_memos_api_v1.CreateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/github_com_usememos_memos_api_v1.Role" + }, + "username": { + "type": "string" + } + } + }, + "github_com_usememos_memos_api_v1.CustomizedProfile": { + "type": "object", + "properties": { + "appearance": { + "description": "Appearance is the server default appearance.", + "type": "string" + }, + "description": { + "description": "Description is the server description.", "type": "string" }, "locale": { @@ -2437,7 +2568,7 @@ const docTemplate = `{ } } }, - "v1.DeleteTagRequest": { + "github_com_usememos_memos_api_v1.DeleteTagRequest": { "type": "object", "properties": { "name": { @@ -2445,7 +2576,7 @@ const docTemplate = `{ } } }, - "v1.FieldMapping": { + "github_com_usememos_memos_api_v1.FieldMapping": { "type": "object", "properties": { "displayName": { @@ -2459,11 +2590,11 @@ const docTemplate = `{ } } }, - "v1.IdentityProvider": { + "github_com_usememos_memos_api_v1.IdentityProvider": { "type": "object", "properties": { "config": { - "$ref": "#/definitions/v1.IdentityProviderConfig" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig" }, "id": { "type": "integer" @@ -2475,19 +2606,19 @@ const docTemplate = `{ "type": "string" }, "type": { - "$ref": "#/definitions/v1.IdentityProviderType" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType" } } }, - "v1.IdentityProviderConfig": { + "github_com_usememos_memos_api_v1.IdentityProviderConfig": { "type": "object", "properties": { "oauth2Config": { - "$ref": "#/definitions/v1.IdentityProviderOAuth2Config" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config" } } }, - "v1.IdentityProviderOAuth2Config": { + "github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config": { "type": "object", "properties": { "authUrl": { @@ -2500,7 +2631,7 @@ const docTemplate = `{ "type": "string" }, "fieldMapping": { - "$ref": "#/definitions/v1.FieldMapping" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.FieldMapping" }, "scopes": { "type": "array", @@ -2516,7 +2647,7 @@ const docTemplate = `{ } } }, - "v1.IdentityProviderType": { + "github_com_usememos_memos_api_v1.IdentityProviderType": { "type": "string", "enum": [ "OAUTH2" @@ -2525,18 +2656,18 @@ const docTemplate = `{ "IdentityProviderOAuth2Type" ] }, - "v1.MemoRelationType": { + "github_com_usememos_memos_api_v1.MemoRelationType": { "type": "string", "enum": [ "REFERENCE", - "ADDITIONAL" + "COMMENT" ], "x-enum-varnames": [ "MemoRelationReference", - "MemoRelationAdditional" + "MemoRelationComment" ] }, - "v1.PatchMemoRequest": { + "github_com_usememos_memos_api_v1.PatchMemoRequest": { "type": "object", "properties": { "content": { @@ -2550,7 +2681,7 @@ const docTemplate = `{ "relationList": { "type": "array", "items": { - "$ref": "#/definitions/v1.UpsertMemoRelationRequest" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest" } }, "resourceIdList": { @@ -2561,17 +2692,17 @@ const docTemplate = `{ } }, "rowStatus": { - "$ref": "#/definitions/v1.RowStatus" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.RowStatus" }, "updatedTs": { "type": "integer" }, "visibility": { - "$ref": "#/definitions/v1.Visibility" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.Visibility" } } }, - "v1.Role": { + "github_com_usememos_memos_api_v1.Role": { "type": "string", "enum": [ "HOST", @@ -2584,7 +2715,7 @@ const docTemplate = `{ "RoleUser" ] }, - "v1.RowStatus": { + "github_com_usememos_memos_api_v1.RowStatus": { "type": "string", "enum": [ "NORMAL", @@ -2595,7 +2726,7 @@ const docTemplate = `{ "Archived" ] }, - "v1.SSOSignIn": { + "github_com_usememos_memos_api_v1.SSOSignIn": { "type": "object", "properties": { "code": { @@ -2609,18 +2740,21 @@ const docTemplate = `{ } } }, - "v1.SignIn": { + "github_com_usememos_memos_api_v1.SignIn": { "type": "object", "properties": { "password": { "type": "string" }, + "remember": { + "type": "boolean" + }, "username": { "type": "string" } } }, - "v1.SignUp": { + "github_com_usememos_memos_api_v1.SignUp": { "type": "object", "properties": { "password": { @@ -2631,15 +2765,15 @@ const docTemplate = `{ } } }, - "v1.StorageConfig": { + "github_com_usememos_memos_api_v1.StorageConfig": { "type": "object", "properties": { "s3Config": { - "$ref": "#/definitions/v1.StorageS3Config" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageS3Config" } } }, - "v1.StorageS3Config": { + "github_com_usememos_memos_api_v1.StorageS3Config": { "type": "object", "properties": { "accessKey": { @@ -2654,6 +2788,9 @@ const docTemplate = `{ "path": { "type": "string" }, + "presign": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -2668,7 +2805,7 @@ const docTemplate = `{ } } }, - "v1.StorageType": { + "github_com_usememos_memos_api_v1.StorageType": { "type": "string", "enum": [ "S3" @@ -2677,14 +2814,14 @@ const docTemplate = `{ "StorageS3" ] }, - "v1.SystemSetting": { + "github_com_usememos_memos_api_v1.SystemSetting": { "type": "object", "properties": { "description": { "type": "string" }, "name": { - "$ref": "#/definitions/v1.SystemSettingName" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.SystemSettingName" }, "value": { "description": "Value is a JSON string with basic value.", @@ -2692,42 +2829,32 @@ const docTemplate = `{ } } }, - "v1.SystemSettingName": { + "github_com_usememos_memos_api_v1.SystemSettingName": { "type": "string", "enum": [ "server-id", "secret-session", - "allow-signup", - "disable-password-login", "disable-public-memos", "max-upload-size-mib", - "additional-style", - "additional-script", "customized-profile", "storage-service-id", "local-storage-path", "telegram-bot-token", - "memo-display-with-updated-ts", - "auto-backup-interval" + "memo-display-with-updated-ts" ], "x-enum-varnames": [ "SystemSettingServerIDName", "SystemSettingSecretSessionName", - "SystemSettingAllowSignUpName", - "SystemSettingDisablePasswordLoginName", "SystemSettingDisablePublicMemosName", "SystemSettingMaxUploadSizeMiBName", - "SystemSettingAdditionalStyleName", - "SystemSettingAdditionalScriptName", "SystemSettingCustomizedProfileName", "SystemSettingStorageServiceIDName", "SystemSettingLocalStoragePathName", "SystemSettingTelegramBotTokenName", - "SystemSettingMemoDisplayWithUpdatedTsName", - "SystemSettingAutoBackupIntervalName" + "SystemSettingMemoDisplayWithUpdatedTsName" ] }, - "v1.SystemStatus": { + "github_com_usememos_memos_api_v1.SystemStatus": { "type": "object", "properties": { "additionalScript": { @@ -2742,15 +2869,11 @@ const docTemplate = `{ "description": "System settings\nAllow sign up.", "type": "boolean" }, - "autoBackupInterval": { - "description": "Auto Backup Interval.", - "type": "integer" - }, "customizedProfile": { "description": "Customized server profile, including server name and external url.", "allOf": [ { - "$ref": "#/definitions/v1.CustomizedProfile" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.CustomizedProfile" } ] }, @@ -2766,7 +2889,7 @@ const docTemplate = `{ "type": "boolean" }, "host": { - "$ref": "#/definitions/v1.User" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.User" }, "localStoragePath": { "description": "Local storage path.", @@ -2789,11 +2912,11 @@ const docTemplate = `{ } } }, - "v1.UpdateIdentityProviderRequest": { + "github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest": { "type": "object", "properties": { "config": { - "$ref": "#/definitions/v1.IdentityProviderConfig" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig" }, "identifierFilter": { "type": "string" @@ -2802,11 +2925,11 @@ const docTemplate = `{ "type": "string" }, "type": { - "$ref": "#/definitions/v1.IdentityProviderType" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType" } } }, - "v1.UpdateResourceRequest": { + "github_com_usememos_memos_api_v1.UpdateResourceRequest": { "type": "object", "properties": { "filename": { @@ -2814,21 +2937,21 @@ const docTemplate = `{ } } }, - "v1.UpdateStorageRequest": { + "github_com_usememos_memos_api_v1.UpdateStorageRequest": { "type": "object", "properties": { "config": { - "$ref": "#/definitions/v1.StorageConfig" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageConfig" }, "name": { "type": "string" }, "type": { - "$ref": "#/definitions/v1.StorageType" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageType" } } }, - "v1.UpdateUserRequest": { + "github_com_usememos_memos_api_v1.UpdateUserRequest": { "type": "object", "properties": { "avatarUrl": { @@ -2844,14 +2967,14 @@ const docTemplate = `{ "type": "string" }, "rowStatus": { - "$ref": "#/definitions/v1.RowStatus" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.RowStatus" }, "username": { "type": "string" } } }, - "v1.UpsertMemoOrganizerRequest": { + "github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest": { "type": "object", "properties": { "pinned": { @@ -2859,32 +2982,32 @@ const docTemplate = `{ } } }, - "v1.UpsertMemoRelationRequest": { + "github_com_usememos_memos_api_v1.UpsertMemoRelationRequest": { "type": "object", "properties": { "relatedMemoId": { "type": "integer" }, "type": { - "$ref": "#/definitions/v1.MemoRelationType" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.MemoRelationType" } } }, - "v1.UpsertSystemSettingRequest": { + "github_com_usememos_memos_api_v1.UpsertSystemSettingRequest": { "type": "object", "properties": { "description": { "type": "string" }, "name": { - "$ref": "#/definitions/v1.SystemSettingName" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.SystemSettingName" }, "value": { "type": "string" } } }, - "v1.UpsertTagRequest": { + "github_com_usememos_memos_api_v1.UpsertTagRequest": { "type": "object", "properties": { "name": { @@ -2892,18 +3015,7 @@ const docTemplate = `{ } } }, - "v1.UpsertUserSettingRequest": { - "type": "object", - "properties": { - "key": { - "$ref": "#/definitions/v1.UserSettingKey" - }, - "value": { - "type": "string" - } - } - }, - "v1.User": { + "github_com_usememos_memos_api_v1.User": { "type": "object", "properties": { "avatarUrl": { @@ -2922,61 +3034,327 @@ const docTemplate = `{ "type": "string" }, "role": { - "$ref": "#/definitions/v1.Role" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.Role" }, "rowStatus": { "description": "Standard fields", "allOf": [ { - "$ref": "#/definitions/v1.RowStatus" + "$ref": "#/definitions/github_com_usememos_memos_api_v1.RowStatus" } ] }, "updatedTs": { "type": "integer" }, - "userSettingList": { + "username": { + "description": "Domain specific fields", + "type": "string" + } + } + }, + "github_com_usememos_memos_api_v1.Visibility": { + "type": "string", + "enum": [ + "PUBLIC", + "PROTECTED", + "PRIVATE" + ], + "x-enum-varnames": [ + "Public", + "Protected", + "Private" + ] + }, + "profile.Profile": { + "type": "object", + "properties": { + "mode": { + "description": "Mode can be \"prod\" or \"dev\" or \"demo\"", + "type": "string" + }, + "version": { + "description": "Version is the current version of server", + "type": "string" + } + } + }, + "store.FieldMapping": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "identifier": { + "type": "string" + } + } + }, + "store.IdentityProvider": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/store.IdentityProviderConfig" + }, + "id": { + "type": "integer" + }, + "identifierFilter": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/store.IdentityProviderType" + } + } + }, + "store.IdentityProviderConfig": { + "type": "object", + "properties": { + "oauth2Config": { + "$ref": "#/definitions/store.IdentityProviderOAuth2Config" + } + } + }, + "store.IdentityProviderOAuth2Config": { + "type": "object", + "properties": { + "authUrl": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "fieldMapping": { + "$ref": "#/definitions/store.FieldMapping" + }, + "scopes": { "type": "array", "items": { - "$ref": "#/definitions/v1.UserSetting" + "type": "string" } }, - "username": { + "tokenUrl": { + "type": "string" + }, + "userInfoUrl": { + "type": "string" + } + } + }, + "store.IdentityProviderType": { + "type": "string", + "enum": [ + "OAUTH2" + ], + "x-enum-varnames": [ + "IdentityProviderOAuth2Type" + ] + }, + "store.Memo": { + "type": "object", + "properties": { + "content": { "description": "Domain specific fields", "type": "string" + }, + "createdTs": { + "type": "integer" + }, + "creatorID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "parentID": { + "type": "integer" + }, + "pinned": { + "description": "Composed fields", + "type": "boolean" + }, + "resourceName": { + "type": "string" + }, + "rowStatus": { + "description": "Standard fields", + "allOf": [ + { + "$ref": "#/definitions/store.RowStatus" + } + ] + }, + "updatedTs": { + "type": "integer" + }, + "visibility": { + "$ref": "#/definitions/store.Visibility" } } }, - "v1.UserSetting": { + "store.MemoRelation": { "type": "object", "properties": { - "key": { - "$ref": "#/definitions/v1.UserSettingKey" + "memoID": { + "type": "integer" }, - "userId": { + "relatedMemoID": { "type": "integer" }, - "value": { + "type": { + "$ref": "#/definitions/store.MemoRelationType" + } + } + }, + "store.MemoRelationType": { + "type": "string", + "enum": [ + "REFERENCE", + "COMMENT" + ], + "x-enum-varnames": [ + "MemoRelationReference", + "MemoRelationComment" + ] + }, + "store.Resource": { + "type": "object", + "properties": { + "blob": { + "type": "array", + "items": { + "type": "integer" + } + }, + "createdTs": { + "type": "integer" + }, + "creatorID": { + "description": "Standard fields", + "type": "integer" + }, + "externalLink": { + "type": "string" + }, + "filename": { + "description": "Domain specific fields", + "type": "string" + }, + "id": { + "type": "integer" + }, + "internalPath": { + "type": "string" + }, + "memoID": { + "type": "integer" + }, + "resourceName": { "type": "string" + }, + "size": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "updatedTs": { + "type": "integer" } } }, - "v1.UserSettingKey": { + "store.Role": { "type": "string", "enum": [ - "locale", - "appearance", - "memo-visibility", - "telegram-user-id" + "HOST", + "ADMIN", + "USER" ], "x-enum-varnames": [ - "UserSettingLocaleKey", - "UserSettingAppearanceKey", - "UserSettingMemoVisibilityKey", - "UserSettingTelegramUserIDKey" + "RoleHost", + "RoleAdmin", + "RoleUser" + ] + }, + "store.RowStatus": { + "type": "string", + "enum": [ + "NORMAL", + "ARCHIVED" + ], + "x-enum-varnames": [ + "Normal", + "Archived" ] }, - "v1.Visibility": { + "store.Storage": { + "type": "object", + "properties": { + "config": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "store.User": { + "type": "object", + "properties": { + "avatarURL": { + "type": "string" + }, + "createdTs": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "passwordHash": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/store.Role" + }, + "rowStatus": { + "description": "Standard fields", + "allOf": [ + { + "$ref": "#/definitions/store.RowStatus" + } + ] + }, + "updatedTs": { + "type": "integer" + }, + "username": { + "description": "Domain specific fields", + "type": "string" + } + } + }, + "store.Visibility": { "type": "string", "enum": [ "PUBLIC", diff --git a/api/v1/http_getter.go b/server/route/api/v1/http_getter.go similarity index 98% rename from api/v1/http_getter.go rename to server/route/api/v1/http_getter.go index fa6093bcbd358..e63c5ab0dc44e 100644 --- a/api/v1/http_getter.go +++ b/server/route/api/v1/http_getter.go @@ -18,7 +18,7 @@ func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) { // GetImage godoc // // @Summary Get GetImage from URL -// @Tags get +// @Tags image-url // @Produce GetImage/* // @Param url query string true "Image url" // @Success 200 {object} nil "Image" diff --git a/api/v1/idp.go b/server/route/api/v1/idp.go similarity index 100% rename from api/v1/idp.go rename to server/route/api/v1/idp.go diff --git a/api/v1/jwt.go b/server/route/api/v1/jwt.go similarity index 85% rename from api/v1/jwt.go rename to server/route/api/v1/jwt.go index 68a8893eaf8cf..b11b8aa8eb285 100644 --- a/api/v1/jwt.go +++ b/server/route/api/v1/jwt.go @@ -2,16 +2,17 @@ package v1 import ( "fmt" + "log/slog" "net/http" "strings" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" "github.com/labstack/echo/v4" "github.com/pkg/errors" - "github.com/usememos/memos/api/auth" "github.com/usememos/memos/internal/util" storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/server/route/api/auth" "github.com/usememos/memos/store" ) @@ -35,7 +36,7 @@ func extractTokenFromHeader(c echo.Context) (string, error) { return authHeaderParts[1], nil } -func findAccessToken(c echo.Context) string { +func FindAccessToken(c echo.Context) string { // Check the HTTP request header first. accessToken, _ := extractTokenFromHeader(c) if accessToken == "" { @@ -60,18 +61,18 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e } // Skip validation for server status endpoints. - if util.HasPrefixes(path, "/api/v1/ping", "/api/v1/idp", "/api/v1/status", "/api/v1/user") && path != "/api/v1/user/me" && method == http.MethodGet { + if util.HasPrefixes(path, "/api/v1/ping", "/api/v1/status") && method == http.MethodGet { return next(c) } - accessToken := findAccessToken(c) + accessToken := FindAccessToken(c) if accessToken == "" { // Allow the user to access the public endpoints. if util.HasPrefixes(path, "/o") { return next(c) } // When the request is not authenticated, we allow the user to access the memo endpoints for those public memos. - if util.HasPrefixes(path, "/api/v1/memo") && method == http.MethodGet { + if util.HasPrefixes(path, "/api/v1/idp", "/api/v1/memo", "/api/v1/user") && path != "/api/v1/user" && method == http.MethodGet { return next(c) } return echo.NewHTTPError(http.StatusUnauthorized, "Missing access token") @@ -79,7 +80,10 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e userID, err := getUserIDFromAccessToken(accessToken, secret) if err != nil { - removeAccessTokenAndCookies(c) + err = removeAccessTokenAndCookies(c, server.Store, userID, accessToken) + if err != nil { + slog.Warn("fail to remove AccessToken and Cookies", err) + } return echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired access token") } @@ -88,7 +92,10 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user access tokens.").WithInternal(err) } if !validateAccessToken(accessToken, accessTokens) { - removeAccessTokenAndCookies(c) + err = removeAccessTokenAndCookies(c, server.Store, userID, accessToken) + if err != nil { + slog.Warn("fail to remove AccessToken and Cookies", err) + } return echo.NewHTTPError(http.StatusUnauthorized, "Invalid access token.") } diff --git a/api/v1/memo.go b/server/route/api/v1/memo.go similarity index 77% rename from api/v1/memo.go rename to server/route/api/v1/memo.go index db3ecc2fd9d05..90f01f084afb8 100644 --- a/api/v1/memo.go +++ b/server/route/api/v1/memo.go @@ -4,18 +4,18 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "net/http" "strconv" "time" "github.com/labstack/echo/v4" + "github.com/lithammer/shortuuid/v4" "github.com/pkg/errors" - "go.uber.org/zap" - "github.com/usememos/memos/internal/log" "github.com/usememos/memos/internal/util" + "github.com/usememos/memos/plugin/webhook" storepb "github.com/usememos/memos/proto/gen/store" - "github.com/usememos/memos/server/service/metric" "github.com/usememos/memos/store" ) @@ -44,7 +44,8 @@ func (v Visibility) String() string { } type Memo struct { - ID int32 `json:"id"` + ID int32 `json:"id"` + Name string `json:"name"` // Standard fields RowStatus RowStatus `json:"rowStatus"` @@ -59,7 +60,6 @@ type Memo struct { Pinned bool `json:"pinned"` // Related fields - Parent *Memo `json:"parent"` CreatorName string `json:"creatorName"` CreatorUsername string `json:"creatorUsername"` ResourceList []*Resource `json:"resourceList"` @@ -146,48 +146,42 @@ func (s *APIV1Service) registerMemoRoutes(g *echo.Group) { // @Router /api/v1/memo [GET] func (s *APIV1Service) GetMemoList(c echo.Context) error { ctx := c.Request().Context() - hasParentFlag := false - findMemoMessage := &store.FindMemo{ - HasParent: &hasParentFlag, + find := &store.FindMemo{ + OrderByPinned: true, } if userID, err := util.ConvertStringToInt32(c.QueryParam("creatorId")); err == nil { - findMemoMessage.CreatorID = &userID + find.CreatorID = &userID } if username := c.QueryParam("creatorUsername"); username != "" { user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username}) if user != nil { - findMemoMessage.CreatorID = &user.ID + find.CreatorID = &user.ID } } currentUserID, ok := c.Get(userIDContextKey).(int32) if !ok { // Anonymous use should only fetch PUBLIC memos with specified user - if findMemoMessage.CreatorID == nil { + if find.CreatorID == nil { return echo.NewHTTPError(http.StatusBadRequest, "Missing user to find memo") } - findMemoMessage.VisibilityList = []store.Visibility{store.Public} + find.VisibilityList = []store.Visibility{store.Public} } else { // Authorized user can fetch all PUBLIC/PROTECTED memo visibilityList := []store.Visibility{store.Public, store.Protected} // If Creator is authorized user (as default), PRIVATE memo is OK - if findMemoMessage.CreatorID == nil || *findMemoMessage.CreatorID == currentUserID { - findMemoMessage.CreatorID = ¤tUserID + if find.CreatorID == nil || *find.CreatorID == currentUserID { + find.CreatorID = ¤tUserID visibilityList = append(visibilityList, store.Private) } - findMemoMessage.VisibilityList = visibilityList + find.VisibilityList = visibilityList } rowStatus := store.RowStatus(c.QueryParam("rowStatus")) if rowStatus != "" { - findMemoMessage.RowStatus = &rowStatus - } - pinnedStr := c.QueryParam("pinned") - if pinnedStr != "" { - pinned := pinnedStr == "true" - findMemoMessage.Pinned = &pinned + find.RowStatus = &rowStatus } contentSearch := []string{} @@ -199,13 +193,13 @@ func (s *APIV1Service) GetMemoList(c echo.Context) error { if content != "" { contentSearch = append(contentSearch, content) } - findMemoMessage.ContentSearch = contentSearch + find.ContentSearch = contentSearch if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil { - findMemoMessage.Limit = &limit + find.Limit = &limit } if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil { - findMemoMessage.Offset = &offset + find.Offset = &offset } memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) @@ -213,10 +207,10 @@ func (s *APIV1Service) GetMemoList(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err) } if memoDisplayWithUpdatedTs { - findMemoMessage.OrderByUpdatedTs = true + find.OrderByUpdatedTs = true } - list, err := s.Store.ListMemos(ctx, findMemoMessage) + list, err := s.Store.ListMemos(ctx, find) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err) } @@ -267,18 +261,13 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error { if createMemoRequest.Visibility == "" { userMemoVisibilitySetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{ UserID: &userID, - Key: UserSettingMemoVisibilityKey.String(), + Key: storepb.UserSettingKey_USER_SETTING_MEMO_VISIBILITY, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err) } if userMemoVisibilitySetting != nil { - memoVisibility := Private - err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err) - } - createMemoRequest.Visibility = memoVisibility + createMemoRequest.Visibility = Visibility(userMemoVisibilitySetting.GetMemoVisibility()) } else { // Private is the default memo visibility. createMemoRequest.Visibility = Private @@ -286,7 +275,7 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error { } // Find disable public memos system setting. - disablePublicMemosSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{ + disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ Name: SystemSettingDisablePublicMemosName.String(), }) if err != nil { @@ -390,25 +379,16 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err) } - // send notification by telegram bot if memo is not Private + // Send notification to telegram if memo is not private. if memoResponse.Visibility != Private { // fetch all telegram UserID - userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{Key: UserSettingTelegramUserIDKey.String()}) + userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{Key: storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID}) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to ListUserSettings").SetInternal(err) } for _, userSetting := range userSettings { - // parse telegram UserID setting value into a int64 - var tgUserIDStr string - err := json.Unmarshal([]byte(userSetting.Value), &tgUserIDStr) - if err != nil { - log.Error("failed to parse Telegram UserID", zap.Error(err)) - continue - } - - tgUserID, err := strconv.ParseInt(tgUserIDStr, 10, 64) + tgUserID, err := strconv.ParseInt(userSetting.GetTelegramUserId(), 10, 64) if err != nil { - log.Error("failed to parse Telegram UserID", zap.Error(err)) continue } @@ -416,12 +396,15 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error { content := memoResponse.CreatorName + " Says:\n\n" + memoResponse.Content _, err = s.telegramBot.SendMessage(ctx, tgUserID, content) if err != nil { - log.Error("Failed to send Telegram notification", zap.Error(err)) continue } } } - metric.Enqueue("memo create") + // Try to dispatch webhook when memo is created. + if err := s.DispatchMemoCreatedWebhook(ctx, memoResponse); err != nil { + slog.Warn("Failed to dispatch memo created webhook", err) + } + return c.JSON(http.StatusOK, memoResponse) } @@ -442,37 +425,34 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error { // - creatorUsername is listed at ./web/src/helpers/api.ts:82, but it's not present here func (s *APIV1Service) GetAllMemos(c echo.Context) error { ctx := c.Request().Context() - hasParentFlag := false - findMemoMessage := &store.FindMemo{ - HasParent: &hasParentFlag, - } + memoFind := &store.FindMemo{} _, ok := c.Get(userIDContextKey).(int32) if !ok { - findMemoMessage.VisibilityList = []store.Visibility{store.Public} + memoFind.VisibilityList = []store.Visibility{store.Public} } else { - findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected} + memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} } if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil { - findMemoMessage.Limit = &limit + memoFind.Limit = &limit } if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil { - findMemoMessage.Offset = &offset + memoFind.Offset = &offset } // Only fetch normal status memos. normalStatus := store.Normal - findMemoMessage.RowStatus = &normalStatus + memoFind.RowStatus = &normalStatus memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err) } if memoDisplayWithUpdatedTs { - findMemoMessage.OrderByUpdatedTs = true + memoFind.OrderByUpdatedTs = true } - list, err := s.Store.ListMemos(ctx, findMemoMessage) + list, err := s.Store.ListMemos(ctx, memoFind) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err) } @@ -502,10 +482,8 @@ func (s *APIV1Service) GetAllMemos(c echo.Context) error { func (s *APIV1Service) GetMemoStats(c echo.Context) error { ctx := c.Request().Context() normalStatus := store.Normal - hasParentFlag := false findMemoMessage := &store.FindMemo{ RowStatus: &normalStatus, - HasParent: &hasParentFlag, ExcludeContent: true, } if creatorID, err := util.ConvertStringToInt32(c.QueryParam("creatorId")); err == nil { @@ -643,6 +621,13 @@ func (s *APIV1Service) DeleteMemo(c echo.Context) error { return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized") } + if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil { + // Try to dispatch webhook when memo is deleted. + if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil { + slog.Warn("Failed to dispatch memo deleted webhook", err) + } + } + if err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{ ID: memoID, }); err != nil { @@ -722,6 +707,36 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error { if patchMemoRequest.Visibility != nil { visibility := store.Visibility(patchMemoRequest.Visibility.String()) updateMemoMessage.Visibility = &visibility + // Find disable public memos system setting. + disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ + Name: SystemSettingDisablePublicMemosName.String(), + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err) + } + if disablePublicMemosSystemSetting != nil { + disablePublicMemos := false + err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err) + } + if disablePublicMemos { + user, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &userID, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err) + } + if user == nil { + return echo.NewHTTPError(http.StatusNotFound, "User not found") + } + // Enforce normal user to save as private memo if public memos are disabled. + if user.Role == store.RoleUser { + visibility = store.Visibility("PRIVATE") + updateMemoMessage.Visibility = &visibility + } + } + } } err = s.Store.UpdateMemo(ctx, updateMemoMessage) @@ -736,8 +751,16 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error { return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID)) } + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err) + } if patchMemoRequest.ResourceIDList != nil { - addedResourceIDList, removedResourceIDList := getIDListDiff(memo.ResourceIDList, patchMemoRequest.ResourceIDList) + originResourceIDList := []int32{} + for _, resource := range memoMessage.ResourceList { + originResourceIDList = append(originResourceIDList, resource.ID) + } + addedResourceIDList, removedResourceIDList := getIDListDiff(originResourceIDList, patchMemoRequest.ResourceIDList) for _, resourceID := range addedResourceIDList { if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{ ID: resourceID, @@ -756,15 +779,15 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error { } if patchMemoRequest.RelationList != nil { - patchMemoRelationList := make([]*store.MemoRelation, 0) + patchMemoRelationList := make([]*MemoRelation, 0) for _, memoRelation := range patchMemoRequest.RelationList { - patchMemoRelationList = append(patchMemoRelationList, &store.MemoRelation{ + patchMemoRelationList = append(patchMemoRelationList, &MemoRelation{ MemoID: memo.ID, RelatedMemoID: memoRelation.RelatedMemoID, - Type: store.MemoRelationType(memoRelation.Type), + Type: memoRelation.Type, }) } - addedMemoRelationList, removedMemoRelationList := getMemoRelationListDiff(memo.RelationList, patchMemoRelationList) + addedMemoRelationList, removedMemoRelationList := getMemoRelationListDiff(memoMessage.RelationList, patchMemoRelationList) for _, memoRelation := range addedMemoRelationList { if _, err := s.Store.UpsertMemoRelation(ctx, memoRelation); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err) @@ -793,12 +816,18 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err) } + // Try to dispatch webhook when memo is updated. + if err := s.DispatchMemoUpdatedWebhook(ctx, memoResponse); err != nil { + slog.Error("Failed to dispatch memo updated webhook", err) + } + return c.JSON(http.StatusOK, memoResponse) } func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*Memo, error) { - memoResponse := &Memo{ + memoMessage := &Memo{ ID: memo.ID, + Name: memo.ResourceName, RowStatus: RowStatus(memo.RowStatus.String()), CreatorID: memo.CreatorID, CreatedTs: memo.CreatedTs, @@ -810,67 +839,67 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem // Compose creator name. user, err := s.Store.GetUser(ctx, &store.FindUser{ - ID: &memoResponse.CreatorID, + ID: &memoMessage.CreatorID, }) if err != nil { return nil, err } if user.Nickname != "" { - memoResponse.CreatorName = user.Nickname + memoMessage.CreatorName = user.Nickname } else { - memoResponse.CreatorName = user.Username + memoMessage.CreatorName = user.Username } - memoResponse.CreatorUsername = user.Username + memoMessage.CreatorUsername = user.Username // Compose display ts. - memoResponse.DisplayTs = memoResponse.CreatedTs + memoMessage.DisplayTs = memoMessage.CreatedTs // Find memo display with updated ts setting. memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) if err != nil { return nil, err } if memoDisplayWithUpdatedTs { - memoResponse.DisplayTs = memoResponse.UpdatedTs + memoMessage.DisplayTs = memoMessage.UpdatedTs } + // Compose related resources. + resourceList, err := s.Store.ListResources(ctx, &store.FindResource{ + MemoID: &memo.ID, + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to list resources") + } + memoMessage.ResourceList = []*Resource{} + for _, resource := range resourceList { + memoMessage.ResourceList = append(memoMessage.ResourceList, convertResourceFromStore(resource)) + } + + // Compose related memo relations. relationList := []*MemoRelation{} - for _, relation := range memo.RelationList { + tempList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ + MemoID: &memo.ID, + }) + if err != nil { + return nil, err + } + for _, relation := range tempList { relationList = append(relationList, convertMemoRelationFromStore(relation)) } - memoResponse.RelationList = relationList - - resourceList := []*Resource{} - for _, resourceID := range memo.ResourceIDList { - resource, err := s.Store.GetResource(ctx, &store.FindResource{ - ID: &resourceID, - }) - if resource != nil && err == nil { - resourceList = append(resourceList, convertResourceFromStore(resource)) - } + tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ + RelatedMemoID: &memo.ID, + }) + if err != nil { + return nil, err } - memoResponse.ResourceList = resourceList - - if memo.ParentID != nil { - parentMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ - ID: memo.ParentID, - }) - if err != nil { - return nil, err - } - if parentMemo != nil { - parent, err := s.convertMemoFromStore(ctx, parentMemo) - if err != nil { - return nil, err - } - memoResponse.Parent = parent - } + for _, relation := range tempList { + relationList = append(relationList, convertMemoRelationFromStore(relation)) } - - return memoResponse, nil + memoMessage.RelationList = relationList + return memoMessage, nil } func (s *APIV1Service) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) { - memoDisplayWithUpdatedTsSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{ + memoDisplayWithUpdatedTsSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ Name: SystemSettingMemoDisplayWithUpdatedTsName.String(), }) if err != nil { @@ -892,14 +921,15 @@ func convertCreateMemoRequestToMemoMessage(memoCreate *CreateMemoRequest) *store createdTs = *memoCreate.CreatedTs } return &store.Memo{ - CreatorID: memoCreate.CreatorID, - CreatedTs: createdTs, - Content: memoCreate.Content, - Visibility: store.Visibility(memoCreate.Visibility), + ResourceName: shortuuid.New(), + CreatorID: memoCreate.CreatorID, + CreatedTs: createdTs, + Content: memoCreate.Content, + Visibility: store.Visibility(memoCreate.Visibility), } } -func getMemoRelationListDiff(oldList, newList []*store.MemoRelation) (addedList, removedList []*store.MemoRelation) { +func getMemoRelationListDiff(oldList, newList []*MemoRelation) (addedList, removedList []*store.MemoRelation) { oldMap := map[string]bool{} for _, relation := range oldList { oldMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true @@ -911,13 +941,21 @@ func getMemoRelationListDiff(oldList, newList []*store.MemoRelation) (addedList, for _, relation := range oldList { key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type) if !newMap[key] { - removedList = append(removedList, relation) + removedList = append(removedList, &store.MemoRelation{ + MemoID: relation.MemoID, + RelatedMemoID: relation.RelatedMemoID, + Type: store.MemoRelationType(relation.Type), + }) } } for _, relation := range newList { key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type) if !oldMap[key] { - addedList = append(addedList, relation) + addedList = append(addedList, &store.MemoRelation{ + MemoID: relation.MemoID, + RelatedMemoID: relation.RelatedMemoID, + Type: store.MemoRelationType(relation.Type), + }) } } return addedList, removedList @@ -944,3 +982,81 @@ func getIDListDiff(oldList, newList []int32) (addedList, removedList []int32) { } return addedList, removedList } + +// DispatchMemoCreatedWebhook dispatches webhook when memo is created. +func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *Memo) error { + return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created") +} + +// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated. +func (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *Memo) error { + return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated") +} + +// DispatchMemoDeletedWebhook dispatches webhook when memo is deletedd. +func (s *APIV1Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *Memo) error { + return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted") +} + +func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *Memo, activityType string) error { + webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{ + CreatorID: &memo.CreatorID, + }) + if err != nil { + return err + } + for _, hook := range webhooks { + payload := convertMemoToWebhookPayload(memo) + payload.ActivityType = activityType + payload.URL = hook.Url + err := webhook.Post(*payload) + if err != nil { + return errors.Wrap(err, "failed to post webhook") + } + } + return nil +} + +func convertMemoToWebhookPayload(memo *Memo) *webhook.WebhookPayload { + return &webhook.WebhookPayload{ + CreatorID: memo.CreatorID, + CreatedTs: time.Now().Unix(), + Memo: &webhook.Memo{ + ID: memo.ID, + CreatorID: memo.CreatorID, + CreatedTs: memo.CreatedTs, + UpdatedTs: memo.UpdatedTs, + Content: memo.Content, + Visibility: memo.Visibility.String(), + Pinned: memo.Pinned, + ResourceList: func() []*webhook.Resource { + resources := []*webhook.Resource{} + for _, resource := range memo.ResourceList { + resources = append(resources, &webhook.Resource{ + ID: resource.ID, + CreatorID: resource.CreatorID, + CreatedTs: resource.CreatedTs, + UpdatedTs: resource.UpdatedTs, + Filename: resource.Filename, + InternalPath: resource.InternalPath, + ExternalLink: resource.ExternalLink, + Type: resource.Type, + Size: resource.Size, + }) + } + return resources + }(), + RelationList: func() []*webhook.MemoRelation { + relations := []*webhook.MemoRelation{} + for _, relation := range memo.RelationList { + relations = append(relations, &webhook.MemoRelation{ + MemoID: relation.MemoID, + RelatedMemoID: relation.RelatedMemoID, + Type: relation.Type.String(), + }) + } + return relations + }(), + }, + } +} diff --git a/api/v1/memo_organizer.go b/server/route/api/v1/memo_organizer.go similarity index 100% rename from api/v1/memo_organizer.go rename to server/route/api/v1/memo_organizer.go diff --git a/api/v1/memo_relation.go b/server/route/api/v1/memo_relation.go similarity index 98% rename from api/v1/memo_relation.go rename to server/route/api/v1/memo_relation.go index 61350af8b4244..cd0e818dc8923 100644 --- a/api/v1/memo_relation.go +++ b/server/route/api/v1/memo_relation.go @@ -18,6 +18,10 @@ const ( MemoRelationComment MemoRelationType = "COMMENT" ) +func (t MemoRelationType) String() string { + return string(t) +} + type MemoRelation struct { MemoID int32 `json:"memoId"` RelatedMemoID int32 `json:"relatedMemoId"` diff --git a/api/v1/resource.go b/server/route/api/v1/resource.go similarity index 89% rename from api/v1/resource.go rename to server/route/api/v1/resource.go index 959fb8fc5e943..6f51169533ae7 100644 --- a/api/v1/resource.go +++ b/server/route/api/v1/resource.go @@ -15,17 +15,17 @@ import ( "time" "github.com/labstack/echo/v4" + "github.com/lithammer/shortuuid/v4" "github.com/pkg/errors" - "go.uber.org/zap" - "github.com/usememos/memos/internal/log" "github.com/usememos/memos/internal/util" "github.com/usememos/memos/plugin/storage/s3" "github.com/usememos/memos/store" ) type Resource struct { - ID int32 `json:"id"` + ID int32 `json:"id"` + Name string `json:"name"` // Standard fields CreatorID int32 `json:"creatorId"` @@ -138,6 +138,7 @@ func (s *APIV1Service) CreateResource(c echo.Context) error { } create := &store.Resource{ + ResourceName: shortuuid.New(), CreatorID: userID, Filename: request.Filename, ExternalLink: request.ExternalLink, @@ -180,14 +181,20 @@ func (s *APIV1Service) UploadResource(c echo.Context) error { return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") } - // This is the backend default max upload size limit. - maxUploadSetting := s.Store.GetSystemSettingValueWithDefault(ctx, SystemSettingMaxUploadSizeMiBName.String(), "32") + maxUploadSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingMaxUploadSizeMiBName.String()}) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get max upload size").SetInternal(err) + } var settingMaxUploadSizeBytes int - if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting); err == nil { - settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte + if maxUploadSetting != nil { + if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting.Value); err == nil { + settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte + } else { + settingMaxUploadSizeBytes = 0 + } } else { - log.Warn("Failed to parse max upload size", zap.Error(err)) - settingMaxUploadSizeBytes = 0 + // Default to 32 MiB. + settingMaxUploadSizeBytes = 32 * MebiByte } file, err := c.FormFile("file") @@ -213,10 +220,11 @@ func (s *APIV1Service) UploadResource(c echo.Context) error { defer sourceFile.Close() create := &store.Resource{ - CreatorID: userID, - Filename: file.Filename, - Type: file.Header.Get("Content-Type"), - Size: file.Size, + ResourceName: shortuuid.New(), + CreatorID: userID, + Filename: file.Filename, + Type: file.Header.Get("Content-Type"), + Size: file.Size, } err = SaveResourceBlob(ctx, s.Store, create, sourceFile) if err != nil { @@ -352,6 +360,8 @@ func replacePathTemplate(path, filename string) string { return fmt.Sprintf("%02d", t.Minute()) case "{second}": return fmt.Sprintf("%02d", t.Second()) + case "{uuid}": + return util.GenUUID() } return s }) @@ -361,6 +371,7 @@ func replacePathTemplate(path, filename string) string { func convertResourceFromStore(resource *store.Resource) *Resource { return &Resource{ ID: resource.ID, + Name: resource.ResourceName, CreatorID: resource.CreatorID, CreatedTs: resource.CreatedTs, UpdatedTs: resource.UpdatedTs, @@ -380,7 +391,7 @@ func convertResourceFromStore(resource *store.Resource) *Resource { // 2. *LocalStorage*: `create.InternalPath`. // 3. Others( external service): `create.ExternalLink`. func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resource, r io.Reader) error { - systemSettingStorageServiceID, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()}) + systemSettingStorageServiceID, err := s.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingStorageServiceIDName.String()}) if err != nil { return errors.Wrap(err, "Failed to find SystemSettingStorageServiceIDName") } @@ -403,7 +414,7 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc return nil } else if storageServiceID == LocalStorage { // `LocalStorage` means save blob into local disk - systemSettingLocalStoragePath, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingLocalStoragePathName.String()}) + systemSettingLocalStoragePath, err := s.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingLocalStoragePathName.String()}) if err != nil { return errors.Wrap(err, "Failed to find SystemSettingLocalStoragePathName") } @@ -414,17 +425,24 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc return errors.Wrap(err, "Failed to unmarshal SystemSettingLocalStoragePathName") } } - filePath := filepath.FromSlash(localStoragePath) - if !strings.Contains(filePath, "{filename}") { - filePath = filepath.Join(filePath, "{filename}") + + internalPath := localStoragePath + if !strings.Contains(internalPath, "{filename}") { + internalPath = filepath.Join(internalPath, "{filename}") } - filePath = filepath.Join(s.Profile.Data, replacePathTemplate(filePath, create.Filename)) + internalPath = replacePathTemplate(internalPath, create.Filename) + internalPath = filepath.ToSlash(internalPath) + create.InternalPath = internalPath - dir := filepath.Dir(filePath) + osPath := filepath.FromSlash(internalPath) + if !filepath.IsAbs(osPath) { + osPath = filepath.Join(s.Profile.Data, osPath) + } + dir := filepath.Dir(osPath) if err = os.MkdirAll(dir, os.ModePerm); err != nil { return errors.Wrap(err, "Failed to create directory") } - dst, err := os.Create(filePath) + dst, err := os.Create(osPath) if err != nil { return errors.Wrap(err, "Failed to create file") } @@ -434,7 +452,6 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc return errors.Wrap(err, "Failed to copy file") } - create.InternalPath = filePath return nil } @@ -464,6 +481,7 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc Bucket: s3Config.Bucket, URLPrefix: s3Config.URLPrefix, URLSuffix: s3Config.URLSuffix, + PreSign: s3Config.PreSign, }) if err != nil { return errors.Wrap(err, "Failed to create s3 client") diff --git a/api/v1/storage.go b/server/route/api/v1/storage.go similarity index 98% rename from api/v1/storage.go rename to server/route/api/v1/storage.go index 4479a1d064f3e..a18cae5a5e85f 100644 --- a/api/v1/storage.go +++ b/server/route/api/v1/storage.go @@ -43,6 +43,7 @@ type StorageS3Config struct { Bucket string `json:"bucket"` URLPrefix string `json:"urlPrefix"` URLSuffix string `json:"urlSuffix"` + PreSign bool `json:"presign"` } type Storage struct { @@ -208,7 +209,7 @@ func (s *APIV1Service) DeleteStorage(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err) } - systemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()}) + systemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingStorageServiceIDName.String()}) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err) } diff --git a/server/route/api/v1/swagger.md b/server/route/api/v1/swagger.md new file mode 100644 index 0000000000000..103cde334715a --- /dev/null +++ b/server/route/api/v1/swagger.md @@ -0,0 +1,1708 @@ +# memos API +A privacy-first, lightweight note-taking service. + +## Version: 1.0 + +**Contact information:** +API Support +https://github.com/orgs/usememos/discussions + +**License:** [MIT License](https://github.com/usememos/memos/blob/main/LICENSE) + +[Find out more about Memos.](https://usememos.com/) + +--- +### /api/v1/auth/signin + +#### POST +##### Summary + +Sign-in to memos. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Sign-in object | Yes | [github_com_usememos_memos_api_v1.SignIn](#github_com_usememos_memos_api_v1signin) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | User information | [store.User](#storeuser) | +| 400 | Malformatted signin request | | +| 401 | Password login is deactivated \| Incorrect login credentials, please try again | | +| 403 | User has been archived with username %s | | +| 500 | Failed to find system setting \| Failed to unmarshal system setting \| Incorrect login credentials, please try again \| Failed to generate tokens \| Failed to create activity | | + +### /api/v1/auth/signin/sso + +#### POST +##### Summary + +Sign-in to memos using SSO. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | SSO sign-in object | Yes | [github_com_usememos_memos_api_v1.SSOSignIn](#github_com_usememos_memos_api_v1ssosignin) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | User information | [store.User](#storeuser) | +| 400 | Malformatted signin request | | +| 401 | Access denied, identifier does not match the filter. | | +| 403 | User has been archived with username {username} | | +| 404 | Identity provider not found | | +| 500 | Failed to find identity provider \| Failed to create identity provider instance \| Failed to exchange token \| Failed to get user info \| Failed to compile identifier filter \| Incorrect login credentials, please try again \| Failed to generate random password \| Failed to generate password hash \| Failed to create user \| Failed to generate tokens \| Failed to create activity | | + +### /api/v1/auth/signout + +#### POST +##### Summary + +Sign-out from memos. + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Sign-out success | boolean | + +### /api/v1/auth/signup + +#### POST +##### Summary + +Sign-up to memos. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Sign-up object | Yes | [github_com_usememos_memos_api_v1.SignUp](#github_com_usememos_memos_api_v1signup) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | User information | [store.User](#storeuser) | +| 400 | Malformatted signup request \| Failed to find users | | +| 401 | signup is disabled | | +| 403 | Forbidden | | +| 404 | Not found | | +| 500 | Failed to find system setting \| Failed to unmarshal system setting allow signup \| Failed to generate password hash \| Failed to create user \| Failed to generate tokens \| Failed to create activity | | + +--- +### /api/v1/idp + +#### GET +##### Summary + +Get a list of identity providers + +##### Description + +*clientSecret is only available for host user + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List of available identity providers | [ [api_v1.IdentityProvider](#api_v1identityprovider) ] | +| 500 | Failed to find identity provider list \| Failed to find user | | + +#### POST +##### Summary + +Create Identity Provider + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Identity provider information | Yes | [api_v1.CreateIdentityProviderRequest](#api_v1createidentityproviderrequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Identity provider information | [store.IdentityProvider](#storeidentityprovider) | +| 400 | Malformatted post identity provider request | | +| 401 | Missing user in session \| Unauthorized | | +| 500 | Failed to find user \| Failed to create identity provider | | + +### /api/v1/idp/{idpId} + +#### DELETE +##### Summary + +Delete an identity provider by ID + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| idpId | path | Identity Provider ID | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Identity Provider deleted | boolean | +| 400 | ID is not a number: %s \| Malformatted patch identity provider request | | +| 401 | Missing user in session \| Unauthorized | | +| 500 | Failed to find user \| Failed to patch identity provider | | + +#### GET +##### Summary + +Get an identity provider by ID + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| idpId | path | Identity provider ID | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Requested identity provider | [store.IdentityProvider](#storeidentityprovider) | +| 400 | ID is not a number: %s | | +| 401 | Missing user in session \| Unauthorized | | +| 404 | Identity provider not found | | +| 500 | Failed to find identity provider list \| Failed to find user | | + +#### PATCH +##### Summary + +Update an identity provider by ID + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| idpId | path | Identity Provider ID | Yes | integer | +| body | body | Patched identity provider information | Yes | [api_v1.UpdateIdentityProviderRequest](#api_v1updateidentityproviderrequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Patched identity provider | [store.IdentityProvider](#storeidentityprovider) | +| 400 | ID is not a number: %s \| Malformatted patch identity provider request | | +| 401 | Missing user in session \| Unauthorized | | +| 500 | Failed to find user \| Failed to patch identity provider | | + +--- +### /api/v1/memo + +#### GET +##### Summary + +Get a list of memos matching optional filters + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| creatorId | query | Creator ID | No | integer | +| creatorUsername | query | Creator username | No | string | +| rowStatus | query | Row status | No | string | +| pinned | query | Pinned | No | boolean | +| tag | query | Search for tag. Do not append # | No | string | +| content | query | Search for content | No | string | +| limit | query | Limit | No | integer | +| offset | query | Offset | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Memo list | [ [store.Memo](#storememo) ] | +| 400 | Missing user to find memo | | +| 500 | Failed to get memo display with updated ts setting value \| Failed to fetch memo list \| Failed to compose memo response | | + +#### POST +##### Summary + +Create a memo + +##### Description + +Visibility can be PUBLIC, PROTECTED or PRIVATE +*You should omit fields to use their default values + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.CreateMemoRequest](#github_com_usememos_memos_api_v1creatememorequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Stored memo | [store.Memo](#storememo) | +| 400 | Malformatted post memo request \| Content size overflow, up to 1MB | | +| 401 | Missing user in session | | +| 404 | User not found \| Memo not found: %d | | +| 500 | Failed to find user setting \| Failed to unmarshal user setting value \| Failed to find system setting \| Failed to unmarshal system setting \| Failed to find user \| Failed to create memo \| Failed to create activity \| Failed to upsert memo resource \| Failed to upsert memo relation \| Failed to compose memo \| Failed to compose memo response | | + +### /api/v1/memo/{memoId} + +#### DELETE +##### Summary + +Delete memo by ID + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| memoId | path | Memo ID to delete | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Memo deleted | boolean | +| 400 | ID is not a number: %s | | +| 401 | Missing user in session \| Unauthorized | | +| 404 | Memo not found: %d | | +| 500 | Failed to find memo \| Failed to delete memo ID: %v | | + +#### GET +##### Summary + +Get memo by ID + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| memoId | path | Memo ID | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Memo list | [ [store.Memo](#storememo) ] | +| 400 | ID is not a number: %s | | +| 401 | Missing user in session | | +| 403 | this memo is private only \| this memo is protected, missing user in session | | +| 404 | Memo not found: %d | | +| 500 | Failed to find memo by ID: %v \| Failed to compose memo response | | + +#### PATCH +##### Summary + +Update a memo + +##### Description + +Visibility can be PUBLIC, PROTECTED or PRIVATE +*You should omit fields to use their default values + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| memoId | path | ID of memo to update | Yes | integer | +| body | body | Patched object. | Yes | [github_com_usememos_memos_api_v1.PatchMemoRequest](#github_com_usememos_memos_api_v1patchmemorequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Stored memo | [store.Memo](#storememo) | +| 400 | ID is not a number: %s \| Malformatted patch memo request \| Content size overflow, up to 1MB | | +| 401 | Missing user in session \| Unauthorized | | +| 404 | Memo not found: %d | | +| 500 | Failed to find memo \| Failed to patch memo \| Failed to upsert memo resource \| Failed to delete memo resource \| Failed to compose memo response | | + +### /api/v1/memo/all + +#### GET +##### Summary + +Get a list of public memos matching optional filters + +##### Description + +This should also list protected memos if the user is logged in +Authentication is optional + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| limit | query | Limit | No | integer | +| offset | query | Offset | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Memo list | [ [store.Memo](#storememo) ] | +| 500 | Failed to get memo display with updated ts setting value \| Failed to fetch all memo list \| Failed to compose memo response | | + +### /api/v1/memo/stats + +#### GET +##### Summary + +Get memo stats by creator ID or username + +##### Description + +Used to generate the heatmap + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| creatorId | query | Creator ID | No | integer | +| creatorUsername | query | Creator username | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Memo createdTs list | [ integer ] | +| 400 | Missing user id to find memo | | +| 500 | Failed to get memo display with updated ts setting value \| Failed to find memo list \| Failed to compose memo response | | + +--- +### /api/v1/memo/{memoId}/organizer + +#### POST +##### Summary + +Organize memo (pin/unpin) + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| memoId | path | ID of memo to organize | Yes | integer | +| body | body | Memo organizer object | Yes | [github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest](#github_com_usememos_memos_api_v1upsertmemoorganizerrequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Memo information | [store.Memo](#storememo) | +| 400 | ID is not a number: %s \| Malformatted post memo organizer request | | +| 401 | Missing user in session \| Unauthorized | | +| 404 | Memo not found: %v | | +| 500 | Failed to find memo \| Failed to upsert memo organizer \| Failed to find memo by ID: %v \| Failed to compose memo response | | + +--- +### /api/v1/memo/{memoId}/relation + +#### GET +##### Summary + +Get a list of Memo Relations + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| memoId | path | ID of memo to find relations | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Memo relation information list | [ [store.MemoRelation](#storememorelation) ] | +| 400 | ID is not a number: %s | | +| 500 | Failed to list memo relations | | + +#### POST +##### Summary + +Create Memo Relation + +##### Description + +Create a relation between two memos + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| memoId | path | ID of memo to relate | Yes | integer | +| body | body | Memo relation object | Yes | [github_com_usememos_memos_api_v1.UpsertMemoRelationRequest](#github_com_usememos_memos_api_v1upsertmemorelationrequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Memo relation information | [store.MemoRelation](#storememorelation) | +| 400 | ID is not a number: %s \| Malformatted post memo relation request | | +| 500 | Failed to upsert memo relation | | + +### /api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType} + +#### DELETE +##### Summary + +Delete a Memo Relation + +##### Description + +Removes a relation between two memos + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| memoId | path | ID of memo to find relations | Yes | integer | +| relatedMemoId | path | ID of memo to remove relation to | Yes | integer | +| relationType | path | Type of relation to remove | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Memo relation deleted | boolean | +| 400 | Memo ID is not a number: %s \| Related memo ID is not a number: %s | | +| 500 | Failed to delete memo relation | | + +--- +### /api/v1/ping + +#### GET +##### Summary + +Ping the system + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | If succeed to ping the system | boolean | + +### /api/v1/status + +#### GET +##### Summary + +Get system GetSystemStatus + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | System GetSystemStatus | [api_v1.SystemStatus](#api_v1systemstatus) | +| 401 | Missing user in session \| Unauthorized | | +| 500 | Failed to find host user \| Failed to find system setting list \| Failed to unmarshal system setting customized profile value | | + +### /api/v1/system/vacuum + +#### POST +##### Summary + +Vacuum the database + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Database vacuumed | boolean | +| 401 | Missing user in session \| Unauthorized | | +| 500 | Failed to find user \| Failed to ExecVacuum database | | + +--- +### /api/v1/resource + +#### GET +##### Summary + +Get a list of resources + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| limit | query | Limit | No | integer | +| offset | query | Offset | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Resource list | [ [store.Resource](#storeresource) ] | +| 401 | Missing user in session | | +| 500 | Failed to fetch resource list | | + +#### POST +##### Summary + +Create resource + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Request object. | Yes | [api_v1.CreateResourceRequest](#api_v1createresourcerequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Created resource | [store.Resource](#storeresource) | +| 400 | Malformatted post resource request \| Invalid external link \| Invalid external link scheme \| Failed to request %s \| Failed to read %s \| Failed to read mime from %s | | +| 401 | Missing user in session | | +| 500 | Failed to save resource \| Failed to create resource \| Failed to create activity | | + +### /api/v1/resource/{resourceId} + +#### DELETE +##### Summary + +Delete a resource + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| resourceId | path | Resource ID | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Resource deleted | boolean | +| 400 | ID is not a number: %s | | +| 401 | Missing user in session | | +| 404 | Resource not found: %d | | +| 500 | Failed to find resource \| Failed to delete resource | | + +#### PATCH +##### Summary + +Update a resource + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| resourceId | path | Resource ID | Yes | integer | +| patch | body | Patch resource request | Yes | [api_v1.UpdateResourceRequest](#api_v1updateresourcerequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Updated resource | [store.Resource](#storeresource) | +| 400 | ID is not a number: %s \| Malformatted patch resource request | | +| 401 | Missing user in session \| Unauthorized | | +| 404 | Resource not found: %d | | +| 500 | Failed to find resource \| Failed to patch resource | | + +### /api/v1/resource/blob + +#### POST +##### Summary + +Upload resource + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| file | formData | File to upload | Yes | file | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Created resource | [store.Resource](#storeresource) | +| 400 | Upload file not found \| File size exceeds allowed limit of %d MiB \| Failed to parse upload data | | +| 401 | Missing user in session | | +| 500 | Failed to get uploading file \| Failed to open file \| Failed to save resource \| Failed to create resource \| Failed to create activity | | + +--- +### /api/v1/storage + +#### GET +##### Summary + +Get a list of storages + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List of storages | [ [store.Storage](#storestorage) ] | +| 401 | Missing user in session \| Unauthorized | | +| 500 | Failed to find user \| Failed to convert storage | | + +#### POST +##### Summary + +Create storage + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.CreateStorageRequest](#github_com_usememos_memos_api_v1createstoragerequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Created storage | [store.Storage](#storestorage) | +| 400 | Malformatted post storage request | | +| 401 | Missing user in session | | +| 500 | Failed to find user \| Failed to create storage \| Failed to convert storage | | + +### /api/v1/storage/{storageId} + +#### DELETE +##### Summary + +Delete a storage + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| storageId | path | Storage ID | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Storage deleted | boolean | +| 400 | ID is not a number: %s \| Storage service %d is using | | +| 401 | Missing user in session \| Unauthorized | | +| 500 | Failed to find user \| Failed to find storage \| Failed to unmarshal storage service id \| Failed to delete storage | | + +#### PATCH +##### Summary + +Update a storage + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| storageId | path | Storage ID | Yes | integer | +| patch | body | Patch request | Yes | [github_com_usememos_memos_api_v1.UpdateStorageRequest](#github_com_usememos_memos_api_v1updatestoragerequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Updated resource | [store.Storage](#storestorage) | +| 400 | ID is not a number: %s \| Malformatted patch storage request \| Malformatted post storage request | | +| 401 | Missing user in session \| Unauthorized | | +| 500 | Failed to find user \| Failed to patch storage \| Failed to convert storage | | + +--- +### /api/v1/system/setting + +#### GET +##### Summary + +Get a list of system settings + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | System setting list | [ [api_v1.SystemSetting](#api_v1systemsetting) ] | +| 401 | Missing user in session \| Unauthorized | | +| 500 | Failed to find user \| Failed to find system setting list | | + +#### POST +##### Summary + +Create system setting + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Request object. | Yes | [api_v1.UpsertSystemSettingRequest](#api_v1upsertsystemsettingrequest) | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 400 | Malformatted post system setting request \| invalid system setting | +| 401 | Missing user in session \| Unauthorized | +| 403 | Cannot disable passwords if no SSO identity provider is configured. | +| 500 | Failed to find user \| Failed to upsert system setting | + +--- +### /api/v1/tag + +#### GET +##### Summary + +Get a list of tags + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Tag list | [ string ] | +| 400 | Missing user id to find tag | | +| 500 | Failed to find tag list | | + +#### POST +##### Summary + +Create a tag + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.UpsertTagRequest](#github_com_usememos_memos_api_v1upserttagrequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Created tag name | string | +| 400 | Malformatted post tag request \| Tag name shouldn't be empty | | +| 401 | Missing user in session | | +| 500 | Failed to upsert tag \| Failed to create activity | | + +### /api/v1/tag/delete + +#### POST +##### Summary + +Delete a tag + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.DeleteTagRequest](#github_com_usememos_memos_api_v1deletetagrequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Tag deleted | boolean | +| 400 | Malformatted post tag request \| Tag name shouldn't be empty | | +| 401 | Missing user in session | | +| 500 | Failed to delete tag name: %v | | + +### /api/v1/tag/suggestion + +#### GET +##### Summary + +Get a list of tags suggested from other memos contents + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Tag list | [ string ] | +| 400 | Missing user session | | +| 500 | Failed to find memo list \| Failed to find tag list | | + +--- +### /api/v1/user + +#### GET +##### Summary + +Get a list of users + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | User list | [ [store.User](#storeuser) ] | +| 500 | Failed to fetch user list | | + +#### POST +##### Summary + +Create a user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | Request object | Yes | [api_v1.CreateUserRequest](#api_v1createuserrequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Created user | [store.User](#storeuser) | +| 400 | Malformatted post user request \| Invalid user create format | | +| 401 | Missing auth session \| Unauthorized to create user | | +| 403 | Could not create host user | | +| 500 | Failed to find user by id \| Failed to generate password hash \| Failed to create user \| Failed to create activity | | + +### /api/v1/user/{id} + +#### DELETE +##### Summary + +Delete a user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | User ID | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | User deleted | boolean | +| 400 | ID is not a number: %s \| Current session user not found with ID: %d | | +| 401 | Missing user in session | | +| 403 | Unauthorized to delete user | | +| 500 | Failed to find user \| Failed to delete user | | + +#### GET +##### Summary + +Get user by id + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | User ID | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Requested user | [store.User](#storeuser) | +| 400 | Malformatted user id | | +| 404 | User not found | | +| 500 | Failed to find user | | + +#### PATCH +##### Summary + +Update a user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | User ID | Yes | string | +| patch | body | Patch request | Yes | [api_v1.UpdateUserRequest](#api_v1updateuserrequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Updated user | [store.User](#storeuser) | +| 400 | ID is not a number: %s \| Current session user not found with ID: %d \| Malformatted patch user request \| Invalid update user request | | +| 401 | Missing user in session | | +| 403 | Unauthorized to update user | | +| 500 | Failed to find user \| Failed to generate password hash \| Failed to patch user \| Failed to find userSettingList | | + +### /api/v1/user/me + +#### GET +##### Summary + +Get current user + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Current user | [store.User](#storeuser) | +| 401 | Missing auth session | | +| 500 | Failed to find user \| Failed to find userSettingList | | + +### /api/v1/user/name/{username} + +#### GET +##### Summary + +Get user by username + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| username | path | Username | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Requested user | [store.User](#storeuser) | +| 404 | User not found | | +| 500 | Failed to find user | | + +--- +### /o/get/GetImage + +#### GET +##### Summary + +Get GetImage from URL + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| url | query | Image url | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Image | +| 400 | Missing GetImage url \| Wrong url \| Failed to get GetImage url: %s | +| 500 | Failed to write GetImage blob | + +--- +### Models + +#### api_v1.CreateIdentityProviderRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [api_v1.IdentityProviderConfig](#api_v1identityproviderconfig) | | No | +| identifierFilter | string | | No | +| name | string | | No | +| type | [api_v1.IdentityProviderType](#api_v1identityprovidertype) | | No | + +#### api_v1.CreateMemoRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | string | | No | +| createdTs | integer | | No | +| relationList | [ [api_v1.UpsertMemoRelationRequest](#api_v1upsertmemorelationrequest) ] | | No | +| resourceIdList | [ integer ] | Related fields | No | +| visibility | [api_v1.Visibility](#api_v1visibility) | Domain specific fields | No | + +#### api_v1.CreateResourceRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| externalLink | string | | No | +| filename | string | | No | +| type | string | | No | + +#### api_v1.CreateStorageRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [api_v1.StorageConfig](#api_v1storageconfig) | | No | +| name | string | | No | +| type | [api_v1.StorageType](#api_v1storagetype) | | No | + +#### api_v1.CreateUserRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | | No | +| nickname | string | | No | +| password | string | | No | +| role | [api_v1.Role](#api_v1role) | | No | +| username | string | | No | + +#### api_v1.CustomizedProfile + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| appearance | string | Appearance is the server default appearance. | No | +| description | string | Description is the server description. | No | +| locale | string | Locale is the server default locale. | No | +| logoUrl | string | LogoURL is the url of logo image. | No | +| name | string | Name is the server name, default is `memos` | No | + +#### api_v1.DeleteTagRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | + +#### api_v1.FieldMapping + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| displayName | string | | No | +| email | string | | No | +| identifier | string | | No | + +#### api_v1.IdentityProvider + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [api_v1.IdentityProviderConfig](#api_v1identityproviderconfig) | | No | +| id | integer | | No | +| identifierFilter | string | | No | +| name | string | | No | +| type | [api_v1.IdentityProviderType](#api_v1identityprovidertype) | | No | + +#### api_v1.IdentityProviderConfig + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| oauth2Config | [api_v1.IdentityProviderOAuth2Config](#api_v1identityprovideroauth2config) | | No | + +#### api_v1.IdentityProviderOAuth2Config + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| authUrl | string | | No | +| clientId | string | | No | +| clientSecret | string | | No | +| fieldMapping | [api_v1.FieldMapping](#api_v1fieldmapping) | | No | +| scopes | [ string ] | | No | +| tokenUrl | string | | No | +| userInfoUrl | string | | No | + +#### api_v1.IdentityProviderType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| api_v1.IdentityProviderType | string | | | + +#### api_v1.MemoRelationType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| api_v1.MemoRelationType | string | | | + +#### api_v1.PatchMemoRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | string | Domain specific fields | No | +| createdTs | integer | Standard fields | No | +| relationList | [ [api_v1.UpsertMemoRelationRequest](#api_v1upsertmemorelationrequest) ] | | No | +| resourceIdList | [ integer ] | Related fields | No | +| rowStatus | [api_v1.RowStatus](#api_v1rowstatus) | | No | +| updatedTs | integer | | No | +| visibility | [api_v1.Visibility](#api_v1visibility) | | No | + +#### api_v1.Role + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| api_v1.Role | string | | | + +#### api_v1.RowStatus + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| api_v1.RowStatus | string | | | + +#### api_v1.SSOSignIn + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| code | string | | No | +| identityProviderId | integer | | No | +| redirectUri | string | | No | + +#### api_v1.SignIn + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| password | string | | No | +| remember | boolean | | No | +| username | string | | No | + +#### api_v1.SignUp + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| password | string | | No | +| username | string | | No | + +#### api_v1.StorageConfig + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| s3Config | [api_v1.StorageS3Config](#api_v1storages3config) | | No | + +#### api_v1.StorageS3Config + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| accessKey | string | | No | +| bucket | string | | No | +| endPoint | string | | No | +| path | string | | No | +| presign | boolean | | No | +| region | string | | No | +| secretKey | string | | No | +| urlPrefix | string | | No | +| urlSuffix | string | | No | + +#### api_v1.StorageType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| api_v1.StorageType | string | | | + +#### api_v1.SystemSetting + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| description | string | | No | +| name | [api_v1.SystemSettingName](#api_v1systemsettingname) | | No | +| value | string | Value is a JSON string with basic value. | No | + +#### api_v1.SystemSettingName + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| api_v1.SystemSettingName | string | | | + +#### api_v1.SystemStatus + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| additionalScript | string | Additional script. | No | +| additionalStyle | string | Additional style. | No | +| allowSignUp | boolean | System settings Allow sign up. | No | +| customizedProfile | [api_v1.CustomizedProfile](#api_v1customizedprofile) | Customized server profile, including server name and external url. | No | +| dbSize | integer | | No | +| disablePasswordLogin | boolean | Disable password login. | No | +| disablePublicMemos | boolean | Disable public memos. | No | +| host | [api_v1.User](#api_v1user) | | No | +| localStoragePath | string | Local storage path. | No | +| maxUploadSizeMiB | integer | Max upload size. | No | +| memoDisplayWithUpdatedTs | boolean | Memo display with updated timestamp. | No | +| profile | [profile.Profile](#profileprofile) | | No | +| storageServiceId | integer | Storage service ID. | No | + +#### api_v1.UpdateIdentityProviderRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [api_v1.IdentityProviderConfig](#api_v1identityproviderconfig) | | No | +| identifierFilter | string | | No | +| name | string | | No | +| type | [api_v1.IdentityProviderType](#api_v1identityprovidertype) | | No | + +#### api_v1.UpdateResourceRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| filename | string | | No | + +#### api_v1.UpdateStorageRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [api_v1.StorageConfig](#api_v1storageconfig) | | No | +| name | string | | No | +| type | [api_v1.StorageType](#api_v1storagetype) | | No | + +#### api_v1.UpdateUserRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| avatarUrl | string | | No | +| email | string | | No | +| nickname | string | | No | +| password | string | | No | +| rowStatus | [api_v1.RowStatus](#api_v1rowstatus) | | No | +| username | string | | No | + +#### api_v1.UpsertMemoOrganizerRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| pinned | boolean | | No | + +#### api_v1.UpsertMemoRelationRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| relatedMemoId | integer | | No | +| type | [api_v1.MemoRelationType](#api_v1memorelationtype) | | No | + +#### api_v1.UpsertSystemSettingRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| description | string | | No | +| name | [api_v1.SystemSettingName](#api_v1systemsettingname) | | No | +| value | string | | No | + +#### api_v1.UpsertTagRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | + +#### api_v1.User + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| avatarUrl | string | | No | +| createdTs | integer | | No | +| email | string | | No | +| id | integer | | No | +| nickname | string | | No | +| role | [api_v1.Role](#api_v1role) | | No | +| rowStatus | [api_v1.RowStatus](#api_v1rowstatus) | Standard fields | No | +| updatedTs | integer | | No | +| username | string | Domain specific fields | No | + +#### api_v1.Visibility + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| api_v1.Visibility | string | | | + +#### github_com_usememos_memos_api_v1.CreateIdentityProviderRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [github_com_usememos_memos_api_v1.IdentityProviderConfig](#github_com_usememos_memos_api_v1identityproviderconfig) | | No | +| identifierFilter | string | | No | +| name | string | | No | +| type | [github_com_usememos_memos_api_v1.IdentityProviderType](#github_com_usememos_memos_api_v1identityprovidertype) | | No | + +#### github_com_usememos_memos_api_v1.CreateMemoRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | string | | No | +| createdTs | integer | | No | +| relationList | [ [github_com_usememos_memos_api_v1.UpsertMemoRelationRequest](#github_com_usememos_memos_api_v1upsertmemorelationrequest) ] | | No | +| resourceIdList | [ integer ] | Related fields | No | +| visibility | [github_com_usememos_memos_api_v1.Visibility](#github_com_usememos_memos_api_v1visibility) | Domain specific fields | No | + +#### github_com_usememos_memos_api_v1.CreateResourceRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| externalLink | string | | No | +| filename | string | | No | +| type | string | | No | + +#### github_com_usememos_memos_api_v1.CreateStorageRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [github_com_usememos_memos_api_v1.StorageConfig](#github_com_usememos_memos_api_v1storageconfig) | | No | +| name | string | | No | +| type | [github_com_usememos_memos_api_v1.StorageType](#github_com_usememos_memos_api_v1storagetype) | | No | + +#### github_com_usememos_memos_api_v1.CreateUserRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | | No | +| nickname | string | | No | +| password | string | | No | +| role | [github_com_usememos_memos_api_v1.Role](#github_com_usememos_memos_api_v1role) | | No | +| username | string | | No | + +#### github_com_usememos_memos_api_v1.CustomizedProfile + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| appearance | string | Appearance is the server default appearance. | No | +| description | string | Description is the server description. | No | +| locale | string | Locale is the server default locale. | No | +| logoUrl | string | LogoURL is the url of logo image. | No | +| name | string | Name is the server name, default is `memos` | No | + +#### github_com_usememos_memos_api_v1.DeleteTagRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | + +#### github_com_usememos_memos_api_v1.FieldMapping + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| displayName | string | | No | +| email | string | | No | +| identifier | string | | No | + +#### github_com_usememos_memos_api_v1.IdentityProvider + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [github_com_usememos_memos_api_v1.IdentityProviderConfig](#github_com_usememos_memos_api_v1identityproviderconfig) | | No | +| id | integer | | No | +| identifierFilter | string | | No | +| name | string | | No | +| type | [github_com_usememos_memos_api_v1.IdentityProviderType](#github_com_usememos_memos_api_v1identityprovidertype) | | No | + +#### github_com_usememos_memos_api_v1.IdentityProviderConfig + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| oauth2Config | [github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config](#github_com_usememos_memos_api_v1identityprovideroauth2config) | | No | + +#### github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| authUrl | string | | No | +| clientId | string | | No | +| clientSecret | string | | No | +| fieldMapping | [github_com_usememos_memos_api_v1.FieldMapping](#github_com_usememos_memos_api_v1fieldmapping) | | No | +| scopes | [ string ] | | No | +| tokenUrl | string | | No | +| userInfoUrl | string | | No | + +#### github_com_usememos_memos_api_v1.IdentityProviderType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| github_com_usememos_memos_api_v1.IdentityProviderType | string | | | + +#### github_com_usememos_memos_api_v1.MemoRelationType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| github_com_usememos_memos_api_v1.MemoRelationType | string | | | + +#### github_com_usememos_memos_api_v1.PatchMemoRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | string | Domain specific fields | No | +| createdTs | integer | Standard fields | No | +| relationList | [ [github_com_usememos_memos_api_v1.UpsertMemoRelationRequest](#github_com_usememos_memos_api_v1upsertmemorelationrequest) ] | | No | +| resourceIdList | [ integer ] | Related fields | No | +| rowStatus | [github_com_usememos_memos_api_v1.RowStatus](#github_com_usememos_memos_api_v1rowstatus) | | No | +| updatedTs | integer | | No | +| visibility | [github_com_usememos_memos_api_v1.Visibility](#github_com_usememos_memos_api_v1visibility) | | No | + +#### github_com_usememos_memos_api_v1.Role + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| github_com_usememos_memos_api_v1.Role | string | | | + +#### github_com_usememos_memos_api_v1.RowStatus + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| github_com_usememos_memos_api_v1.RowStatus | string | | | + +#### github_com_usememos_memos_api_v1.SSOSignIn + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| code | string | | No | +| identityProviderId | integer | | No | +| redirectUri | string | | No | + +#### github_com_usememos_memos_api_v1.SignIn + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| password | string | | No | +| remember | boolean | | No | +| username | string | | No | + +#### github_com_usememos_memos_api_v1.SignUp + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| password | string | | No | +| username | string | | No | + +#### github_com_usememos_memos_api_v1.StorageConfig + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| s3Config | [github_com_usememos_memos_api_v1.StorageS3Config](#github_com_usememos_memos_api_v1storages3config) | | No | + +#### github_com_usememos_memos_api_v1.StorageS3Config + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| accessKey | string | | No | +| bucket | string | | No | +| endPoint | string | | No | +| path | string | | No | +| presign | boolean | | No | +| region | string | | No | +| secretKey | string | | No | +| urlPrefix | string | | No | +| urlSuffix | string | | No | + +#### github_com_usememos_memos_api_v1.StorageType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| github_com_usememos_memos_api_v1.StorageType | string | | | + +#### github_com_usememos_memos_api_v1.SystemSetting + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| description | string | | No | +| name | [github_com_usememos_memos_api_v1.SystemSettingName](#github_com_usememos_memos_api_v1systemsettingname) | | No | +| value | string | Value is a JSON string with basic value. | No | + +#### github_com_usememos_memos_api_v1.SystemSettingName + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| github_com_usememos_memos_api_v1.SystemSettingName | string | | | + +#### github_com_usememos_memos_api_v1.SystemStatus + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| additionalScript | string | Additional script. | No | +| additionalStyle | string | Additional style. | No | +| allowSignUp | boolean | System settings Allow sign up. | No | +| customizedProfile | [github_com_usememos_memos_api_v1.CustomizedProfile](#github_com_usememos_memos_api_v1customizedprofile) | Customized server profile, including server name and external url. | No | +| dbSize | integer | | No | +| disablePasswordLogin | boolean | Disable password login. | No | +| disablePublicMemos | boolean | Disable public memos. | No | +| host | [github_com_usememos_memos_api_v1.User](#github_com_usememos_memos_api_v1user) | | No | +| localStoragePath | string | Local storage path. | No | +| maxUploadSizeMiB | integer | Max upload size. | No | +| memoDisplayWithUpdatedTs | boolean | Memo display with updated timestamp. | No | +| profile | [profile.Profile](#profileprofile) | | No | +| storageServiceId | integer | Storage service ID. | No | + +#### github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [github_com_usememos_memos_api_v1.IdentityProviderConfig](#github_com_usememos_memos_api_v1identityproviderconfig) | | No | +| identifierFilter | string | | No | +| name | string | | No | +| type | [github_com_usememos_memos_api_v1.IdentityProviderType](#github_com_usememos_memos_api_v1identityprovidertype) | | No | + +#### github_com_usememos_memos_api_v1.UpdateResourceRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| filename | string | | No | + +#### github_com_usememos_memos_api_v1.UpdateStorageRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [github_com_usememos_memos_api_v1.StorageConfig](#github_com_usememos_memos_api_v1storageconfig) | | No | +| name | string | | No | +| type | [github_com_usememos_memos_api_v1.StorageType](#github_com_usememos_memos_api_v1storagetype) | | No | + +#### github_com_usememos_memos_api_v1.UpdateUserRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| avatarUrl | string | | No | +| email | string | | No | +| nickname | string | | No | +| password | string | | No | +| rowStatus | [github_com_usememos_memos_api_v1.RowStatus](#github_com_usememos_memos_api_v1rowstatus) | | No | +| username | string | | No | + +#### github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| pinned | boolean | | No | + +#### github_com_usememos_memos_api_v1.UpsertMemoRelationRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| relatedMemoId | integer | | No | +| type | [github_com_usememos_memos_api_v1.MemoRelationType](#github_com_usememos_memos_api_v1memorelationtype) | | No | + +#### github_com_usememos_memos_api_v1.UpsertSystemSettingRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| description | string | | No | +| name | [github_com_usememos_memos_api_v1.SystemSettingName](#github_com_usememos_memos_api_v1systemsettingname) | | No | +| value | string | | No | + +#### github_com_usememos_memos_api_v1.UpsertTagRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | + +#### github_com_usememos_memos_api_v1.User + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| avatarUrl | string | | No | +| createdTs | integer | | No | +| email | string | | No | +| id | integer | | No | +| nickname | string | | No | +| role | [github_com_usememos_memos_api_v1.Role](#github_com_usememos_memos_api_v1role) | | No | +| rowStatus | [github_com_usememos_memos_api_v1.RowStatus](#github_com_usememos_memos_api_v1rowstatus) | Standard fields | No | +| updatedTs | integer | | No | +| username | string | Domain specific fields | No | + +#### github_com_usememos_memos_api_v1.Visibility + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| github_com_usememos_memos_api_v1.Visibility | string | | | + +#### profile.Profile + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| mode | string | Mode can be "prod" or "dev" or "demo" | No | +| version | string | Version is the current version of server | No | + +#### store.FieldMapping + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| displayName | string | | No | +| email | string | | No | +| identifier | string | | No | + +#### store.IdentityProvider + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | [store.IdentityProviderConfig](#storeidentityproviderconfig) | | No | +| id | integer | | No | +| identifierFilter | string | | No | +| name | string | | No | +| type | [store.IdentityProviderType](#storeidentityprovidertype) | | No | + +#### store.IdentityProviderConfig + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| oauth2Config | [store.IdentityProviderOAuth2Config](#storeidentityprovideroauth2config) | | No | + +#### store.IdentityProviderOAuth2Config + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| authUrl | string | | No | +| clientId | string | | No | +| clientSecret | string | | No | +| fieldMapping | [store.FieldMapping](#storefieldmapping) | | No | +| scopes | [ string ] | | No | +| tokenUrl | string | | No | +| userInfoUrl | string | | No | + +#### store.IdentityProviderType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| store.IdentityProviderType | string | | | + +#### store.Memo + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | string | Domain specific fields | No | +| createdTs | integer | | No | +| creatorID | integer | | No | +| id | integer | | No | +| parentID | integer | | No | +| pinned | boolean | Composed fields | No | +| resourceName | string | | No | +| rowStatus | [store.RowStatus](#storerowstatus) | Standard fields | No | +| updatedTs | integer | | No | +| visibility | [store.Visibility](#storevisibility) | | No | + +#### store.MemoRelation + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memoID | integer | | No | +| relatedMemoID | integer | | No | +| type | [store.MemoRelationType](#storememorelationtype) | | No | + +#### store.MemoRelationType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| store.MemoRelationType | string | | | + +#### store.Resource + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| blob | [ integer ] | | No | +| createdTs | integer | | No | +| creatorID | integer | Standard fields | No | +| externalLink | string | | No | +| filename | string | Domain specific fields | No | +| id | integer | | No | +| internalPath | string | | No | +| memoID | integer | | No | +| resourceName | string | | No | +| size | integer | | No | +| type | string | | No | +| updatedTs | integer | | No | + +#### store.Role + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| store.Role | string | | | + +#### store.RowStatus + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| store.RowStatus | string | | | + +#### store.Storage + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| config | string | | No | +| id | integer | | No | +| name | string | | No | +| type | string | | No | + +#### store.User + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| avatarURL | string | | No | +| createdTs | integer | | No | +| email | string | | No | +| id | integer | | No | +| nickname | string | | No | +| passwordHash | string | | No | +| role | [store.Role](#storerole) | | No | +| rowStatus | [store.RowStatus](#storerowstatus) | Standard fields | No | +| updatedTs | integer | | No | +| username | string | Domain specific fields | No | + +#### store.Visibility + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| store.Visibility | string | | | diff --git a/api/v1/swagger.yaml b/server/route/api/v1/swagger.yaml similarity index 75% rename from api/v1/swagger.yaml rename to server/route/api/v1/swagger.yaml index c00bb950ef498..a4161efa3f38f 100644 --- a/api/v1/swagger.yaml +++ b/server/route/api/v1/swagger.yaml @@ -1,32 +1,91 @@ basePath: / definitions: - github_com_usememos_memos_store.UserSetting: + api_v1.CreateIdentityProviderRequest: properties: - key: + config: + $ref: '#/definitions/api_v1.IdentityProviderConfig' + identifierFilter: + type: string + name: + type: string + type: + $ref: '#/definitions/api_v1.IdentityProviderType' + type: object + api_v1.CreateMemoRequest: + properties: + content: type: string - userID: + createdTs: type: integer - value: + relationList: + items: + $ref: '#/definitions/api_v1.UpsertMemoRelationRequest' + type: array + resourceIdList: + description: Related fields + items: + type: integer + type: array + visibility: + allOf: + - $ref: '#/definitions/api_v1.Visibility' + description: Domain specific fields + type: object + api_v1.CreateResourceRequest: + properties: + externalLink: + type: string + filename: + type: string + type: type: string type: object - profile.Profile: + api_v1.CreateStorageRequest: properties: - driver: - description: |- - Driver is the database driver - sqlite, mysql + config: + $ref: '#/definitions/api_v1.StorageConfig' + name: type: string - dsn: - description: DSN points to where Memos stores its own data + type: + $ref: '#/definitions/api_v1.StorageType' + type: object + api_v1.CreateUserRequest: + properties: + email: type: string - mode: - description: Mode can be "prod" or "dev" or "demo" + nickname: type: string - version: - description: Version is the current version of server + password: + type: string + role: + $ref: '#/definitions/api_v1.Role' + username: type: string type: object - store.FieldMapping: + api_v1.CustomizedProfile: + properties: + appearance: + description: Appearance is the server default appearance. + type: string + description: + description: Description is the server description. + type: string + locale: + description: Locale is the server default locale. + type: string + logoUrl: + description: LogoURL is the url of logo image. + type: string + name: + description: Name is the server name, default is `memos` + type: string + type: object + api_v1.DeleteTagRequest: + properties: + name: + type: string + type: object + api_v1.FieldMapping: properties: displayName: type: string @@ -35,10 +94,10 @@ definitions: identifier: type: string type: object - store.IdentityProvider: + api_v1.IdentityProvider: properties: config: - $ref: '#/definitions/store.IdentityProviderConfig' + $ref: '#/definitions/api_v1.IdentityProviderConfig' id: type: integer identifierFilter: @@ -46,14 +105,14 @@ definitions: name: type: string type: - $ref: '#/definitions/store.IdentityProviderType' + $ref: '#/definitions/api_v1.IdentityProviderType' type: object - store.IdentityProviderConfig: + api_v1.IdentityProviderConfig: properties: oauth2Config: - $ref: '#/definitions/store.IdentityProviderOAuth2Config' + $ref: '#/definitions/api_v1.IdentityProviderOAuth2Config' type: object - store.IdentityProviderOAuth2Config: + api_v1.IdentityProviderOAuth2Config: properties: authUrl: type: string @@ -62,7 +121,7 @@ definitions: clientSecret: type: string fieldMapping: - $ref: '#/definitions/store.FieldMapping' + $ref: '#/definitions/api_v1.FieldMapping' scopes: items: type: string @@ -72,95 +131,45 @@ definitions: userInfoUrl: type: string type: object - store.IdentityProviderType: + api_v1.IdentityProviderType: enum: - OAUTH2 type: string x-enum-varnames: - IdentityProviderOAuth2Type - store.Memo: + api_v1.MemoRelationType: + enum: + - REFERENCE + - COMMENT + type: string + x-enum-varnames: + - MemoRelationReference + - MemoRelationComment + api_v1.PatchMemoRequest: properties: content: description: Domain specific fields type: string createdTs: + description: Standard fields type: integer - creatorID: - type: integer - id: - type: integer - parentID: - description: |- - Composed fields - For those comment memos, the parent ID is the memo ID of the memo being commented. - If the parent ID is nil, then this memo is not a comment. - type: integer - pinned: - type: boolean relationList: items: - $ref: '#/definitions/store.MemoRelation' + $ref: '#/definitions/api_v1.UpsertMemoRelationRequest' type: array - resourceIDList: + resourceIdList: + description: Related fields items: type: integer type: array rowStatus: - allOf: - - $ref: '#/definitions/store.RowStatus' - description: Standard fields + $ref: '#/definitions/api_v1.RowStatus' updatedTs: type: integer visibility: - $ref: '#/definitions/store.Visibility' + $ref: '#/definitions/api_v1.Visibility' type: object - store.MemoRelation: - properties: - memoID: - type: integer - relatedMemoID: - type: integer - type: - $ref: '#/definitions/store.MemoRelationType' - type: object - store.MemoRelationType: - enum: - - REFERENCE - - COMMENT - type: string - x-enum-varnames: - - MemoRelationReference - - MemoRelationComment - store.Resource: - properties: - blob: - items: - type: integer - type: array - createdTs: - type: integer - creatorID: - description: Standard fields - type: integer - externalLink: - type: string - filename: - description: Domain specific fields - type: string - id: - type: integer - internalPath: - type: string - memoID: - type: integer - size: - type: integer - type: - type: string - updatedTs: - type: integer - type: object - store.Role: + api_v1.Role: enum: - HOST - ADMIN @@ -170,7 +179,7 @@ definitions: - RoleHost - RoleAdmin - RoleUser - store.RowStatus: + api_v1.RowStatus: enum: - NORMAL - ARCHIVED @@ -178,29 +187,207 @@ definitions: x-enum-varnames: - Normal - Archived - store.Storage: + api_v1.SSOSignIn: properties: - config: + code: type: string - id: + identityProviderId: + type: integer + redirectUri: + type: string + type: object + api_v1.SignIn: + properties: + password: + type: string + remember: + type: boolean + username: + type: string + type: object + api_v1.SignUp: + properties: + password: + type: string + username: + type: string + type: object + api_v1.StorageConfig: + properties: + s3Config: + $ref: '#/definitions/api_v1.StorageS3Config' + type: object + api_v1.StorageS3Config: + properties: + accessKey: + type: string + bucket: + type: string + endPoint: + type: string + path: + type: string + presign: + type: boolean + region: + type: string + secretKey: + type: string + urlPrefix: + type: string + urlSuffix: + type: string + type: object + api_v1.StorageType: + enum: + - S3 + type: string + x-enum-varnames: + - StorageS3 + api_v1.SystemSetting: + properties: + description: + type: string + name: + $ref: '#/definitions/api_v1.SystemSettingName' + value: + description: Value is a JSON string with basic value. + type: string + type: object + api_v1.SystemSettingName: + enum: + - server-id + - secret-session + - disable-public-memos + - max-upload-size-mib + - customized-profile + - storage-service-id + - local-storage-path + - telegram-bot-token + - memo-display-with-updated-ts + type: string + x-enum-varnames: + - SystemSettingServerIDName + - SystemSettingSecretSessionName + - SystemSettingDisablePublicMemosName + - SystemSettingMaxUploadSizeMiBName + - SystemSettingCustomizedProfileName + - SystemSettingStorageServiceIDName + - SystemSettingLocalStoragePathName + - SystemSettingTelegramBotTokenName + - SystemSettingMemoDisplayWithUpdatedTsName + api_v1.SystemStatus: + properties: + additionalScript: + description: Additional script. + type: string + additionalStyle: + description: Additional style. + type: string + allowSignUp: + description: |- + System settings + Allow sign up. + type: boolean + customizedProfile: + allOf: + - $ref: '#/definitions/api_v1.CustomizedProfile' + description: Customized server profile, including server name and external + url. + dbSize: + type: integer + disablePasswordLogin: + description: Disable password login. + type: boolean + disablePublicMemos: + description: Disable public memos. + type: boolean + host: + $ref: '#/definitions/api_v1.User' + localStoragePath: + description: Local storage path. + type: string + maxUploadSizeMiB: + description: Max upload size. + type: integer + memoDisplayWithUpdatedTs: + description: Memo display with updated timestamp. + type: boolean + profile: + $ref: '#/definitions/profile.Profile' + storageServiceId: + description: Storage service ID. type: integer + type: object + api_v1.UpdateIdentityProviderRequest: + properties: + config: + $ref: '#/definitions/api_v1.IdentityProviderConfig' + identifierFilter: + type: string name: type: string type: + $ref: '#/definitions/api_v1.IdentityProviderType' + type: object + api_v1.UpdateResourceRequest: + properties: + filename: + type: string + type: object + api_v1.UpdateStorageRequest: + properties: + config: + $ref: '#/definitions/api_v1.StorageConfig' + name: type: string + type: + $ref: '#/definitions/api_v1.StorageType' type: object - store.SystemSetting: + api_v1.UpdateUserRequest: + properties: + avatarUrl: + type: string + email: + type: string + nickname: + type: string + password: + type: string + rowStatus: + $ref: '#/definitions/api_v1.RowStatus' + username: + type: string + type: object + api_v1.UpsertMemoOrganizerRequest: + properties: + pinned: + type: boolean + type: object + api_v1.UpsertMemoRelationRequest: + properties: + relatedMemoId: + type: integer + type: + $ref: '#/definitions/api_v1.MemoRelationType' + type: object + api_v1.UpsertSystemSettingRequest: properties: description: type: string name: - type: string + $ref: '#/definitions/api_v1.SystemSettingName' value: type: string type: object - store.User: + api_v1.UpsertTagRequest: properties: - avatarURL: + name: + type: string + type: object + api_v1.User: + properties: + avatarUrl: type: string createdTs: type: integer @@ -210,13 +397,11 @@ definitions: type: integer nickname: type: string - passwordHash: - type: string role: - $ref: '#/definitions/store.Role' + $ref: '#/definitions/api_v1.Role' rowStatus: allOf: - - $ref: '#/definitions/store.RowStatus' + - $ref: '#/definitions/api_v1.RowStatus' description: Standard fields updatedTs: type: integer @@ -224,7 +409,7 @@ definitions: description: Domain specific fields type: string type: object - store.Visibility: + api_v1.Visibility: enum: - PUBLIC - PROTECTED @@ -234,18 +419,18 @@ definitions: - Public - Protected - Private - v1.CreateIdentityProviderRequest: + github_com_usememos_memos_api_v1.CreateIdentityProviderRequest: properties: config: - $ref: '#/definitions/v1.IdentityProviderConfig' + $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig' identifierFilter: type: string name: type: string type: - $ref: '#/definitions/v1.IdentityProviderType' + $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType' type: object - v1.CreateMemoRequest: + github_com_usememos_memos_api_v1.CreateMemoRequest: properties: content: type: string @@ -253,7 +438,7 @@ definitions: type: integer relationList: items: - $ref: '#/definitions/v1.UpsertMemoRelationRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest' type: array resourceIdList: description: Related fields @@ -262,10 +447,10 @@ definitions: type: array visibility: allOf: - - $ref: '#/definitions/v1.Visibility' + - $ref: '#/definitions/github_com_usememos_memos_api_v1.Visibility' description: Domain specific fields type: object - v1.CreateResourceRequest: + github_com_usememos_memos_api_v1.CreateResourceRequest: properties: externalLink: type: string @@ -274,16 +459,16 @@ definitions: type: type: string type: object - v1.CreateStorageRequest: + github_com_usememos_memos_api_v1.CreateStorageRequest: properties: config: - $ref: '#/definitions/v1.StorageConfig' + $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageConfig' name: type: string type: - $ref: '#/definitions/v1.StorageType' + $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageType' type: object - v1.CreateUserRequest: + github_com_usememos_memos_api_v1.CreateUserRequest: properties: email: type: string @@ -292,11 +477,11 @@ definitions: password: type: string role: - $ref: '#/definitions/v1.Role' + $ref: '#/definitions/github_com_usememos_memos_api_v1.Role' username: type: string type: object - v1.CustomizedProfile: + github_com_usememos_memos_api_v1.CustomizedProfile: properties: appearance: description: Appearance is the server default appearance. @@ -304,9 +489,6 @@ definitions: description: description: Description is the server description. type: string - externalUrl: - description: ExternalURL is the external url of server. e.g. https://usermemos.com - type: string locale: description: Locale is the server default locale. type: string @@ -317,12 +499,12 @@ definitions: description: Name is the server name, default is `memos` type: string type: object - v1.DeleteTagRequest: + github_com_usememos_memos_api_v1.DeleteTagRequest: properties: name: type: string type: object - v1.FieldMapping: + github_com_usememos_memos_api_v1.FieldMapping: properties: displayName: type: string @@ -331,10 +513,10 @@ definitions: identifier: type: string type: object - v1.IdentityProvider: + github_com_usememos_memos_api_v1.IdentityProvider: properties: config: - $ref: '#/definitions/v1.IdentityProviderConfig' + $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig' id: type: integer identifierFilter: @@ -342,14 +524,14 @@ definitions: name: type: string type: - $ref: '#/definitions/v1.IdentityProviderType' + $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType' type: object - v1.IdentityProviderConfig: + github_com_usememos_memos_api_v1.IdentityProviderConfig: properties: oauth2Config: - $ref: '#/definitions/v1.IdentityProviderOAuth2Config' + $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config' type: object - v1.IdentityProviderOAuth2Config: + github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config: properties: authUrl: type: string @@ -358,7 +540,7 @@ definitions: clientSecret: type: string fieldMapping: - $ref: '#/definitions/v1.FieldMapping' + $ref: '#/definitions/github_com_usememos_memos_api_v1.FieldMapping' scopes: items: type: string @@ -368,21 +550,21 @@ definitions: userInfoUrl: type: string type: object - v1.IdentityProviderType: + github_com_usememos_memos_api_v1.IdentityProviderType: enum: - OAUTH2 type: string x-enum-varnames: - IdentityProviderOAuth2Type - v1.MemoRelationType: + github_com_usememos_memos_api_v1.MemoRelationType: enum: - REFERENCE - - ADDITIONAL + - COMMENT type: string x-enum-varnames: - MemoRelationReference - - MemoRelationAdditional - v1.PatchMemoRequest: + - MemoRelationComment + github_com_usememos_memos_api_v1.PatchMemoRequest: properties: content: description: Domain specific fields @@ -392,7 +574,7 @@ definitions: type: integer relationList: items: - $ref: '#/definitions/v1.UpsertMemoRelationRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest' type: array resourceIdList: description: Related fields @@ -400,13 +582,13 @@ definitions: type: integer type: array rowStatus: - $ref: '#/definitions/v1.RowStatus' + $ref: '#/definitions/github_com_usememos_memos_api_v1.RowStatus' updatedTs: type: integer visibility: - $ref: '#/definitions/v1.Visibility' + $ref: '#/definitions/github_com_usememos_memos_api_v1.Visibility' type: object - v1.Role: + github_com_usememos_memos_api_v1.Role: enum: - HOST - ADMIN @@ -416,7 +598,7 @@ definitions: - RoleHost - RoleAdmin - RoleUser - v1.RowStatus: + github_com_usememos_memos_api_v1.RowStatus: enum: - NORMAL - ARCHIVED @@ -424,7 +606,7 @@ definitions: x-enum-varnames: - Normal - Archived - v1.SSOSignIn: + github_com_usememos_memos_api_v1.SSOSignIn: properties: code: type: string @@ -433,26 +615,28 @@ definitions: redirectUri: type: string type: object - v1.SignIn: + github_com_usememos_memos_api_v1.SignIn: properties: password: type: string + remember: + type: boolean username: type: string type: object - v1.SignUp: + github_com_usememos_memos_api_v1.SignUp: properties: password: type: string username: type: string type: object - v1.StorageConfig: + github_com_usememos_memos_api_v1.StorageConfig: properties: s3Config: - $ref: '#/definitions/v1.StorageS3Config' + $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageS3Config' type: object - v1.StorageS3Config: + github_com_usememos_memos_api_v1.StorageS3Config: properties: accessKey: type: string @@ -462,6 +646,8 @@ definitions: type: string path: type: string + presign: + type: boolean region: type: string secretKey: @@ -471,55 +657,45 @@ definitions: urlSuffix: type: string type: object - v1.StorageType: + github_com_usememos_memos_api_v1.StorageType: enum: - S3 type: string x-enum-varnames: - StorageS3 - v1.SystemSetting: + github_com_usememos_memos_api_v1.SystemSetting: properties: description: type: string name: - $ref: '#/definitions/v1.SystemSettingName' + $ref: '#/definitions/github_com_usememos_memos_api_v1.SystemSettingName' value: description: Value is a JSON string with basic value. type: string type: object - v1.SystemSettingName: + github_com_usememos_memos_api_v1.SystemSettingName: enum: - server-id - secret-session - - allow-signup - - disable-password-login - disable-public-memos - max-upload-size-mib - - additional-style - - additional-script - customized-profile - storage-service-id - local-storage-path - telegram-bot-token - memo-display-with-updated-ts - - auto-backup-interval type: string x-enum-varnames: - SystemSettingServerIDName - SystemSettingSecretSessionName - - SystemSettingAllowSignUpName - - SystemSettingDisablePasswordLoginName - SystemSettingDisablePublicMemosName - SystemSettingMaxUploadSizeMiBName - - SystemSettingAdditionalStyleName - - SystemSettingAdditionalScriptName - SystemSettingCustomizedProfileName - SystemSettingStorageServiceIDName - SystemSettingLocalStoragePathName - SystemSettingTelegramBotTokenName - SystemSettingMemoDisplayWithUpdatedTsName - - SystemSettingAutoBackupIntervalName - v1.SystemStatus: + github_com_usememos_memos_api_v1.SystemStatus: properties: additionalScript: description: Additional script. @@ -532,12 +708,9 @@ definitions: System settings Allow sign up. type: boolean - autoBackupInterval: - description: Auto Backup Interval. - type: integer customizedProfile: allOf: - - $ref: '#/definitions/v1.CustomizedProfile' + - $ref: '#/definitions/github_com_usememos_memos_api_v1.CustomizedProfile' description: Customized server profile, including server name and external url. dbSize: @@ -549,7 +722,7 @@ definitions: description: Disable public memos. type: boolean host: - $ref: '#/definitions/v1.User' + $ref: '#/definitions/github_com_usememos_memos_api_v1.User' localStoragePath: description: Local storage path. type: string @@ -565,32 +738,32 @@ definitions: description: Storage service ID. type: integer type: object - v1.UpdateIdentityProviderRequest: + github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest: properties: config: - $ref: '#/definitions/v1.IdentityProviderConfig' + $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig' identifierFilter: type: string name: type: string type: - $ref: '#/definitions/v1.IdentityProviderType' + $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType' type: object - v1.UpdateResourceRequest: + github_com_usememos_memos_api_v1.UpdateResourceRequest: properties: filename: type: string type: object - v1.UpdateStorageRequest: + github_com_usememos_memos_api_v1.UpdateStorageRequest: properties: config: - $ref: '#/definitions/v1.StorageConfig' + $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageConfig' name: type: string type: - $ref: '#/definitions/v1.StorageType' + $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageType' type: object - v1.UpdateUserRequest: + github_com_usememos_memos_api_v1.UpdateUserRequest: properties: avatarUrl: type: string @@ -601,44 +774,37 @@ definitions: password: type: string rowStatus: - $ref: '#/definitions/v1.RowStatus' + $ref: '#/definitions/github_com_usememos_memos_api_v1.RowStatus' username: type: string type: object - v1.UpsertMemoOrganizerRequest: + github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest: properties: pinned: type: boolean type: object - v1.UpsertMemoRelationRequest: + github_com_usememos_memos_api_v1.UpsertMemoRelationRequest: properties: relatedMemoId: type: integer type: - $ref: '#/definitions/v1.MemoRelationType' + $ref: '#/definitions/github_com_usememos_memos_api_v1.MemoRelationType' type: object - v1.UpsertSystemSettingRequest: + github_com_usememos_memos_api_v1.UpsertSystemSettingRequest: properties: description: type: string name: - $ref: '#/definitions/v1.SystemSettingName' + $ref: '#/definitions/github_com_usememos_memos_api_v1.SystemSettingName' value: type: string type: object - v1.UpsertTagRequest: + github_com_usememos_memos_api_v1.UpsertTagRequest: properties: name: type: string type: object - v1.UpsertUserSettingRequest: - properties: - key: - $ref: '#/definitions/v1.UserSettingKey' - value: - type: string - type: object - v1.User: + github_com_usememos_memos_api_v1.User: properties: avatarUrl: type: string @@ -651,43 +817,219 @@ definitions: nickname: type: string role: - $ref: '#/definitions/v1.Role' + $ref: '#/definitions/github_com_usememos_memos_api_v1.Role' rowStatus: allOf: - - $ref: '#/definitions/v1.RowStatus' + - $ref: '#/definitions/github_com_usememos_memos_api_v1.RowStatus' description: Standard fields updatedTs: type: integer - userSettingList: + username: + description: Domain specific fields + type: string + type: object + github_com_usememos_memos_api_v1.Visibility: + enum: + - PUBLIC + - PROTECTED + - PRIVATE + type: string + x-enum-varnames: + - Public + - Protected + - Private + profile.Profile: + properties: + mode: + description: Mode can be "prod" or "dev" or "demo" + type: string + version: + description: Version is the current version of server + type: string + type: object + store.FieldMapping: + properties: + displayName: + type: string + email: + type: string + identifier: + type: string + type: object + store.IdentityProvider: + properties: + config: + $ref: '#/definitions/store.IdentityProviderConfig' + id: + type: integer + identifierFilter: + type: string + name: + type: string + type: + $ref: '#/definitions/store.IdentityProviderType' + type: object + store.IdentityProviderConfig: + properties: + oauth2Config: + $ref: '#/definitions/store.IdentityProviderOAuth2Config' + type: object + store.IdentityProviderOAuth2Config: + properties: + authUrl: + type: string + clientId: + type: string + clientSecret: + type: string + fieldMapping: + $ref: '#/definitions/store.FieldMapping' + scopes: items: - $ref: '#/definitions/v1.UserSetting' + type: string type: array - username: + tokenUrl: + type: string + userInfoUrl: + type: string + type: object + store.IdentityProviderType: + enum: + - OAUTH2 + type: string + x-enum-varnames: + - IdentityProviderOAuth2Type + store.Memo: + properties: + content: description: Domain specific fields type: string + createdTs: + type: integer + creatorID: + type: integer + id: + type: integer + parentID: + type: integer + pinned: + description: Composed fields + type: boolean + resourceName: + type: string + rowStatus: + allOf: + - $ref: '#/definitions/store.RowStatus' + description: Standard fields + updatedTs: + type: integer + visibility: + $ref: '#/definitions/store.Visibility' type: object - v1.UserSetting: + store.MemoRelation: properties: - key: - $ref: '#/definitions/v1.UserSettingKey' - userId: + memoID: type: integer - value: + relatedMemoID: + type: integer + type: + $ref: '#/definitions/store.MemoRelationType' + type: object + store.MemoRelationType: + enum: + - REFERENCE + - COMMENT + type: string + x-enum-varnames: + - MemoRelationReference + - MemoRelationComment + store.Resource: + properties: + blob: + items: + type: integer + type: array + createdTs: + type: integer + creatorID: + description: Standard fields + type: integer + externalLink: type: string + filename: + description: Domain specific fields + type: string + id: + type: integer + internalPath: + type: string + memoID: + type: integer + resourceName: + type: string + size: + type: integer + type: + type: string + updatedTs: + type: integer type: object - v1.UserSettingKey: + store.Role: + enum: + - HOST + - ADMIN + - USER + type: string + x-enum-varnames: + - RoleHost + - RoleAdmin + - RoleUser + store.RowStatus: enum: - - locale - - appearance - - memo-visibility - - telegram-user-id + - NORMAL + - ARCHIVED type: string x-enum-varnames: - - UserSettingLocaleKey - - UserSettingAppearanceKey - - UserSettingMemoVisibilityKey - - UserSettingTelegramUserIDKey - v1.Visibility: + - Normal + - Archived + store.Storage: + properties: + config: + type: string + id: + type: integer + name: + type: string + type: + type: string + type: object + store.User: + properties: + avatarURL: + type: string + createdTs: + type: integer + email: + type: string + id: + type: integer + nickname: + type: string + passwordHash: + type: string + role: + $ref: '#/definitions/store.Role' + rowStatus: + allOf: + - $ref: '#/definitions/store.RowStatus' + description: Standard fields + updatedTs: + type: integer + username: + description: Domain specific fields + type: string + type: object + store.Visibility: enum: - PUBLIC - PROTECTED @@ -721,7 +1063,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.SignIn' + $ref: '#/definitions/github_com_usememos_memos_api_v1.SignIn' produces: - application/json responses: @@ -753,7 +1095,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.SSOSignIn' + $ref: '#/definitions/github_com_usememos_memos_api_v1.SSOSignIn' produces: - application/json responses: @@ -801,7 +1143,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.SignUp' + $ref: '#/definitions/github_com_usememos_memos_api_v1.SignUp' produces: - application/json responses: @@ -834,7 +1176,7 @@ paths: description: List of available identity providers schema: items: - $ref: '#/definitions/v1.IdentityProvider' + $ref: '#/definitions/api_v1.IdentityProvider' type: array "500": description: Failed to find identity provider list | Failed to find user @@ -850,7 +1192,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.CreateIdentityProviderRequest' + $ref: '#/definitions/api_v1.CreateIdentityProviderRequest' produces: - application/json responses: @@ -935,7 +1277,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.UpdateIdentityProviderRequest' + $ref: '#/definitions/api_v1.UpdateIdentityProviderRequest' produces: - application/json responses: @@ -1020,7 +1362,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.CreateMemoRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.CreateMemoRequest' produces: - application/json responses: @@ -1117,7 +1459,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.PatchMemoRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.PatchMemoRequest' produces: - application/json responses: @@ -1154,7 +1496,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.UpsertMemoOrganizerRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest' produces: - application/json responses: @@ -1216,7 +1558,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.UpsertMemoRelationRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest' produces: - application/json responses: @@ -1250,7 +1592,7 @@ paths: - description: Type of relation to remove enum: - REFERENCE - - ADDITIONAL + - COMMENT in: path name: relationType required: true @@ -1376,7 +1718,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.CreateResourceRequest' + $ref: '#/definitions/api_v1.CreateResourceRequest' produces: - application/json responses: @@ -1434,7 +1776,7 @@ paths: name: patch required: true schema: - $ref: '#/definitions/v1.UpdateResourceRequest' + $ref: '#/definitions/api_v1.UpdateResourceRequest' produces: - application/json responses: @@ -1489,7 +1831,7 @@ paths: "200": description: System GetSystemStatus schema: - $ref: '#/definitions/v1.SystemStatus' + $ref: '#/definitions/api_v1.SystemStatus' "401": description: Missing user in session | Unauthorized "500": @@ -1525,7 +1867,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.CreateStorageRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.CreateStorageRequest' produces: - application/json responses: @@ -1580,7 +1922,7 @@ paths: name: patch required: true schema: - $ref: '#/definitions/v1.UpdateStorageRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.UpdateStorageRequest' produces: - application/json responses: @@ -1608,7 +1950,7 @@ paths: description: System setting list schema: items: - $ref: '#/definitions/v1.SystemSetting' + $ref: '#/definitions/api_v1.SystemSetting' type: array "401": description: Missing user in session | Unauthorized @@ -1626,14 +1968,10 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.UpsertSystemSettingRequest' + $ref: '#/definitions/api_v1.UpsertSystemSettingRequest' produces: - application/json responses: - "200": - description: Created system setting - schema: - $ref: '#/definitions/store.SystemSetting' "400": description: Malformatted post system setting request | invalid system setting "401": @@ -1688,7 +2026,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.UpsertTagRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertTagRequest' produces: - application/json responses: @@ -1715,7 +2053,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.DeleteTagRequest' + $ref: '#/definitions/github_com_usememos_memos_api_v1.DeleteTagRequest' produces: - application/json responses: @@ -1775,7 +2113,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/v1.CreateUserRequest' + $ref: '#/definitions/api_v1.CreateUserRequest' produces: - application/json responses: @@ -1857,7 +2195,7 @@ paths: name: patch required: true schema: - $ref: '#/definitions/v1.UpdateUserRequest' + $ref: '#/definitions/api_v1.UpdateUserRequest' produces: - application/json responses: @@ -1916,47 +2254,6 @@ paths: summary: Get user by username tags: - user - /api/v1/user/setting: - post: - consumes: - - application/json - parameters: - - description: Request object. - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.UpsertUserSettingRequest' - produces: - - application/json - responses: - "200": - description: Created user setting - schema: - $ref: '#/definitions/github_com_usememos_memos_store.UserSetting' - "400": - description: Malformatted post user setting upsert request | Invalid user - setting format - "401": - description: Missing auth session - "500": - description: Failed to upsert user setting - summary: Upsert user setting - tags: - - user-setting - /explore/rss.xml: - get: - produces: - - text/xml - responses: - "200": - description: RSS - "500": - description: Failed to get system customized profile | Failed to find memo - list | Failed to generate rss - summary: Get RSS - tags: - - rss /o/get/GetImage: get: parameters: @@ -1977,57 +2274,5 @@ paths: description: Failed to write GetImage blob summary: Get GetImage from URL tags: - - get - /o/r/{resourceId}: - get: - description: '*Swagger UI may have problems displaying other file types than - images' - parameters: - - description: Resource ID - in: path - name: resourceId - required: true - type: integer - - description: Thumbnail - in: query - name: thumbnail - type: integer - produces: - - application/octet-stream - responses: - "200": - description: Requested resource - "400": - description: 'ID is not a number: %s | Failed to get resource visibility' - "401": - description: Resource visibility not match - "404": - description: 'Resource not found: %d' - "500": - description: 'Failed to find resource by ID: %v | Failed to open the local - resource: %s | Failed to read the local resource: %s' - summary: Stream a resource - tags: - - resource - /u/{id}/rss.xml: - get: - parameters: - - description: User ID - in: path - name: id - required: true - type: integer - produces: - - text/xml - responses: - "200": - description: RSS - "400": - description: User id is not a number - "500": - description: Failed to get system customized profile | Failed to find memo - list | Failed to generate rss - summary: Get RSS for a user - tags: - - rss + - image-url swagger: "2.0" diff --git a/api/v1/system.go b/server/route/api/v1/system.go similarity index 80% rename from api/v1/system.go rename to server/route/api/v1/system.go index 109e7f3196a81..51a26c11cb628 100644 --- a/api/v1/system.go +++ b/server/route/api/v1/system.go @@ -5,9 +5,7 @@ import ( "net/http" "github.com/labstack/echo/v4" - "go.uber.org/zap" - "github.com/usememos/memos/internal/log" "github.com/usememos/memos/server/profile" "github.com/usememos/memos/store" ) @@ -18,20 +16,12 @@ type SystemStatus struct { DBSize int64 `json:"dbSize"` // System settings - // Allow sign up. - AllowSignUp bool `json:"allowSignUp"` // Disable password login. DisablePasswordLogin bool `json:"disablePasswordLogin"` // Disable public memos. DisablePublicMemos bool `json:"disablePublicMemos"` // Max upload size. MaxUploadSizeMiB int `json:"maxUploadSizeMiB"` - // Auto Backup Interval. - AutoBackupInterval int `json:"autoBackupInterval"` - // Additional style. - AdditionalStyle string `json:"additionalStyle"` - // Additional script. - AdditionalScript string `json:"additionalScript"` // Customized server profile, including server name and external url. CustomizedProfile CustomizedProfile `json:"customizedProfile"` // Storage service ID. @@ -76,11 +66,9 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error { Mode: s.Profile.Mode, Version: s.Profile.Version, }, - // Allow sign up by default. - AllowSignUp: true, MaxUploadSizeMiB: 32, CustomizedProfile: CustomizedProfile{ - Name: "memos", + Name: "Memos", Locale: "en", Appearance: "system", }, @@ -99,7 +87,13 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error { systemStatus.Host = &User{ID: hostUser.ID} } - systemSettingList, err := s.Store.ListSystemSettings(ctx, &store.FindSystemSetting{}) + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace general setting").SetInternal(err) + } + systemStatus.DisablePasswordLogin = workspaceGeneralSetting.DisallowPasswordLogin + + systemSettingList, err := s.Store.ListWorkspaceSettings(ctx, &store.FindWorkspaceSetting{}) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err) } @@ -111,25 +105,15 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error { var baseValue any err := json.Unmarshal([]byte(systemSetting.Value), &baseValue) if err != nil { - log.Warn("Failed to unmarshal system setting value", zap.String("setting name", systemSetting.Name)) + // Skip invalid value. continue } switch systemSetting.Name { - case SystemSettingAllowSignUpName.String(): - systemStatus.AllowSignUp = baseValue.(bool) - case SystemSettingDisablePasswordLoginName.String(): - systemStatus.DisablePasswordLogin = baseValue.(bool) case SystemSettingDisablePublicMemosName.String(): systemStatus.DisablePublicMemos = baseValue.(bool) case SystemSettingMaxUploadSizeMiBName.String(): systemStatus.MaxUploadSizeMiB = int(baseValue.(float64)) - case SystemSettingAutoBackupIntervalName.String(): - systemStatus.AutoBackupInterval = int(baseValue.(float64)) - case SystemSettingAdditionalStyleName.String(): - systemStatus.AdditionalStyle = baseValue.(string) - case SystemSettingAdditionalScriptName.String(): - systemStatus.AdditionalScript = baseValue.(string) case SystemSettingCustomizedProfileName.String(): customizedProfile := CustomizedProfile{} if err := json.Unmarshal([]byte(systemSetting.Value), &customizedProfile); err != nil { @@ -143,7 +127,7 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error { case SystemSettingMemoDisplayWithUpdatedTsName.String(): systemStatus.MemoDisplayWithUpdatedTs = baseValue.(bool) default: - log.Warn("Unknown system setting name", zap.String("setting name", systemSetting.Name)) + // Skip unknown system setting. } } diff --git a/api/v1/system_setting.go b/server/route/api/v1/system_setting.go similarity index 73% rename from api/v1/system_setting.go rename to server/route/api/v1/system_setting.go index 708e812b4830a..4aff3816ed248 100644 --- a/api/v1/system_setting.go +++ b/server/route/api/v1/system_setting.go @@ -3,6 +3,7 @@ package v1 import ( "encoding/json" "net/http" + "path/filepath" "strings" "github.com/labstack/echo/v4" @@ -18,18 +19,10 @@ const ( SystemSettingServerIDName SystemSettingName = "server-id" // SystemSettingSecretSessionName is the name of secret session. SystemSettingSecretSessionName SystemSettingName = "secret-session" - // SystemSettingAllowSignUpName is the name of allow signup setting. - SystemSettingAllowSignUpName SystemSettingName = "allow-signup" - // SystemSettingDisablePasswordLoginName is the name of disable password login setting. - SystemSettingDisablePasswordLoginName SystemSettingName = "disable-password-login" // SystemSettingDisablePublicMemosName is the name of disable public memos setting. SystemSettingDisablePublicMemosName SystemSettingName = "disable-public-memos" // SystemSettingMaxUploadSizeMiBName is the name of max upload size setting. SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib" - // SystemSettingAdditionalStyleName is the name of additional style. - SystemSettingAdditionalStyleName SystemSettingName = "additional-style" - // SystemSettingAdditionalScriptName is the name of additional script. - SystemSettingAdditionalScriptName SystemSettingName = "additional-script" // SystemSettingCustomizedProfileName is the name of customized server profile. SystemSettingCustomizedProfileName SystemSettingName = "customized-profile" // SystemSettingStorageServiceIDName is the name of storage service ID. @@ -40,8 +33,6 @@ const ( SystemSettingTelegramBotTokenName SystemSettingName = "telegram-bot-token" // SystemSettingMemoDisplayWithUpdatedTsName is the name of memo display with updated ts. SystemSettingMemoDisplayWithUpdatedTsName SystemSettingName = "memo-display-with-updated-ts" - // SystemSettingAutoBackupIntervalName is the name of auto backup interval as seconds. - SystemSettingAutoBackupIntervalName SystemSettingName = "auto-backup-interval" ) const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"` @@ -57,8 +48,6 @@ type CustomizedProfile struct { Locale string `json:"locale"` // Appearance is the server default appearance. Appearance string `json:"appearance"` - // ExternalURL is the external url of server. e.g. https://usermemos.com - ExternalURL string `json:"externalUrl"` } func (key SystemSettingName) String() string { @@ -109,7 +98,7 @@ func (s *APIV1Service) GetSystemSettingList(c echo.Context) error { return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized") } - list, err := s.Store.ListSystemSettings(ctx, &store.FindSystemSetting{}) + list, err := s.Store.ListWorkspaceSettings(ctx, &store.FindWorkspaceSetting{}) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err) } @@ -128,7 +117,6 @@ func (s *APIV1Service) GetSystemSettingList(c echo.Context) error { // @Accept json // @Produce json // @Param body body UpsertSystemSettingRequest true "Request object." -// @Success 200 {object} store.SystemSetting "Created system setting" // @Failure 400 {object} nil "Malformatted post system setting request | invalid system setting" // @Failure 401 {object} nil "Missing user in session | Unauthorized" // @Failure 403 {object} nil "Cannot disable passwords if no SSO identity provider is configured." @@ -158,22 +146,8 @@ func (s *APIV1Service) CreateSystemSetting(c echo.Context) error { if err := systemSettingUpsert.Validate(); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err) } - if systemSettingUpsert.Name == SystemSettingDisablePasswordLoginName { - var disablePasswordLogin bool - if err := json.Unmarshal([]byte(systemSettingUpsert.Value), &disablePasswordLogin); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err) - } - - identityProviderList, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProvider{}) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err) - } - if disablePasswordLogin && len(identityProviderList) == 0 { - return echo.NewHTTPError(http.StatusForbidden, "Cannot disable passwords if no SSO identity provider is configured.") - } - } - systemSetting, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{ + systemSetting, err := s.Store.UpsertWorkspaceSetting(ctx, &store.WorkspaceSetting{ Name: systemSettingUpsert.Name.String(), Value: systemSettingUpsert.Value, Description: systemSettingUpsert.Description, @@ -188,16 +162,6 @@ func (upsert UpsertSystemSettingRequest) Validate() error { switch settingName := upsert.Name; settingName { case SystemSettingServerIDName: return errors.Errorf("updating %v is not allowed", settingName) - case SystemSettingAllowSignUpName: - var value bool - if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { - return errors.Errorf(systemSettingUnmarshalError, settingName) - } - case SystemSettingDisablePasswordLoginName: - var value bool - if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { - return errors.Errorf(systemSettingUnmarshalError, settingName) - } case SystemSettingDisablePublicMemosName: var value bool if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { @@ -208,24 +172,13 @@ func (upsert UpsertSystemSettingRequest) Validate() error { if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { return errors.Errorf(systemSettingUnmarshalError, settingName) } - case SystemSettingAdditionalStyleName: - var value string - if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { - return errors.Errorf(systemSettingUnmarshalError, settingName) - } - case SystemSettingAdditionalScriptName: - var value string - if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { - return errors.Errorf(systemSettingUnmarshalError, settingName) - } case SystemSettingCustomizedProfileName: customizedProfile := CustomizedProfile{ - Name: "memos", + Name: "Memos", LogoURL: "", Description: "", Locale: "en", Appearance: "system", - ExternalURL: "", } if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil { return errors.Errorf(systemSettingUnmarshalError, settingName) @@ -242,13 +195,23 @@ func (upsert UpsertSystemSettingRequest) Validate() error { if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { return errors.Errorf(systemSettingUnmarshalError, settingName) } - case SystemSettingAutoBackupIntervalName: - var value int - if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { - return errors.Errorf(systemSettingUnmarshalError, settingName) - } - if value < 0 { - return errors.New("must be positive") + + trimmedValue := strings.TrimSpace(value) + switch { + case trimmedValue != value: + return errors.New("local storage path must not contain leading or trailing whitespace") + case trimmedValue == "": + return errors.New("local storage path can't be empty") + case strings.Contains(trimmedValue, "\\"): + return errors.New("local storage path must use forward slashes `/`") + case strings.Contains(trimmedValue, "../"): + return errors.New("local storage path is not allowed to contain `../`") + case strings.HasPrefix(trimmedValue, "./"): + return errors.New("local storage path is not allowed to start with `./`") + case filepath.IsAbs(trimmedValue) || trimmedValue[0] == '/': + return errors.New("local storage path must be a relative path") + case !strings.Contains(trimmedValue, "{filename}"): + return errors.New("local storage path must contain `{filename}`") } case SystemSettingTelegramBotTokenName: if upsert.Value == "" { @@ -277,7 +240,7 @@ func (upsert UpsertSystemSettingRequest) Validate() error { return nil } -func convertSystemSettingFromStore(systemSetting *store.SystemSetting) *SystemSetting { +func convertSystemSettingFromStore(systemSetting *store.WorkspaceSetting) *SystemSetting { return &SystemSetting{ Name: SystemSettingName(systemSetting.Name), Value: systemSetting.Value, diff --git a/api/v1/tag.go b/server/route/api/v1/tag.go similarity index 100% rename from api/v1/tag.go rename to server/route/api/v1/tag.go diff --git a/api/v1/tag_test.go b/server/route/api/v1/tag_test.go similarity index 100% rename from api/v1/tag_test.go rename to server/route/api/v1/tag_test.go diff --git a/api/v1/user.go b/server/route/api/v1/user.go similarity index 89% rename from api/v1/user.go rename to server/route/api/v1/user.go index dbb69739c7ca0..1f56fc2b63b5a 100644 --- a/api/v1/user.go +++ b/server/route/api/v1/user.go @@ -12,7 +12,6 @@ import ( "golang.org/x/crypto/bcrypt" "github.com/usememos/memos/internal/util" - "github.com/usememos/memos/server/service/metric" "github.com/usememos/memos/store" ) @@ -41,13 +40,12 @@ type User struct { UpdatedTs int64 `json:"updatedTs"` // Domain specific fields - Username string `json:"username"` - Role Role `json:"role"` - Email string `json:"email"` - Nickname string `json:"nickname"` - PasswordHash string `json:"-"` - AvatarURL string `json:"avatarUrl"` - UserSettingList []*UserSetting `json:"userSettingList"` + Username string `json:"username"` + Role Role `json:"role"` + Email string `json:"email"` + Nickname string `json:"nickname"` + PasswordHash string `json:"-"` + AvatarURL string `json:"avatarUrl"` } type CreateUserRequest struct { @@ -88,6 +86,23 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) { // @Router /api/v1/user [GET] func (s *APIV1Service) GetUserList(c echo.Context) error { ctx := c.Request().Context() + userID, ok := c.Get(userIDContextKey).(int32) + if !ok { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session") + } + currentUser, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &userID, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err) + } + if currentUser == nil { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session") + } + if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { + return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to list users") + } + list, err := s.Store.ListUsers(ctx, &store.FindUser{}) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err) @@ -142,7 +157,7 @@ func (s *APIV1Service) CreateUser(c echo.Context) error { if err := userCreate.Validate(); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err) } - if !usernameMatcher.MatchString(strings.ToLower(userCreate.Username)) { + if !util.ResourceNameMatcher.MatchString(strings.ToLower(userCreate.Username)) { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", userCreate.Username)).SetInternal(err) } // Disallow host user to be created. @@ -167,7 +182,6 @@ func (s *APIV1Service) CreateUser(c echo.Context) error { } userMessage := convertUserFromStore(user) - metric.Enqueue("user create") return c.JSON(http.StatusOK, userMessage) } @@ -195,18 +209,7 @@ func (s *APIV1Service) GetCurrentUser(c echo.Context) error { return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session") } - list, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{ - UserID: &userID, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err) - } - userSettingList := []*UserSetting{} - for _, userSetting := range list { - userSettingList = append(userSettingList, convertUserSettingFromStore(userSetting)) - } userMessage := convertUserFromStore(user) - userMessage.UserSettingList = userSettingList return c.JSON(http.StatusOK, userMessage) } @@ -264,8 +267,12 @@ func (s *APIV1Service) GetUserByID(c echo.Context) error { } userMessage := convertUserFromStore(user) - // data desensitize - userMessage.Email = "" + userID, ok := c.Get(userIDContextKey).(int32) + if !ok || userID != user.ID { + // Data desensitize. + userMessage.Email = "" + } + return c.JSON(http.StatusOK, userMessage) } @@ -303,11 +310,21 @@ func (s *APIV1Service) DeleteUser(c echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err) } + if currentUserID == userID { + return echo.NewHTTPError(http.StatusBadRequest, "Cannot delete current user") + } - userDelete := &store.DeleteUser{ - ID: userID, + findUser, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID}) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err) + } + if s.Profile.Mode == "demo" && findUser.Username == "memos-demo" { + return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to delete this user in demo mode") } - if err := s.Store.DeleteUser(ctx, userDelete); err != nil { + + if err := s.Store.DeleteUser(ctx, &store.DeleteUser{ + ID: userID, + }); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err) } return c.JSON(http.StatusOK, true) @@ -355,6 +372,10 @@ func (s *APIV1Service) UpdateUser(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid update user request").SetInternal(err) } + if s.Profile.Mode == "demo" && *request.Username == "memos-demo" { + return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to update user in demo mode") + } + currentTs := time.Now().Unix() userUpdate := &store.UpdateUser{ ID: userID, @@ -363,9 +384,12 @@ func (s *APIV1Service) UpdateUser(c echo.Context) error { if request.RowStatus != nil { rowStatus := store.RowStatus(request.RowStatus.String()) userUpdate.RowStatus = &rowStatus + if rowStatus == store.Archived && currentUserID == userID { + return echo.NewHTTPError(http.StatusBadRequest, "Cannot archive current user") + } } if request.Username != nil { - if !usernameMatcher.MatchString(strings.ToLower(*request.Username)) { + if !util.ResourceNameMatcher.MatchString(strings.ToLower(*request.Username)) { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", *request.Username)).SetInternal(err) } userUpdate.Username = request.Username @@ -394,18 +418,7 @@ func (s *APIV1Service) UpdateUser(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err) } - list, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{ - UserID: &userID, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err) - } - userSettingList := []*UserSetting{} - for _, userSetting := range list { - userSettingList = append(userSettingList, convertUserSettingFromStore(userSetting)) - } userMessage := convertUserFromStore(user) - userMessage.UserSettingList = userSettingList return c.JSON(http.StatusOK, userMessage) } diff --git a/api/v1/v1.go b/server/route/api/v1/v1.go similarity index 90% rename from api/v1/v1.go rename to server/route/api/v1/v1.go index 78d6b1ac66acc..c449672712688 100644 --- a/api/v1/v1.go +++ b/server/route/api/v1/v1.go @@ -7,9 +7,10 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" - "github.com/usememos/memos/api/resource" "github.com/usememos/memos/plugin/telegram" "github.com/usememos/memos/server/profile" + "github.com/usememos/memos/server/route/resource" + "github.com/usememos/memos/server/route/rss" "github.com/usememos/memos/store" ) @@ -44,9 +45,6 @@ func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store } func (s *APIV1Service) Register(rootGroup *echo.Group) { - // Register RSS routes. - s.registerRSSRoutes(rootGroup) - // Register API v1 routes. apiV1Group := rootGroup.Group("/api/v1") apiV1Group.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ @@ -72,7 +70,6 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) { s.registerAuthRoutes(apiV1Group) s.registerIdentityProviderRoutes(apiV1Group) s.registerUserRoutes(apiV1Group) - s.registerUserSettingRoutes(apiV1Group) s.registerTagRoutes(apiV1Group) s.registerStorageRoutes(apiV1Group) s.registerResourceRoutes(apiV1Group) @@ -86,9 +83,12 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) { return JWTMiddleware(s, next, s.Secret) }) s.registerGetterPublicRoutes(publicGroup) + // Create and register resource public routes. - resourceService := resource.NewService(s.Profile, s.Store) - resourceService.RegisterResourcePublicRoutes(publicGroup) + resource.NewResourceService(s.Profile, s.Store).RegisterRoutes(publicGroup) + + // Create and register rss public routes. + rss.NewRSSService(s.Profile, s.Store).RegisterRoutes(rootGroup) // programmatically set API version same as the server version SwaggerInfo.Version = s.Profile.Version diff --git a/api/v2/acl.go b/server/route/api/v2/acl.go similarity index 96% rename from api/v2/acl.go rename to server/route/api/v2/acl.go index cb3f140529ee7..23b0a59761ec2 100644 --- a/api/v2/acl.go +++ b/server/route/api/v2/acl.go @@ -5,16 +5,16 @@ import ( "net/http" "strings" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - "github.com/usememos/memos/api/auth" "github.com/usememos/memos/internal/util" storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/server/route/api/auth" "github.com/usememos/memos/store" ) @@ -68,6 +68,9 @@ func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, re if user == nil { return nil, errors.Errorf("user %q not exists", username) } + if user.RowStatus == store.Archived { + return nil, errors.Errorf("user %q is archived", username) + } if isOnlyForAdminAllowedMethod(serverInfo.FullMethod) && user.Role != store.RoleHost && user.Role != store.RoleAdmin { return nil, errors.Errorf("user %q is not admin", username) } diff --git a/server/route/api/v2/acl_config.go b/server/route/api/v2/acl_config.go new file mode 100644 index 0000000000000..0ed4fbaa41df5 --- /dev/null +++ b/server/route/api/v2/acl_config.go @@ -0,0 +1,37 @@ +package v2 + +import "strings" + +var authenticationAllowlistMethods = map[string]bool{ + "/memos.api.v2.WorkspaceService/GetWorkspaceProfile": true, + "/memos.api.v2.WorkspaceSettingService/GetWorkspaceSetting": true, + "/memos.api.v2.AuthService/GetAuthStatus": true, + "/memos.api.v2.AuthService/SignIn": true, + "/memos.api.v2.AuthService/SignInWithSSO": true, + "/memos.api.v2.AuthService/SignOut": true, + "/memos.api.v2.AuthService/SignUp": true, + "/memos.api.v2.UserService/GetUser": true, + "/memos.api.v2.MemoService/ListMemos": true, + "/memos.api.v2.MemoService/GetMemo": true, + "/memos.api.v2.MemoService/GetMemoByName": true, + "/memos.api.v2.MemoService/ListMemoResources": true, + "/memos.api.v2.MemoService/ListMemoRelations": true, + "/memos.api.v2.MemoService/ListMemoComments": true, +} + +// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication. +func isUnauthorizeAllowedMethod(fullMethodName string) bool { + if strings.HasPrefix(fullMethodName, "/grpc.reflection") { + return true + } + return authenticationAllowlistMethods[fullMethodName] +} + +var allowedMethodsOnlyForAdmin = map[string]bool{ + "/memos.api.v2.UserService/CreateUser": true, +} + +// isOnlyForAdminAllowedMethod returns true if the method is allowed to be called only by admin. +func isOnlyForAdminAllowedMethod(methodName string) bool { + return allowedMethodsOnlyForAdmin[methodName] +} diff --git a/api/v2/activity_service .go b/server/route/api/v2/activity_service.go similarity index 91% rename from api/v2/activity_service .go rename to server/route/api/v2/activity_service.go index 62c499016b43f..468747ed07b75 100644 --- a/api/v2/activity_service .go +++ b/server/route/api/v2/activity_service.go @@ -49,5 +49,10 @@ func convertActivityPayloadFromStore(payload *storepb.ActivityPayload) *apiv2pb. RelatedMemoId: payload.MemoComment.RelatedMemoId, } } + if payload.VersionUpdate != nil { + v2Payload.VersionUpdate = &apiv2pb.ActivityVersionUpdatePayload{ + Version: payload.VersionUpdate.Version, + } + } return v2Payload } diff --git a/server/route/api/v2/apidocs.swagger.md b/server/route/api/v2/apidocs.swagger.md new file mode 100644 index 0000000000000..ee03dcf99c332 --- /dev/null +++ b/server/route/api/v2/apidocs.swagger.md @@ -0,0 +1,1731 @@ +# api/v2/activity_service.proto +## Version: version not set + +--- +## AuthService + +### /api/v2/auth/signin + +#### POST +##### Summary + +SignIn signs in the user with the given username and password. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| username | query | | No | string | +| password | query | | No | string | +| neverExpire | query | | No | boolean | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2SignInResponse](#v2signinresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/auth/signin/sso + +#### POST +##### Summary + +SignInWithSSO signs in the user with the given SSO code. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| idpId | query | | No | integer | +| code | query | | No | string | +| redirectUri | query | | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2SignInWithSSOResponse](#v2signinwithssoresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/auth/signout + +#### POST +##### Summary + +SignOut signs out the user. + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2SignOutResponse](#v2signoutresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/auth/signup + +#### POST +##### Summary + +SignUp signs up the user with the given username and password. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| username | query | | No | string | +| password | query | | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2SignUpResponse](#v2signupresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/auth/status + +#### POST +##### Summary + +GetAuthStatus returns the current auth status of the user. + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetAuthStatusResponse](#v2getauthstatusresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +## InboxService + +### /api/v2/inboxes + +#### GET +##### Summary + +ListInboxes lists inboxes for a user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| user | query | Format: users/{username} | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListInboxesResponse](#v2listinboxesresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/{inbox.name} + +#### PATCH +##### Summary + +UpdateInbox updates an inbox. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| inbox.name | path | The name of the inbox. Format: inboxes/{uid} | Yes | string | +| inbox | body | | Yes | { **"sender"**: string, **"receiver"**: string, **"status"**: [v2InboxStatus](#v2inboxstatus), **"createTime"**: dateTime, **"type"**: [v2InboxType](#v2inboxtype), **"activityId"**: integer } | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2UpdateInboxResponse](#v2updateinboxresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/{name_1} + +#### DELETE +##### Summary + +DeleteInbox deletes an inbox. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name_1 | path | The name of the inbox to delete. Format: inboxes/{uid} | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2DeleteInboxResponse](#v2deleteinboxresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +## MemoService + +### /api/v2/memos + +#### GET +##### Summary + +ListMemos lists memos with pagination and filter. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| pageSize | query | The maximum number of memos to return. | No | integer | +| pageToken | query | A page token, received from a previous `ListMemos` call. Provide this to retrieve the subsequent page. | No | string | +| filter | query | Filter is used to filter memos returned in the list. Format: "creator == users/{username} && visibilities == ['PUBLIC', 'PROTECTED']" | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListMemosResponse](#v2listmemosresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +CreateMemo creates a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | | Yes | [v2CreateMemoRequest](#v2creatememorequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2CreateMemoResponse](#v2creatememoresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos/name/{name} + +#### GET +##### Summary + +GetMemoByName gets a memo by name. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | path | | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetMemoByNameResponse](#v2getmemobynameresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos/stats + +#### GET +##### Summary + +GetUserMemosStats gets stats of memos for a user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | query | name is the name of the user to get stats for. Format: users/{username} | No | string | +| timezone | query | timezone location Format: uses tz identifier https://en.wikipedia.org/wiki/List_of_tz_database_time_zones | No | string | +| filter | query | Same as ListMemosRequest.filter | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetUserMemosStatsResponse](#v2getusermemosstatsresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos/{id} + +#### GET +##### Summary + +GetMemo gets a memo by id. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetMemoResponse](#v2getmemoresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### DELETE +##### Summary + +DeleteMemo deletes a memo by id. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2DeleteMemoResponse](#v2deletememoresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos/{id}/comments + +#### GET +##### Summary + +ListMemoComments lists comments for a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListMemoCommentsResponse](#v2listmemocommentsresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +CreateMemoComment creates a comment for a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | id is the memo id to create comment for. | Yes | integer | +| create.content | query | | No | string | +| create.visibility | query | | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2CreateMemoCommentResponse](#v2creatememocommentresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos/{id}/reactions + +#### GET +##### Summary + +ListMemoReactions lists reactions for a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListMemoReactionsResponse](#v2listmemoreactionsresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +UpsertMemoReaction upserts a reaction for a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | +| reaction.id | query | | No | integer | +| reaction.creator | query | | No | string | +| reaction.contentId | query | | No | string | +| reaction.reactionType | query | | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2UpsertMemoReactionResponse](#v2upsertmemoreactionresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos/{id}/reactions/{reactionId} + +#### DELETE +##### Summary + +DeleteMemoReaction deletes a reaction for a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | +| reactionId | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2DeleteMemoReactionResponse](#v2deletememoreactionresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos/{id}/relations + +#### GET +##### Summary + +ListMemoRelations lists relations for a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListMemoRelationsResponse](#v2listmemorelationsresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +SetMemoRelations sets relations for a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | +| body | body | | Yes | [MemoServiceSetMemoRelationsBody](#memoservicesetmemorelationsbody) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2SetMemoRelationsResponse](#v2setmemorelationsresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos/{id}/resources + +#### GET +##### Summary + +ListMemoResources lists resources for a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListMemoResourcesResponse](#v2listmemoresourcesresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +SetMemoResources sets resources for a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | +| body | body | | Yes | [MemoServiceSetMemoResourcesBody](#memoservicesetmemoresourcesbody) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2SetMemoResourcesResponse](#v2setmemoresourcesresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos/{memo.id} + +#### PATCH +##### Summary + +UpdateMemo updates a memo. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| memo.id | path | id is the system generated unique identifier. | Yes | integer | +| memo | body | | Yes | { **"name"**: string, **"rowStatus"**: [apiv2RowStatus](#apiv2rowstatus), **"creator"**: string, **"creatorId"**: integer, **"createTime"**: dateTime, **"updateTime"**: dateTime, **"displayTime"**: dateTime, **"content"**: string, **"visibility"**: [v2Visibility](#v2visibility), **"pinned"**: boolean, **"parentId"**: integer, **"resources"**: [ [v2Resource](#v2resource) ], **"relations"**: [ [v2MemoRelation](#v2memorelation) ], **"reactions"**: [ [apiv2Reaction](#apiv2reaction) ] } | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2UpdateMemoResponse](#v2updatememoresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/memos:export + +#### POST +##### Summary + +ExportMemos exports memos. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| filter | query | Same as ListMemosRequest.filter | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ExportMemosResponse](#v2exportmemosresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +## ResourceService + +### /api/v2/resources + +#### GET +##### Summary + +ListResources lists all resources. + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListResourcesResponse](#v2listresourcesresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +CreateResource creates a new resource. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| filename | query | | No | string | +| externalLink | query | | No | string | +| type | query | | No | string | +| memoId | query | | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2CreateResourceResponse](#v2createresourceresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/resources/name/{name} + +#### GET +##### Summary + +GetResourceByName returns a resource by name. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | path | | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetResourceByNameResponse](#v2getresourcebynameresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/resources/{id} + +#### GET +##### Summary + +GetResource returns a resource by id. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetResourceResponse](#v2getresourceresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### DELETE +##### Summary + +DeleteResource deletes a resource by id. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2DeleteResourceResponse](#v2deleteresourceresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/resources/{resource.id} + +#### PATCH +##### Summary + +UpdateResource updates a resource. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| resource.id | path | id is the system generated unique identifier. | Yes | integer | +| resource | body | | Yes | { **"name"**: string, **"createTime"**: dateTime, **"filename"**: string, **"externalLink"**: string, **"type"**: string, **"size"**: string (int64), **"memoId"**: integer } | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2UpdateResourceResponse](#v2updateresourceresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +## TagService + +### /api/v2/tags + +#### GET +##### Summary + +ListTags lists tags. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| user | query | The creator of tags. Format: users/{username} | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListTagsResponse](#v2listtagsresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### DELETE +##### Summary + +DeleteTag deletes a tag. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| tag.name | query | | No | string | +| tag.creator | query | The creator of tags. Format: users/{username} | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2DeleteTagResponse](#v2deletetagresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +UpsertTag upserts a tag. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | query | | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2UpsertTagResponse](#v2upserttagresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/tags/suggestion + +#### GET +##### Summary + +GetTagSuggestions gets tag suggestions from the user's memos. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| user | query | The creator of tags. Format: users/{username} | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetTagSuggestionsResponse](#v2gettagsuggestionsresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/tags:batchUpsert + +#### POST +##### Summary + +BatchUpsertTag upserts multiple tags. + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2BatchUpsertTagResponse](#v2batchupserttagresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/tags:rename + +#### PATCH +##### Summary + +RenameTag renames a tag. +All related memos will be updated. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| user | query | The creator of tags. Format: users/{username} | No | string | +| oldName | query | | No | string | +| newName | query | | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2RenameTagResponse](#v2renametagresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +## UserService + +### /api/v2/users + +#### GET +##### Summary + +ListUsers returns a list of users. + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListUsersResponse](#v2listusersresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +CreateUser creates a new user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| user | body | | Yes | [v2User](#v2user) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2CreateUserResponse](#v2createuserresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/{name} + +#### GET +##### Summary + +GetUser gets a user by name. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | path | The name of the user. Format: users/{username} | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetUserResponse](#v2getuserresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### DELETE +##### Summary + +DeleteUser deletes a user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | path | The name of the user. Format: users/{username} | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2DeleteUserResponse](#v2deleteuserresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/{name}/access_tokens + +#### GET +##### Summary + +ListUserAccessTokens returns a list of access tokens for a user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | path | The name of the user. Format: users/{username} | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListUserAccessTokensResponse](#v2listuseraccesstokensresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +CreateUserAccessToken creates a new access token for a user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | path | The name of the user. Format: users/{username} | Yes | string | +| body | body | | Yes | [UserServiceCreateUserAccessTokenBody](#userservicecreateuseraccesstokenbody) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2CreateUserAccessTokenResponse](#v2createuseraccesstokenresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/{name}/access_tokens/{accessToken} + +#### DELETE +##### Summary + +DeleteUserAccessToken deletes an access token for a user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | path | The name of the user. Format: users/{username} | Yes | string | +| accessToken | path | access_token is the access token to delete. | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2DeleteUserAccessTokenResponse](#v2deleteuseraccesstokenresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/{name}/setting + +#### GET +##### Summary + +GetUserSetting gets the setting of a user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | path | The name of the user. Format: users/{username} | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetUserSettingResponse](#v2getusersettingresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/{setting.name} + +#### PATCH +##### Summary + +UpdateUserSetting updates the setting of a user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| setting.name | path | The name of the user. Format: users/{username} | Yes | string | +| setting | body | | Yes | { **"locale"**: string, **"appearance"**: string, **"memoVisibility"**: string, **"telegramUserId"**: string, **"compactView"**: boolean } | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2UpdateUserSettingResponse](#v2updateusersettingresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/{user.name} + +#### PATCH +##### Summary + +UpdateUser updates a user. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| user.name | path | The name of the user. Format: users/{username} | Yes | string | +| user | body | | Yes | { **"id"**: integer, **"role"**: [UserRole](#userrole), **"username"**: string, **"email"**: string, **"nickname"**: string, **"avatarUrl"**: string, **"password"**: string, **"rowStatus"**: [apiv2RowStatus](#apiv2rowstatus), **"createTime"**: dateTime, **"updateTime"**: dateTime } | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2UpdateUserResponse](#v2updateuserresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +## WebhookService + +### /api/v2/webhooks + +#### GET +##### Summary + +ListWebhooks returns a list of webhooks. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| creatorId | query | | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2ListWebhooksResponse](#v2listwebhooksresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### POST +##### Summary + +CreateWebhook creates a new webhook. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| body | body | | Yes | [v2CreateWebhookRequest](#v2createwebhookrequest) | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2CreateWebhookResponse](#v2createwebhookresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/webhooks/{id} + +#### GET +##### Summary + +GetWebhook returns a webhook by id. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetWebhookResponse](#v2getwebhookresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +#### DELETE +##### Summary + +DeleteWebhook deletes a webhook by id. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2DeleteWebhookResponse](#v2deletewebhookresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/webhooks/{webhook.id} + +#### PATCH +##### Summary + +UpdateWebhook updates a webhook. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| webhook.id | path | | Yes | integer | +| webhook | body | | Yes | { **"creatorId"**: integer, **"createdTime"**: dateTime, **"updatedTime"**: dateTime, **"rowStatus"**: [apiv2RowStatus](#apiv2rowstatus), **"name"**: string, **"url"**: string } | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2UpdateWebhookResponse](#v2updatewebhookresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +## WorkspaceService + +### /api/v2/workspace/profile + +#### GET +##### Summary + +GetWorkspaceProfile returns the workspace profile. + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetWorkspaceProfileResponse](#v2getworkspaceprofileresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +## WorkspaceSettingService + +### /api/v2/workspace/{name} + +#### GET +##### Summary + +GetWorkspaceSetting returns the setting by name. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| name | path | The resource name of the workspace setting. Format: settings/{setting} | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetWorkspaceSettingResponse](#v2getworkspacesettingresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +### /api/v2/workspace/{setting.name} + +#### PATCH +##### Summary + +SetWorkspaceSetting updates the setting. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| setting.name | path | name is the name of the setting. Format: settings/{setting} | Yes | string | +| setting | body | setting is the setting to update. | Yes | { **"generalSetting"**: [apiv2WorkspaceGeneralSetting](#apiv2workspacegeneralsetting) } | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2SetWorkspaceSettingResponse](#v2setworkspacesettingresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +## ActivityService + +### /v2/activities/{id} + +#### GET +##### Summary + +GetActivity returns the activity with the given id. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| id | path | | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | A successful response. | [v2GetActivityResponse](#v2getactivityresponse) | +| default | An unexpected error response. | [googlerpcStatus](#googlerpcstatus) | + +--- +### Models + +#### MemoServiceSetMemoRelationsBody + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| relations | [ [v2MemoRelation](#v2memorelation) ] | | No | + +#### MemoServiceSetMemoResourcesBody + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| resources | [ [v2Resource](#v2resource) ] | | No | + +#### UserRole + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| UserRole | string | | | + +#### UserServiceCreateUserAccessTokenBody + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| description | string | | No | +| expiresAt | dateTime | | No | + +#### apiv2ActivityMemoCommentPayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memoId | integer | | No | +| relatedMemoId | integer | | No | + +#### apiv2ActivityPayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memoComment | [apiv2ActivityMemoCommentPayload](#apiv2activitymemocommentpayload) | | No | +| versionUpdate | [apiv2ActivityVersionUpdatePayload](#apiv2activityversionupdatepayload) | | No | + +#### apiv2ActivityVersionUpdatePayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| version | string | | No | + +#### apiv2Reaction + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | | No | +| creator | string | | No | +| contentId | string | | No | +| reactionType | [apiv2ReactionType](#apiv2reactiontype) | | No | + +#### apiv2ReactionType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| apiv2ReactionType | string | | | + +#### apiv2RowStatus + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| apiv2RowStatus | string | | | + +#### apiv2UserSetting + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | +| locale | string | The preferred locale of the user. | No | +| appearance | string | The preferred appearance of the user. | No | +| memoVisibility | string | The default visibility of the memo. | No | +| telegramUserId | string | The telegram user id of the user. | No | +| compactView | boolean | The compact view for a memo. | No | + +#### apiv2Webhook + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | | No | +| creatorId | integer | | No | +| createdTime | dateTime | | No | +| updatedTime | dateTime | | No | +| rowStatus | [apiv2RowStatus](#apiv2rowstatus) | | No | +| name | string | | No | +| url | string | | No | + +#### apiv2WorkspaceGeneralSetting + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| instanceUrl | string | instance_url is the instance URL. | No | +| disallowSignup | boolean | disallow_signup is the flag to disallow signup. | No | +| disallowPasswordLogin | boolean | disallow_password_login is the flag to disallow password login. | No | +| additionalScript | string | additional_script is the additional script. | No | +| additionalStyle | string | additional_style is the additional style. | No | + +#### apiv2WorkspaceSetting + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | +| generalSetting | [apiv2WorkspaceGeneralSetting](#apiv2workspacegeneralsetting) | general_setting is the general setting of workspace. | No | + +#### googlerpcStatus + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| code | integer | | No | +| message | string | | No | +| details | [ [protobufAny](#protobufany) ] | | No | + +#### protobufAny + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| @type | string | | No | + +#### v2Activity + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | | No | +| creatorId | integer | | No | +| type | string | | No | +| level | string | | No | +| createTime | dateTime | | No | +| payload | [apiv2ActivityPayload](#apiv2activitypayload) | | No | + +#### v2BatchUpsertTagResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2BatchUpsertTagResponse | object | | | + +#### v2CreateMemoCommentResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memo | [v2Memo](#v2memo) | | No | + +#### v2CreateMemoRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | string | | No | +| visibility | [v2Visibility](#v2visibility) | | No | + +#### v2CreateMemoResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memo | [v2Memo](#v2memo) | | No | + +#### v2CreateResourceResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| resource | [v2Resource](#v2resource) | | No | + +#### v2CreateUserAccessTokenResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| accessToken | [v2UserAccessToken](#v2useraccesstoken) | | No | + +#### v2CreateUserResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | [v2User](#v2user) | | No | + +#### v2CreateWebhookRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | +| url | string | | No | + +#### v2CreateWebhookResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| webhook | [apiv2Webhook](#apiv2webhook) | | No | + +#### v2DeleteInboxResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2DeleteInboxResponse | object | | | + +#### v2DeleteMemoReactionResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2DeleteMemoReactionResponse | object | | | + +#### v2DeleteMemoResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2DeleteMemoResponse | object | | | + +#### v2DeleteResourceResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2DeleteResourceResponse | object | | | + +#### v2DeleteTagResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2DeleteTagResponse | object | | | + +#### v2DeleteUserAccessTokenResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2DeleteUserAccessTokenResponse | object | | | + +#### v2DeleteUserResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2DeleteUserResponse | object | | | + +#### v2DeleteWebhookResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2DeleteWebhookResponse | object | | | + +#### v2ExportMemosResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | byte | | No | + +#### v2GetActivityResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| activity | [v2Activity](#v2activity) | | No | + +#### v2GetAuthStatusResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | [v2User](#v2user) | | No | + +#### v2GetMemoByNameResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memo | [v2Memo](#v2memo) | | No | + +#### v2GetMemoResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memo | [v2Memo](#v2memo) | | No | + +#### v2GetResourceByNameResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| resource | [v2Resource](#v2resource) | | No | + +#### v2GetResourceResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| resource | [v2Resource](#v2resource) | | No | + +#### v2GetTagSuggestionsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| tags | [ string ] | | No | + +#### v2GetUserMemosStatsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| stats | object | stats is the stats of memo creating/updating activities. key is the year-month-day string. e.g. "2020-01-01". | No | + +#### v2GetUserResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | [v2User](#v2user) | | No | + +#### v2GetUserSettingResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| setting | [apiv2UserSetting](#apiv2usersetting) | | No | + +#### v2GetWebhookResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| webhook | [apiv2Webhook](#apiv2webhook) | | No | + +#### v2GetWorkspaceProfileResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| workspaceProfile | [v2WorkspaceProfile](#v2workspaceprofile) | | No | + +#### v2GetWorkspaceSettingResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| setting | [apiv2WorkspaceSetting](#apiv2workspacesetting) | | No | + +#### v2Inbox + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | +| sender | string | | No | +| receiver | string | | No | +| status | [v2InboxStatus](#v2inboxstatus) | | No | +| createTime | dateTime | | No | +| type | [v2InboxType](#v2inboxtype) | | No | +| activityId | integer | | No | + +#### v2InboxStatus + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2InboxStatus | string | | | + +#### v2InboxType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2InboxType | string | | | + +#### v2ListInboxesResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| inboxes | [ [v2Inbox](#v2inbox) ] | | No | + +#### v2ListMemoCommentsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memos | [ [v2Memo](#v2memo) ] | | No | + +#### v2ListMemoReactionsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| reactions | [ [apiv2Reaction](#apiv2reaction) ] | | No | + +#### v2ListMemoRelationsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| relations | [ [v2MemoRelation](#v2memorelation) ] | | No | + +#### v2ListMemoResourcesResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| resources | [ [v2Resource](#v2resource) ] | | No | + +#### v2ListMemosResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memos | [ [v2Memo](#v2memo) ] | | No | +| nextPageToken | string | A token, which can be sent as `page_token` to retrieve the next page. If this field is omitted, there are no subsequent pages. | No | + +#### v2ListResourcesResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| resources | [ [v2Resource](#v2resource) ] | | No | + +#### v2ListTagsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| tags | [ [v2Tag](#v2tag) ] | | No | + +#### v2ListUserAccessTokensResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| accessTokens | [ [v2UserAccessToken](#v2useraccesstoken) ] | | No | + +#### v2ListUsersResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| users | [ [v2User](#v2user) ] | | No | + +#### v2ListWebhooksResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| webhooks | [ [apiv2Webhook](#apiv2webhook) ] | | No | + +#### v2Memo + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | id is the system generated unique identifier. | No | +| name | string | name is the user provided name. | No | +| rowStatus | [apiv2RowStatus](#apiv2rowstatus) | | No | +| creator | string | | No | +| creatorId | integer | | No | +| createTime | dateTime | | No | +| updateTime | dateTime | | No | +| displayTime | dateTime | | No | +| content | string | | No | +| visibility | [v2Visibility](#v2visibility) | | No | +| pinned | boolean | | No | +| parentId | integer | | No | +| resources | [ [v2Resource](#v2resource) ] | | No | +| relations | [ [v2MemoRelation](#v2memorelation) ] | | No | +| reactions | [ [apiv2Reaction](#apiv2reaction) ] | | No | + +#### v2MemoRelation + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memoId | integer | | No | +| relatedMemoId | integer | | No | +| type | [v2MemoRelationType](#v2memorelationtype) | | No | + +#### v2MemoRelationType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2MemoRelationType | string | | | + +#### v2RenameTagResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| tag | [v2Tag](#v2tag) | | No | + +#### v2Resource + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | id is the system generated unique identifier. | No | +| name | string | name is the user provided name. | No | +| createTime | dateTime | | No | +| filename | string | | No | +| externalLink | string | | No | +| type | string | | No | +| size | string (int64) | | No | +| memoId | integer | | No | + +#### v2SetMemoRelationsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2SetMemoRelationsResponse | object | | | + +#### v2SetMemoResourcesResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2SetMemoResourcesResponse | object | | | + +#### v2SetWorkspaceSettingResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| setting | [apiv2WorkspaceSetting](#apiv2workspacesetting) | | No | + +#### v2SignInResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | [v2User](#v2user) | | No | + +#### v2SignInWithSSOResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | [v2User](#v2user) | | No | + +#### v2SignOutResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2SignOutResponse | object | | | + +#### v2SignUpResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | [v2User](#v2user) | | No | + +#### v2Tag + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | +| creator | string | | No | + +#### v2UpdateInboxResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| inbox | [v2Inbox](#v2inbox) | | No | + +#### v2UpdateMemoResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| memo | [v2Memo](#v2memo) | | No | + +#### v2UpdateResourceResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| resource | [v2Resource](#v2resource) | | No | + +#### v2UpdateUserResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | [v2User](#v2user) | | No | + +#### v2UpdateUserSettingResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| setting | [apiv2UserSetting](#apiv2usersetting) | | No | + +#### v2UpdateWebhookResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| webhook | [apiv2Webhook](#apiv2webhook) | | No | + +#### v2UpsertMemoReactionResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| reaction | [apiv2Reaction](#apiv2reaction) | | No | + +#### v2UpsertTagRequest + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | + +#### v2UpsertTagResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| tag | [v2Tag](#v2tag) | | No | + +#### v2User + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | | No | +| id | integer | | No | +| role | [UserRole](#userrole) | | No | +| username | string | | No | +| email | string | | No | +| nickname | string | | No | +| avatarUrl | string | | No | +| password | string | | No | +| rowStatus | [apiv2RowStatus](#apiv2rowstatus) | | No | +| createTime | dateTime | | No | +| updateTime | dateTime | | No | + +#### v2UserAccessToken + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| accessToken | string | | No | +| description | string | | No | +| issuedAt | dateTime | | No | +| expiresAt | dateTime | | No | + +#### v2Visibility + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| v2Visibility | string | | | + +#### v2WorkspaceProfile + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| version | string | | No | +| mode | string | mode is the instance mode (e.g. "prod", "dev" or "demo"). | No | +| allowRegistration | boolean | allow_registration is whether the registration is allowed. | No | +| disablePasswordLogin | boolean | allow_password_login is whether the password login is allowed. | No | +| additionalScript | string | additional_script is the additional script. | No | +| additionalStyle | string | additional_style is the additional style. | No | diff --git a/server/route/api/v2/apidocs.swagger.yaml b/server/route/api/v2/apidocs.swagger.yaml new file mode 100644 index 0000000000000..a495910fa64bd --- /dev/null +++ b/server/route/api/v2/apidocs.swagger.yaml @@ -0,0 +1,2277 @@ +swagger: "2.0" +info: + title: api/v2/activity_service.proto + version: version not set +tags: + - name: ActivityService + - name: UserService + - name: AuthService + - name: InboxService + - name: LinkService + - name: ResourceService + - name: MemoService + - name: TagService + - name: WebhookService + - name: WorkspaceService + - name: WorkspaceSettingService +consumes: + - application/json +produces: + - application/json +paths: + /api/v2/auth/signin: + post: + summary: SignIn signs in the user with the given username and password. + operationId: AuthService_SignIn + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2SignInResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: username + in: query + required: false + type: string + - name: password + in: query + required: false + type: string + - name: neverExpire + in: query + required: false + type: boolean + tags: + - AuthService + /api/v2/auth/signin/sso: + post: + summary: SignInWithSSO signs in the user with the given SSO code. + operationId: AuthService_SignInWithSSO + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2SignInWithSSOResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: idpId + in: query + required: false + type: integer + format: int32 + - name: code + in: query + required: false + type: string + - name: redirectUri + in: query + required: false + type: string + tags: + - AuthService + /api/v2/auth/signout: + post: + summary: SignOut signs out the user. + operationId: AuthService_SignOut + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2SignOutResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - AuthService + /api/v2/auth/signup: + post: + summary: SignUp signs up the user with the given username and password. + operationId: AuthService_SignUp + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2SignUpResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: username + in: query + required: false + type: string + - name: password + in: query + required: false + type: string + tags: + - AuthService + /api/v2/auth/status: + post: + summary: GetAuthStatus returns the current auth status of the user. + operationId: AuthService_GetAuthStatus + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetAuthStatusResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - AuthService + /api/v2/inboxes: + get: + summary: ListInboxes lists inboxes for a user. + operationId: InboxService_ListInboxes + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListInboxesResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: user + description: 'Format: users/{username}' + in: query + required: false + type: string + tags: + - InboxService + /api/v2/memos: + get: + summary: ListMemos lists memos with pagination and filter. + operationId: MemoService_ListMemos + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListMemosResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: pageSize + description: The maximum number of memos to return. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: |- + A page token, received from a previous `ListMemos` call. + Provide this to retrieve the subsequent page. + in: query + required: false + type: string + - name: filter + description: |- + Filter is used to filter memos returned in the list. + Format: "creator == users/{username} && visibilities == ['PUBLIC', 'PROTECTED']" + in: query + required: false + type: string + tags: + - MemoService + post: + summary: CreateMemo creates a memo. + operationId: MemoService_CreateMemo + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2CreateMemoResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: body + in: body + required: true + schema: + $ref: '#/definitions/v2CreateMemoRequest' + tags: + - MemoService + /api/v2/memos/name/{name}: + get: + summary: GetMemoByName gets a memo by name. + operationId: MemoService_GetMemoByName + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetMemoByNameResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + in: path + required: true + type: string + tags: + - MemoService + /api/v2/memos/stats: + get: + summary: GetUserMemosStats gets stats of memos for a user. + operationId: MemoService_GetUserMemosStats + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetUserMemosStatsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + name is the name of the user to get stats for. + Format: users/{username} + in: query + required: false + type: string + - name: timezone + description: |- + timezone location + Format: uses tz identifier + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + in: query + required: false + type: string + - name: filter + description: Same as ListMemosRequest.filter + in: query + required: false + type: string + tags: + - MemoService + /api/v2/memos/{id}: + get: + summary: GetMemo gets a memo by id. + operationId: MemoService_GetMemo + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetMemoResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - MemoService + delete: + summary: DeleteMemo deletes a memo by id. + operationId: MemoService_DeleteMemo + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2DeleteMemoResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - MemoService + /api/v2/memos/{id}/comments: + get: + summary: ListMemoComments lists comments for a memo. + operationId: MemoService_ListMemoComments + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListMemoCommentsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - MemoService + post: + summary: CreateMemoComment creates a comment for a memo. + operationId: MemoService_CreateMemoComment + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2CreateMemoCommentResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + description: id is the memo id to create comment for. + in: path + required: true + type: integer + format: int32 + - name: create.content + in: query + required: false + type: string + - name: create.visibility + in: query + required: false + type: string + enum: + - VISIBILITY_UNSPECIFIED + - PRIVATE + - PROTECTED + - PUBLIC + default: VISIBILITY_UNSPECIFIED + tags: + - MemoService + /api/v2/memos/{id}/reactions: + get: + summary: ListMemoReactions lists reactions for a memo. + operationId: MemoService_ListMemoReactions + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListMemoReactionsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - MemoService + post: + summary: UpsertMemoReaction upserts a reaction for a memo. + operationId: MemoService_UpsertMemoReaction + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2UpsertMemoReactionResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + - name: reaction.id + in: query + required: false + type: integer + format: int32 + - name: reaction.creator + in: query + required: false + type: string + - name: reaction.contentId + in: query + required: false + type: string + - name: reaction.reactionType + in: query + required: false + type: string + enum: + - TYPE_UNSPECIFIED + - THUMBS_UP + - THUMBS_DOWN + - HEART + - FIRE + - CLAPPING_HANDS + - LAUGH + - OK_HAND + - ROCKET + - EYES + - THINKING_FACE + - CLOWN_FACE + - QUESTION_MARK + default: TYPE_UNSPECIFIED + tags: + - MemoService + /api/v2/memos/{id}/reactions/{reactionId}: + delete: + summary: DeleteMemoReaction deletes a reaction for a memo. + operationId: MemoService_DeleteMemoReaction + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2DeleteMemoReactionResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + - name: reactionId + in: path + required: true + type: integer + format: int32 + tags: + - MemoService + /api/v2/memos/{id}/relations: + get: + summary: ListMemoRelations lists relations for a memo. + operationId: MemoService_ListMemoRelations + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListMemoRelationsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - MemoService + post: + summary: SetMemoRelations sets relations for a memo. + operationId: MemoService_SetMemoRelations + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2SetMemoRelationsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + - name: body + in: body + required: true + schema: + $ref: '#/definitions/MemoServiceSetMemoRelationsBody' + tags: + - MemoService + /api/v2/memos/{id}/resources: + get: + summary: ListMemoResources lists resources for a memo. + operationId: MemoService_ListMemoResources + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListMemoResourcesResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - MemoService + post: + summary: SetMemoResources sets resources for a memo. + operationId: MemoService_SetMemoResources + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2SetMemoResourcesResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + - name: body + in: body + required: true + schema: + $ref: '#/definitions/MemoServiceSetMemoResourcesBody' + tags: + - MemoService + /api/v2/memos/{memo.id}: + patch: + summary: UpdateMemo updates a memo. + operationId: MemoService_UpdateMemo + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2UpdateMemoResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: memo.id + description: id is the system generated unique identifier. + in: path + required: true + type: integer + format: int32 + - name: memo + in: body + required: true + schema: + type: object + properties: + name: + type: string + description: name is the user provided name. + rowStatus: + $ref: '#/definitions/apiv2RowStatus' + creator: + type: string + title: |- + The name of the creator. + Format: users/{username} + creatorId: + type: integer + format: int32 + createTime: + type: string + format: date-time + updateTime: + type: string + format: date-time + displayTime: + type: string + format: date-time + content: + type: string + visibility: + $ref: '#/definitions/v2Visibility' + pinned: + type: boolean + parentId: + type: integer + format: int32 + readOnly: true + resources: + type: array + items: + type: object + $ref: '#/definitions/v2Resource' + readOnly: true + relations: + type: array + items: + type: object + $ref: '#/definitions/v2MemoRelation' + readOnly: true + reactions: + type: array + items: + type: object + $ref: '#/definitions/apiv2Reaction' + readOnly: true + tags: + - MemoService + /api/v2/memos:export: + post: + summary: ExportMemos exports memos. + operationId: MemoService_ExportMemos + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ExportMemosResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: filter + description: Same as ListMemosRequest.filter + in: query + required: false + type: string + tags: + - MemoService + /api/v2/metadata: + get: + operationId: LinkService_GetLinkMetadata + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetLinkMetadataResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: link + in: query + required: false + type: string + tags: + - LinkService + /api/v2/resources: + get: + summary: ListResources lists all resources. + operationId: ResourceService_ListResources + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListResourcesResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - ResourceService + post: + summary: CreateResource creates a new resource. + operationId: ResourceService_CreateResource + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2CreateResourceResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: filename + in: query + required: false + type: string + - name: externalLink + in: query + required: false + type: string + - name: type + in: query + required: false + type: string + - name: memoId + in: query + required: false + type: integer + format: int32 + tags: + - ResourceService + /api/v2/resources/name/{name}: + get: + summary: GetResourceByName returns a resource by name. + operationId: ResourceService_GetResourceByName + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetResourceByNameResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + in: path + required: true + type: string + tags: + - ResourceService + /api/v2/resources/{id}: + get: + summary: GetResource returns a resource by id. + operationId: ResourceService_GetResource + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetResourceResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - ResourceService + delete: + summary: DeleteResource deletes a resource by id. + operationId: ResourceService_DeleteResource + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2DeleteResourceResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - ResourceService + /api/v2/resources/{resource.id}: + patch: + summary: UpdateResource updates a resource. + operationId: ResourceService_UpdateResource + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2UpdateResourceResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: resource.id + description: id is the system generated unique identifier. + in: path + required: true + type: integer + format: int32 + - name: resource + in: body + required: true + schema: + type: object + properties: + name: + type: string + description: name is the user provided name. + createTime: + type: string + format: date-time + filename: + type: string + externalLink: + type: string + type: + type: string + size: + type: string + format: int64 + memoId: + type: integer + format: int32 + tags: + - ResourceService + /api/v2/tags: + get: + summary: ListTags lists tags. + operationId: TagService_ListTags + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListTagsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: user + description: |- + The creator of tags. + Format: users/{username} + in: query + required: false + type: string + tags: + - TagService + delete: + summary: DeleteTag deletes a tag. + operationId: TagService_DeleteTag + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2DeleteTagResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: tag.name + in: query + required: false + type: string + - name: tag.creator + description: |- + The creator of tags. + Format: users/{username} + in: query + required: false + type: string + tags: + - TagService + post: + summary: UpsertTag upserts a tag. + operationId: TagService_UpsertTag + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2UpsertTagResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + in: query + required: false + type: string + tags: + - TagService + /api/v2/tags/suggestion: + get: + summary: GetTagSuggestions gets tag suggestions from the user's memos. + operationId: TagService_GetTagSuggestions + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetTagSuggestionsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: user + description: |- + The creator of tags. + Format: users/{username} + in: query + required: false + type: string + tags: + - TagService + /api/v2/tags:batchUpsert: + post: + summary: BatchUpsertTag upserts multiple tags. + operationId: TagService_BatchUpsertTag + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2BatchUpsertTagResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - TagService + /api/v2/tags:rename: + patch: + summary: |- + RenameTag renames a tag. + All related memos will be updated. + operationId: TagService_RenameTag + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2RenameTagResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: user + description: |- + The creator of tags. + Format: users/{username} + in: query + required: false + type: string + - name: oldName + in: query + required: false + type: string + - name: newName + in: query + required: false + type: string + tags: + - TagService + /api/v2/users: + get: + summary: ListUsers returns a list of users. + operationId: UserService_ListUsers + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListUsersResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - UserService + post: + summary: CreateUser creates a new user. + operationId: UserService_CreateUser + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2CreateUserResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: user + in: body + required: true + schema: + $ref: '#/definitions/v2User' + tags: + - UserService + /api/v2/webhooks: + get: + summary: ListWebhooks returns a list of webhooks. + operationId: WebhookService_ListWebhooks + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListWebhooksResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: creatorId + in: query + required: false + type: integer + format: int32 + tags: + - WebhookService + post: + summary: CreateWebhook creates a new webhook. + operationId: WebhookService_CreateWebhook + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2CreateWebhookResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: body + in: body + required: true + schema: + $ref: '#/definitions/v2CreateWebhookRequest' + tags: + - WebhookService + /api/v2/webhooks/{id}: + get: + summary: GetWebhook returns a webhook by id. + operationId: WebhookService_GetWebhook + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetWebhookResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - WebhookService + delete: + summary: DeleteWebhook deletes a webhook by id. + operationId: WebhookService_DeleteWebhook + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2DeleteWebhookResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - WebhookService + /api/v2/webhooks/{webhook.id}: + patch: + summary: UpdateWebhook updates a webhook. + operationId: WebhookService_UpdateWebhook + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2UpdateWebhookResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: webhook.id + in: path + required: true + type: integer + format: int32 + - name: webhook + in: body + required: true + schema: + type: object + properties: + creatorId: + type: integer + format: int32 + createdTime: + type: string + format: date-time + updatedTime: + type: string + format: date-time + rowStatus: + $ref: '#/definitions/apiv2RowStatus' + name: + type: string + url: + type: string + tags: + - WebhookService + /api/v2/workspace/profile: + get: + summary: GetWorkspaceProfile returns the workspace profile. + operationId: WorkspaceService_GetWorkspaceProfile + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetWorkspaceProfileResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - WorkspaceService + /api/v2/workspace/{name}: + get: + summary: GetWorkspaceSetting returns the setting by name. + operationId: WorkspaceSettingService_GetWorkspaceSetting + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetWorkspaceSettingResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + The resource name of the workspace setting. + Format: settings/{setting} + in: path + required: true + type: string + pattern: settings/[^/]+ + tags: + - WorkspaceSettingService + /api/v2/workspace/{setting.name}: + patch: + summary: SetWorkspaceSetting updates the setting. + operationId: WorkspaceSettingService_SetWorkspaceSetting + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2SetWorkspaceSettingResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: setting.name + description: |- + name is the name of the setting. + Format: settings/{setting} + in: path + required: true + type: string + pattern: settings/[^/]+ + - name: setting + description: setting is the setting to update. + in: body + required: true + schema: + type: object + properties: + generalSetting: + $ref: '#/definitions/apiv2WorkspaceGeneralSetting' + description: general_setting is the general setting of workspace. + title: setting is the setting to update. + tags: + - WorkspaceSettingService + /api/v2/{inbox.name}: + patch: + summary: UpdateInbox updates an inbox. + operationId: InboxService_UpdateInbox + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2UpdateInboxResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: inbox.name + description: |- + The name of the inbox. + Format: inboxes/{uid} + in: path + required: true + type: string + pattern: inboxes/[^/]+ + - name: inbox + in: body + required: true + schema: + type: object + properties: + sender: + type: string + title: 'Format: users/{username}' + receiver: + type: string + title: 'Format: users/{username}' + status: + $ref: '#/definitions/v2InboxStatus' + createTime: + type: string + format: date-time + type: + $ref: '#/definitions/v2InboxType' + activityId: + type: integer + format: int32 + tags: + - InboxService + /api/v2/{name_1}: + delete: + summary: DeleteInbox deletes an inbox. + operationId: InboxService_DeleteInbox + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2DeleteInboxResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_1 + description: |- + The name of the inbox to delete. + Format: inboxes/{uid} + in: path + required: true + type: string + pattern: inboxes/[^/]+ + tags: + - InboxService + /api/v2/{name}: + get: + summary: GetUser gets a user by name. + operationId: UserService_GetUser + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetUserResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + The name of the user. + Format: users/{username} + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - UserService + delete: + summary: DeleteUser deletes a user. + operationId: UserService_DeleteUser + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2DeleteUserResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + The name of the user. + Format: users/{username} + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - UserService + /api/v2/{name}/access_tokens: + get: + summary: ListUserAccessTokens returns a list of access tokens for a user. + operationId: UserService_ListUserAccessTokens + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2ListUserAccessTokensResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + The name of the user. + Format: users/{username} + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - UserService + post: + summary: CreateUserAccessToken creates a new access token for a user. + operationId: UserService_CreateUserAccessToken + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2CreateUserAccessTokenResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + The name of the user. + Format: users/{username} + in: path + required: true + type: string + pattern: users/[^/]+ + - name: body + in: body + required: true + schema: + $ref: '#/definitions/UserServiceCreateUserAccessTokenBody' + tags: + - UserService + /api/v2/{name}/access_tokens/{accessToken}: + delete: + summary: DeleteUserAccessToken deletes an access token for a user. + operationId: UserService_DeleteUserAccessToken + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2DeleteUserAccessTokenResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + The name of the user. + Format: users/{username} + in: path + required: true + type: string + pattern: users/[^/]+ + - name: accessToken + description: access_token is the access token to delete. + in: path + required: true + type: string + tags: + - UserService + /api/v2/{name}/setting: + get: + summary: GetUserSetting gets the setting of a user. + operationId: UserService_GetUserSetting + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetUserSettingResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + The name of the user. + Format: users/{username} + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - UserService + /api/v2/{setting.name}: + patch: + summary: UpdateUserSetting updates the setting of a user. + operationId: UserService_UpdateUserSetting + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2UpdateUserSettingResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: setting.name + description: |- + The name of the user. + Format: users/{username} + in: path + required: true + type: string + pattern: users/[^/]+/setting + - name: setting + in: body + required: true + schema: + type: object + properties: + locale: + type: string + description: The preferred locale of the user. + appearance: + type: string + description: The preferred appearance of the user. + memoVisibility: + type: string + description: The default visibility of the memo. + telegramUserId: + type: string + description: The telegram user id of the user. + tags: + - UserService + /api/v2/{user.name}: + patch: + summary: UpdateUser updates a user. + operationId: UserService_UpdateUser + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2UpdateUserResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: user.name + description: |- + The name of the user. + Format: users/{username} + in: path + required: true + type: string + pattern: users/[^/]+ + - name: user + in: body + required: true + schema: + type: object + properties: + id: + type: integer + format: int32 + role: + $ref: '#/definitions/UserRole' + username: + type: string + email: + type: string + nickname: + type: string + avatarUrl: + type: string + password: + type: string + rowStatus: + $ref: '#/definitions/apiv2RowStatus' + createTime: + type: string + format: date-time + updateTime: + type: string + format: date-time + tags: + - UserService + /v2/activities/{id}: + get: + summary: GetActivity returns the activity with the given id. + operationId: ActivityService_GetActivity + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v2GetActivityResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: id + in: path + required: true + type: integer + format: int32 + tags: + - ActivityService +definitions: + MemoServiceSetMemoRelationsBody: + type: object + properties: + relations: + type: array + items: + type: object + $ref: '#/definitions/v2MemoRelation' + MemoServiceSetMemoResourcesBody: + type: object + properties: + resources: + type: array + items: + type: object + $ref: '#/definitions/v2Resource' + UserRole: + type: string + enum: + - ROLE_UNSPECIFIED + - HOST + - ADMIN + - USER + default: ROLE_UNSPECIFIED + UserServiceCreateUserAccessTokenBody: + type: object + properties: + description: + type: string + expiresAt: + type: string + format: date-time + apiv2ActivityMemoCommentPayload: + type: object + properties: + memoId: + type: integer + format: int32 + relatedMemoId: + type: integer + format: int32 + apiv2ActivityPayload: + type: object + properties: + memoComment: + $ref: '#/definitions/apiv2ActivityMemoCommentPayload' + versionUpdate: + $ref: '#/definitions/apiv2ActivityVersionUpdatePayload' + apiv2ActivityVersionUpdatePayload: + type: object + properties: + version: + type: string + apiv2Reaction: + type: object + properties: + id: + type: integer + format: int32 + creator: + type: string + contentId: + type: string + reactionType: + $ref: '#/definitions/apiv2ReactionType' + apiv2ReactionType: + type: string + enum: + - TYPE_UNSPECIFIED + - THUMBS_UP + - THUMBS_DOWN + - HEART + - FIRE + - CLAPPING_HANDS + - LAUGH + - OK_HAND + - ROCKET + - EYES + - THINKING_FACE + - CLOWN_FACE + - QUESTION_MARK + default: TYPE_UNSPECIFIED + apiv2RowStatus: + type: string + enum: + - ROW_STATUS_UNSPECIFIED + - ACTIVE + - ARCHIVED + default: ROW_STATUS_UNSPECIFIED + apiv2UserSetting: + type: object + properties: + name: + type: string + title: |- + The name of the user. + Format: users/{username} + locale: + type: string + description: The preferred locale of the user. + appearance: + type: string + description: The preferred appearance of the user. + memoVisibility: + type: string + description: The default visibility of the memo. + telegramUserId: + type: string + description: The telegram user id of the user. + apiv2Webhook: + type: object + properties: + id: + type: integer + format: int32 + creatorId: + type: integer + format: int32 + createdTime: + type: string + format: date-time + updatedTime: + type: string + format: date-time + rowStatus: + $ref: '#/definitions/apiv2RowStatus' + name: + type: string + url: + type: string + apiv2WorkspaceGeneralSetting: + type: object + properties: + instanceUrl: + type: string + description: instance_url is the instance URL. + disallowSignup: + type: boolean + description: disallow_signup is the flag to disallow signup. + disallowPasswordLogin: + type: boolean + description: disallow_password_login is the flag to disallow password login. + additionalScript: + type: string + description: additional_script is the additional script. + additionalStyle: + type: string + description: additional_style is the additional style. + apiv2WorkspaceSetting: + type: object + properties: + name: + type: string + title: |- + name is the name of the setting. + Format: settings/{setting} + generalSetting: + $ref: '#/definitions/apiv2WorkspaceGeneralSetting' + description: general_setting is the general setting of workspace. + googlerpcStatus: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + $ref: '#/definitions/protobufAny' + protobufAny: + type: object + properties: + '@type': + type: string + additionalProperties: {} + v2Activity: + type: object + properties: + id: + type: integer + format: int32 + creatorId: + type: integer + format: int32 + type: + type: string + level: + type: string + createTime: + type: string + format: date-time + payload: + $ref: '#/definitions/apiv2ActivityPayload' + v2BatchUpsertTagResponse: + type: object + v2CreateMemoCommentResponse: + type: object + properties: + memo: + $ref: '#/definitions/v2Memo' + v2CreateMemoRequest: + type: object + properties: + content: + type: string + visibility: + $ref: '#/definitions/v2Visibility' + v2CreateMemoResponse: + type: object + properties: + memo: + $ref: '#/definitions/v2Memo' + v2CreateResourceResponse: + type: object + properties: + resource: + $ref: '#/definitions/v2Resource' + v2CreateUserAccessTokenResponse: + type: object + properties: + accessToken: + $ref: '#/definitions/v2UserAccessToken' + v2CreateUserResponse: + type: object + properties: + user: + $ref: '#/definitions/v2User' + v2CreateWebhookRequest: + type: object + properties: + name: + type: string + url: + type: string + v2CreateWebhookResponse: + type: object + properties: + webhook: + $ref: '#/definitions/apiv2Webhook' + v2DeleteInboxResponse: + type: object + v2DeleteMemoReactionResponse: + type: object + v2DeleteMemoResponse: + type: object + v2DeleteResourceResponse: + type: object + v2DeleteTagResponse: + type: object + v2DeleteUserAccessTokenResponse: + type: object + v2DeleteUserResponse: + type: object + v2DeleteWebhookResponse: + type: object + v2ExportMemosResponse: + type: object + properties: + content: + type: string + format: byte + v2GetActivityResponse: + type: object + properties: + activity: + $ref: '#/definitions/v2Activity' + v2GetAuthStatusResponse: + type: object + properties: + user: + $ref: '#/definitions/v2User' + v2GetLinkMetadataResponse: + type: object + properties: + metadata: + $ref: '#/definitions/v2LinkMetadata' + v2GetMemoByNameResponse: + type: object + properties: + memo: + $ref: '#/definitions/v2Memo' + v2GetMemoResponse: + type: object + properties: + memo: + $ref: '#/definitions/v2Memo' + v2GetResourceByNameResponse: + type: object + properties: + resource: + $ref: '#/definitions/v2Resource' + v2GetResourceResponse: + type: object + properties: + resource: + $ref: '#/definitions/v2Resource' + v2GetTagSuggestionsResponse: + type: object + properties: + tags: + type: array + items: + type: string + v2GetUserMemosStatsResponse: + type: object + properties: + stats: + type: object + additionalProperties: + type: integer + format: int32 + description: |- + stats is the stats of memo creating/updating activities. + key is the year-month-day string. e.g. "2020-01-01". + v2GetUserResponse: + type: object + properties: + user: + $ref: '#/definitions/v2User' + v2GetUserSettingResponse: + type: object + properties: + setting: + $ref: '#/definitions/apiv2UserSetting' + v2GetWebhookResponse: + type: object + properties: + webhook: + $ref: '#/definitions/apiv2Webhook' + v2GetWorkspaceProfileResponse: + type: object + properties: + workspaceProfile: + $ref: '#/definitions/v2WorkspaceProfile' + v2GetWorkspaceSettingResponse: + type: object + properties: + setting: + $ref: '#/definitions/apiv2WorkspaceSetting' + v2Inbox: + type: object + properties: + name: + type: string + title: |- + The name of the inbox. + Format: inboxes/{uid} + sender: + type: string + title: 'Format: users/{username}' + receiver: + type: string + title: 'Format: users/{username}' + status: + $ref: '#/definitions/v2InboxStatus' + createTime: + type: string + format: date-time + type: + $ref: '#/definitions/v2InboxType' + activityId: + type: integer + format: int32 + v2InboxStatus: + type: string + enum: + - STATUS_UNSPECIFIED + - UNREAD + - ARCHIVED + default: STATUS_UNSPECIFIED + v2InboxType: + type: string + enum: + - TYPE_UNSPECIFIED + - TYPE_MEMO_COMMENT + - TYPE_VERSION_UPDATE + default: TYPE_UNSPECIFIED + v2LinkMetadata: + type: object + properties: + title: + type: string + description: + type: string + image: + type: string + v2ListInboxesResponse: + type: object + properties: + inboxes: + type: array + items: + type: object + $ref: '#/definitions/v2Inbox' + v2ListMemoCommentsResponse: + type: object + properties: + memos: + type: array + items: + type: object + $ref: '#/definitions/v2Memo' + v2ListMemoReactionsResponse: + type: object + properties: + reactions: + type: array + items: + type: object + $ref: '#/definitions/apiv2Reaction' + v2ListMemoRelationsResponse: + type: object + properties: + relations: + type: array + items: + type: object + $ref: '#/definitions/v2MemoRelation' + v2ListMemoResourcesResponse: + type: object + properties: + resources: + type: array + items: + type: object + $ref: '#/definitions/v2Resource' + v2ListMemosResponse: + type: object + properties: + memos: + type: array + items: + type: object + $ref: '#/definitions/v2Memo' + nextPageToken: + type: string + description: |- + A token, which can be sent as `page_token` to retrieve the next page. + If this field is omitted, there are no subsequent pages. + v2ListResourcesResponse: + type: object + properties: + resources: + type: array + items: + type: object + $ref: '#/definitions/v2Resource' + v2ListTagsResponse: + type: object + properties: + tags: + type: array + items: + type: object + $ref: '#/definitions/v2Tag' + v2ListUserAccessTokensResponse: + type: object + properties: + accessTokens: + type: array + items: + type: object + $ref: '#/definitions/v2UserAccessToken' + v2ListUsersResponse: + type: object + properties: + users: + type: array + items: + type: object + $ref: '#/definitions/v2User' + v2ListWebhooksResponse: + type: object + properties: + webhooks: + type: array + items: + type: object + $ref: '#/definitions/apiv2Webhook' + v2Memo: + type: object + properties: + id: + type: integer + format: int32 + description: id is the system generated unique identifier. + name: + type: string + description: name is the user provided name. + rowStatus: + $ref: '#/definitions/apiv2RowStatus' + creator: + type: string + title: |- + The name of the creator. + Format: users/{username} + creatorId: + type: integer + format: int32 + createTime: + type: string + format: date-time + updateTime: + type: string + format: date-time + displayTime: + type: string + format: date-time + content: + type: string + visibility: + $ref: '#/definitions/v2Visibility' + pinned: + type: boolean + parentId: + type: integer + format: int32 + readOnly: true + resources: + type: array + items: + type: object + $ref: '#/definitions/v2Resource' + readOnly: true + relations: + type: array + items: + type: object + $ref: '#/definitions/v2MemoRelation' + readOnly: true + reactions: + type: array + items: + type: object + $ref: '#/definitions/apiv2Reaction' + readOnly: true + v2MemoRelation: + type: object + properties: + memoId: + type: integer + format: int32 + relatedMemoId: + type: integer + format: int32 + type: + $ref: '#/definitions/v2MemoRelationType' + v2MemoRelationType: + type: string + enum: + - TYPE_UNSPECIFIED + - REFERENCE + - COMMENT + default: TYPE_UNSPECIFIED + v2RenameTagResponse: + type: object + properties: + tag: + $ref: '#/definitions/v2Tag' + v2Resource: + type: object + properties: + id: + type: integer + format: int32 + description: id is the system generated unique identifier. + name: + type: string + description: name is the user provided name. + createTime: + type: string + format: date-time + filename: + type: string + externalLink: + type: string + type: + type: string + size: + type: string + format: int64 + memoId: + type: integer + format: int32 + v2SetMemoRelationsResponse: + type: object + v2SetMemoResourcesResponse: + type: object + v2SetWorkspaceSettingResponse: + type: object + properties: + setting: + $ref: '#/definitions/apiv2WorkspaceSetting' + v2SignInResponse: + type: object + properties: + user: + $ref: '#/definitions/v2User' + v2SignInWithSSOResponse: + type: object + properties: + user: + $ref: '#/definitions/v2User' + v2SignOutResponse: + type: object + v2SignUpResponse: + type: object + properties: + user: + $ref: '#/definitions/v2User' + v2Tag: + type: object + properties: + name: + type: string + creator: + type: string + title: |- + The creator of tags. + Format: users/{username} + v2UpdateInboxResponse: + type: object + properties: + inbox: + $ref: '#/definitions/v2Inbox' + v2UpdateMemoResponse: + type: object + properties: + memo: + $ref: '#/definitions/v2Memo' + v2UpdateResourceResponse: + type: object + properties: + resource: + $ref: '#/definitions/v2Resource' + v2UpdateUserResponse: + type: object + properties: + user: + $ref: '#/definitions/v2User' + v2UpdateUserSettingResponse: + type: object + properties: + setting: + $ref: '#/definitions/apiv2UserSetting' + v2UpdateWebhookResponse: + type: object + properties: + webhook: + $ref: '#/definitions/apiv2Webhook' + v2UpsertMemoReactionResponse: + type: object + properties: + reaction: + $ref: '#/definitions/apiv2Reaction' + v2UpsertTagRequest: + type: object + properties: + name: + type: string + v2UpsertTagResponse: + type: object + properties: + tag: + $ref: '#/definitions/v2Tag' + v2User: + type: object + properties: + name: + type: string + title: |- + The name of the user. + Format: users/{username} + id: + type: integer + format: int32 + role: + $ref: '#/definitions/UserRole' + username: + type: string + email: + type: string + nickname: + type: string + avatarUrl: + type: string + password: + type: string + rowStatus: + $ref: '#/definitions/apiv2RowStatus' + createTime: + type: string + format: date-time + updateTime: + type: string + format: date-time + v2UserAccessToken: + type: object + properties: + accessToken: + type: string + description: + type: string + issuedAt: + type: string + format: date-time + expiresAt: + type: string + format: date-time + v2Visibility: + type: string + enum: + - VISIBILITY_UNSPECIFIED + - PRIVATE + - PROTECTED + - PUBLIC + default: VISIBILITY_UNSPECIFIED + v2WorkspaceProfile: + type: object + properties: + version: + type: string + title: version is the current version of instance + mode: + type: string + description: mode is the instance mode (e.g. "prod", "dev" or "demo"). + allowRegistration: + type: boolean + description: allow_registration is whether the registration is allowed. + disablePasswordLogin: + type: boolean + description: allow_password_login is whether the password login is allowed. + additionalScript: + type: string + description: additional_script is the additional script. + additionalStyle: + type: string + description: additional_style is the additional style. diff --git a/server/route/api/v2/auth_service.go b/server/route/api/v2/auth_service.go new file mode 100644 index 0000000000000..ef4b139ff4b68 --- /dev/null +++ b/server/route/api/v2/auth_service.go @@ -0,0 +1,268 @@ +package v2 + +import ( + "context" + "fmt" + "regexp" + "strings" + "time" + + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/usememos/memos/internal/util" + "github.com/usememos/memos/plugin/idp" + "github.com/usememos/memos/plugin/idp/oauth2" + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" + "github.com/usememos/memos/server/route/api/auth" + "github.com/usememos/memos/store" +) + +func (s *APIV2Service) GetAuthStatus(ctx context.Context, _ *apiv2pb.GetAuthStatusRequest) (*apiv2pb.GetAuthStatusResponse, error) { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err) + } + if user == nil { + // Set the cookie header to expire access token. + if err := s.clearAccessTokenCookie(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "failed to set grpc header") + } + return nil, status.Errorf(codes.Unauthenticated, "user not found") + } + return &apiv2pb.GetAuthStatusResponse{ + User: convertUserFromStore(user), + }, nil +} + +func (s *APIV2Service) SignIn(ctx context.Context, request *apiv2pb.SignInRequest) (*apiv2pb.SignInResponse, error) { + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &request.Username, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to find user by username %s", request.Username)) + } + if user == nil { + return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("user not found with username %s", request.Username)) + } else if user.RowStatus == store.Archived { + return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("user has been archived with username %s", request.Username)) + } + + // Compare the stored hashed password, with the hashed version of the password that was received. + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(request.Password)); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "unmatched email and password") + } + + expireTime := time.Now().Add(auth.AccessTokenDuration) + if request.NeverExpire { + // Set the expire time to 100 years. + expireTime = time.Now().Add(100 * 365 * 24 * time.Hour) + } + if err := s.doSignIn(ctx, user, expireTime); err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to sign in, err: %s", err)) + } + return &apiv2pb.SignInResponse{ + User: convertUserFromStore(user), + }, nil +} + +func (s *APIV2Service) SignInWithSSO(ctx context.Context, request *apiv2pb.SignInWithSSORequest) (*apiv2pb.SignInWithSSOResponse, error) { + identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{ + ID: &request.IdpId, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get identity provider, err: %s", err)) + } + if identityProvider == nil { + return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("identity provider not found with id %d", request.IdpId)) + } + + var userInfo *idp.IdentityProviderUserInfo + if identityProvider.Type == store.IdentityProviderOAuth2Type { + oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.OAuth2Config) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create oauth2 identity provider, err: %s", err)) + } + token, err := oauth2IdentityProvider.ExchangeToken(ctx, request.RedirectUri, request.Code) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to exchange token, err: %s", err)) + } + userInfo, err = oauth2IdentityProvider.UserInfo(token) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get user info, err: %s", err)) + } + } + + identifierFilter := identityProvider.IdentifierFilter + if identifierFilter != "" { + identifierFilterRegex, err := regexp.Compile(identifierFilter) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to compile identifier filter regex, err: %s", err)) + } + if !identifierFilterRegex.MatchString(userInfo.Identifier) { + return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("identifier %s is not allowed", userInfo.Identifier)) + } + } + + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &userInfo.Identifier, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to find user by username %s", userInfo.Identifier)) + } + if user == nil { + userCreate := &store.User{ + Username: userInfo.Identifier, + // The new signup user should be normal user by default. + Role: store.RoleUser, + Nickname: userInfo.DisplayName, + Email: userInfo.Email, + } + password, err := util.RandomString(20) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to generate random password, err: %s", err)) + } + passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to generate password hash, err: %s", err)) + } + userCreate.PasswordHash = string(passwordHash) + user, err = s.Store.CreateUser(ctx, userCreate) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create user, err: %s", err)) + } + } + if user.RowStatus == store.Archived { + return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("user has been archived with username %s", userInfo.Identifier)) + } + + if err := s.doSignIn(ctx, user, time.Now().Add(auth.AccessTokenDuration)); err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to sign in, err: %s", err)) + } + return &apiv2pb.SignInWithSSOResponse{ + User: convertUserFromStore(user), + }, nil +} + +func (s *APIV2Service) doSignIn(ctx context.Context, user *store.User, expireTime time.Time) error { + accessToken, err := auth.GenerateAccessToken(user.Email, user.ID, expireTime, []byte(s.Secret)) + if err != nil { + return status.Errorf(codes.Internal, fmt.Sprintf("failed to generate tokens, err: %s", err)) + } + if err := s.UpsertAccessTokenToStore(ctx, user, accessToken, "user login"); err != nil { + return status.Errorf(codes.Internal, fmt.Sprintf("failed to upsert access token to store, err: %s", err)) + } + + cookie, err := s.buildAccessTokenCookie(ctx, accessToken, expireTime) + if err != nil { + return status.Errorf(codes.Internal, fmt.Sprintf("failed to build access token cookie, err: %s", err)) + } + if err := grpc.SetHeader(ctx, metadata.New(map[string]string{ + "Set-Cookie": cookie, + })); err != nil { + return status.Errorf(codes.Internal, "failed to set grpc header, error: %v", err) + } + + return nil +} + +func (s *APIV2Service) SignUp(ctx context.Context, request *apiv2pb.SignUpRequest) (*apiv2pb.SignUpResponse, error) { + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get workspace setting, err: %s", err)) + } + if workspaceGeneralSetting.DisallowSignup || workspaceGeneralSetting.DisallowPasswordLogin { + return nil, status.Errorf(codes.PermissionDenied, "sign up is not allowed") + } + + passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to generate password hash, err: %s", err)) + } + + create := &store.User{ + Username: request.Username, + Nickname: request.Username, + PasswordHash: string(passwordHash), + } + + hostUserType := store.RoleHost + existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{ + Role: &hostUserType, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to list users, err: %s", err)) + } + if len(existedHostUsers) == 0 { + // Change the default role to host if there is no host user. + create.Role = store.RoleHost + } else { + create.Role = store.RoleUser + } + + user, err := s.Store.CreateUser(ctx, create) + if err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create user, err: %s", err)) + } + + if err := s.doSignIn(ctx, user, time.Now().Add(auth.AccessTokenDuration)); err != nil { + return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to sign in, err: %s", err)) + } + return &apiv2pb.SignUpResponse{ + User: convertUserFromStore(user), + }, nil +} + +func (s *APIV2Service) SignOut(ctx context.Context, _ *apiv2pb.SignOutRequest) (*apiv2pb.SignOutResponse, error) { + if err := s.clearAccessTokenCookie(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "failed to set grpc header, error: %v", err) + } + return &apiv2pb.SignOutResponse{}, nil +} + +func (s *APIV2Service) clearAccessTokenCookie(ctx context.Context) error { + cookie, err := s.buildAccessTokenCookie(ctx, "", time.Time{}) + if err != nil { + return errors.Wrap(err, "failed to build access token cookie") + } + if err := grpc.SetHeader(ctx, metadata.New(map[string]string{ + "Set-Cookie": cookie, + })); err != nil { + return errors.Wrap(err, "failed to set grpc header") + } + return nil +} + +func (*APIV2Service) buildAccessTokenCookie(ctx context.Context, accessToken string, expireTime time.Time) (string, error) { + attrs := []string{ + fmt.Sprintf("%s=%s", auth.AccessTokenCookieName, accessToken), + "Path=/", + "HttpOnly", + } + if expireTime.IsZero() { + attrs = append(attrs, "Expires=Thu, 01 Jan 1970 00:00:00 GMT") + } else { + attrs = append(attrs, "Expires="+expireTime.Format(time.RFC1123)) + } + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", errors.New("failed to get metadata from context") + } + var origin string + for _, v := range md.Get("origin") { + origin = v + } + isHTTPS := strings.HasPrefix(origin, "https://") + if isHTTPS { + attrs = append(attrs, "SameSite=None") + attrs = append(attrs, "Secure") + } else { + attrs = append(attrs, "SameSite=Strict") + } + return strings.Join(attrs, "; "), nil +} diff --git a/api/v2/common.go b/server/route/api/v2/common.go similarity index 53% rename from api/v2/common.go rename to server/route/api/v2/common.go index fac53be720b50..3ed0856742216 100644 --- a/api/v2/common.go +++ b/server/route/api/v2/common.go @@ -2,6 +2,10 @@ package v2 import ( "context" + "encoding/base64" + + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" apiv2pb "github.com/usememos/memos/proto/gen/api/v2" "github.com/usememos/memos/store" @@ -42,3 +46,29 @@ func getCurrentUser(ctx context.Context, s *store.Store) (*store.User, error) { } return user, nil } + +func getPageToken(limit int, offset int) (string, error) { + return marshalPageToken(&apiv2pb.PageToken{ + Limit: int32(limit), + Offset: int32(offset), + }) +} + +func marshalPageToken(pageToken *apiv2pb.PageToken) (string, error) { + b, err := proto.Marshal(pageToken) + if err != nil { + return "", errors.Wrapf(err, "failed to marshal page token") + } + return base64.StdEncoding.EncodeToString(b), nil +} + +func unmarshalPageToken(s string, pageToken *apiv2pb.PageToken) error { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return errors.Wrapf(err, "failed to decode page token") + } + if err := proto.Unmarshal(b, pageToken); err != nil { + return errors.Wrapf(err, "failed to unmarshal page token") + } + return nil +} diff --git a/api/v2/inbox_service .go b/server/route/api/v2/inbox_service.go similarity index 95% rename from api/v2/inbox_service .go rename to server/route/api/v2/inbox_service.go index 75df3a75937c3..51b170b2e1ba4 100644 --- a/api/v2/inbox_service .go +++ b/server/route/api/v2/inbox_service.go @@ -46,15 +46,15 @@ func (s *APIV2Service) UpdateInbox(ctx context.Context, request *apiv2pb.UpdateI return nil, status.Errorf(codes.InvalidArgument, "update mask is required") } - inboxID, err := GetInboxID(request.Inbox.Name) + inboxID, err := ExtractInboxIDFromName(request.Inbox.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name: %v", err) } update := &store.UpdateInbox{ ID: inboxID, } - for _, path := range request.UpdateMask.Paths { - if path == "status" { + for _, field := range request.UpdateMask.Paths { + if field == "status" { if request.Inbox.Status == apiv2pb.Inbox_STATUS_UNSPECIFIED { return nil, status.Errorf(codes.InvalidArgument, "status is required") } @@ -77,7 +77,7 @@ func (s *APIV2Service) UpdateInbox(ctx context.Context, request *apiv2pb.UpdateI } func (s *APIV2Service) DeleteInbox(ctx context.Context, request *apiv2pb.DeleteInboxRequest) (*apiv2pb.DeleteInboxResponse, error) { - inboxID, err := GetInboxID(request.Name) + inboxID, err := ExtractInboxIDFromName(request.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name: %v", err) } diff --git a/server/route/api/v2/link_service.go b/server/route/api/v2/link_service.go new file mode 100644 index 0000000000000..5d8948393bbe9 --- /dev/null +++ b/server/route/api/v2/link_service.go @@ -0,0 +1,23 @@ +package v2 + +import ( + "context" + + getter "github.com/usememos/memos/plugin/http-getter" + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" +) + +func (*APIV2Service) GetLinkMetadata(_ context.Context, request *apiv2pb.GetLinkMetadataRequest) (*apiv2pb.GetLinkMetadataResponse, error) { + htmlMeta, err := getter.GetHTMLMeta(request.Link) + if err != nil { + return nil, err + } + + return &apiv2pb.GetLinkMetadataResponse{ + Metadata: &apiv2pb.LinkMetadata{ + Title: htmlMeta.Title, + Description: htmlMeta.Description, + Image: htmlMeta.Image, + }, + }, nil +} diff --git a/server/route/api/v2/logger_interceptor.go b/server/route/api/v2/logger_interceptor.go new file mode 100644 index 0000000000000..5f7d2f2d995b0 --- /dev/null +++ b/server/route/api/v2/logger_interceptor.go @@ -0,0 +1,48 @@ +package v2 + +import ( + "context" + "log/slog" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type LoggerInterceptor struct { +} + +func NewLoggerInterceptor() *LoggerInterceptor { + return &LoggerInterceptor{} +} + +func (in *LoggerInterceptor) LoggerInterceptor(ctx context.Context, request any, serverInfo *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + resp, err := handler(ctx, request) + in.loggerInterceptorDo(ctx, serverInfo.FullMethod, err) + return resp, err +} + +func (*LoggerInterceptor) loggerInterceptorDo(ctx context.Context, fullMethod string, err error) { + st := status.Convert(err) + var logLevel slog.Level + var logMsg string + switch st.Code() { + case codes.OK: + logLevel = slog.LevelInfo + logMsg = "OK" + case codes.Unauthenticated, codes.OutOfRange, codes.PermissionDenied, codes.NotFound: + logLevel = slog.LevelInfo + logMsg = "client error" + case codes.Internal, codes.Unknown, codes.DataLoss, codes.Unavailable, codes.DeadlineExceeded: + logLevel = slog.LevelError + logMsg = "server error" + default: + logLevel = slog.LevelError + logMsg = "unknown error" + } + logAttrs := []slog.Attr{slog.String("method", fullMethod)} + if err != nil { + logAttrs = append(logAttrs, slog.String("error", err.Error())) + } + slog.LogAttrs(ctx, logLevel, logMsg, logAttrs...) +} diff --git a/server/route/api/v2/memo_relation_service.go b/server/route/api/v2/memo_relation_service.go new file mode 100644 index 0000000000000..3c20e43f11ccb --- /dev/null +++ b/server/route/api/v2/memo_relation_service.go @@ -0,0 +1,100 @@ +package v2 + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" + "github.com/usememos/memos/store" +) + +func (s *APIV2Service) SetMemoRelations(ctx context.Context, request *apiv2pb.SetMemoRelationsRequest) (*apiv2pb.SetMemoRelationsResponse, error) { + referenceType := store.MemoRelationReference + // Delete all reference relations first. + if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{ + MemoID: &request.Id, + Type: &referenceType, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete memo relation") + } + + for _, relation := range request.Relations { + // Ignore reflexive relations. + if request.Id == relation.RelatedMemoId { + continue + } + // Ignore comment relations as there's no need to update a comment's relation. + // Inserting/Deleting a comment is handled elsewhere. + if relation.Type == apiv2pb.MemoRelation_COMMENT { + continue + } + if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{ + MemoID: request.Id, + RelatedMemoID: relation.RelatedMemoId, + Type: convertMemoRelationTypeToStore(relation.Type), + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert memo relation") + } + } + + return &apiv2pb.SetMemoRelationsResponse{}, nil +} + +func (s *APIV2Service) ListMemoRelations(ctx context.Context, request *apiv2pb.ListMemoRelationsRequest) (*apiv2pb.ListMemoRelationsResponse, error) { + relationList := []*apiv2pb.MemoRelation{} + tempList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ + MemoID: &request.Id, + }) + if err != nil { + return nil, err + } + for _, relation := range tempList { + relationList = append(relationList, convertMemoRelationFromStore(relation)) + } + tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ + RelatedMemoID: &request.Id, + }) + if err != nil { + return nil, err + } + for _, relation := range tempList { + relationList = append(relationList, convertMemoRelationFromStore(relation)) + } + + response := &apiv2pb.ListMemoRelationsResponse{ + Relations: relationList, + } + return response, nil +} + +func convertMemoRelationFromStore(memoRelation *store.MemoRelation) *apiv2pb.MemoRelation { + return &apiv2pb.MemoRelation{ + MemoId: memoRelation.MemoID, + RelatedMemoId: memoRelation.RelatedMemoID, + Type: convertMemoRelationTypeFromStore(memoRelation.Type), + } +} + +func convertMemoRelationTypeFromStore(relationType store.MemoRelationType) apiv2pb.MemoRelation_Type { + switch relationType { + case store.MemoRelationReference: + return apiv2pb.MemoRelation_REFERENCE + case store.MemoRelationComment: + return apiv2pb.MemoRelation_COMMENT + default: + return apiv2pb.MemoRelation_TYPE_UNSPECIFIED + } +} + +func convertMemoRelationTypeToStore(relationType apiv2pb.MemoRelation_Type) store.MemoRelationType { + switch relationType { + case apiv2pb.MemoRelation_REFERENCE: + return store.MemoRelationReference + case apiv2pb.MemoRelation_COMMENT: + return store.MemoRelationComment + default: + return store.MemoRelationReference + } +} diff --git a/server/route/api/v2/memo_resource_service.go b/server/route/api/v2/memo_resource_service.go new file mode 100644 index 0000000000000..9071d115d7f48 --- /dev/null +++ b/server/route/api/v2/memo_resource_service.go @@ -0,0 +1,73 @@ +package v2 + +import ( + "context" + "slices" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" + "github.com/usememos/memos/store" +) + +func (s *APIV2Service) SetMemoResources(ctx context.Context, request *apiv2pb.SetMemoResourcesRequest) (*apiv2pb.SetMemoResourcesResponse, error) { + resources, err := s.Store.ListResources(ctx, &store.FindResource{ + MemoID: &request.Id, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list resources") + } + + // Delete resources that are not in the request. + for _, resource := range resources { + found := false + for _, requestResource := range request.Resources { + if resource.ID == int32(requestResource.Id) { + found = true + break + } + } + if !found { + if err = s.Store.DeleteResource(ctx, &store.DeleteResource{ + ID: int32(resource.ID), + MemoID: &request.Id, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete resource") + } + } + } + + slices.Reverse(request.Resources) + // Update resources' memo_id in the request. + for index, resource := range request.Resources { + updatedTs := time.Now().Unix() + int64(index) + if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{ + ID: resource.Id, + MemoID: &request.Id, + UpdatedTs: &updatedTs, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to update resource: %v", err) + } + } + + return &apiv2pb.SetMemoResourcesResponse{}, nil +} + +func (s *APIV2Service) ListMemoResources(ctx context.Context, request *apiv2pb.ListMemoResourcesRequest) (*apiv2pb.ListMemoResourcesResponse, error) { + resources, err := s.Store.ListResources(ctx, &store.FindResource{ + MemoID: &request.Id, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list resources") + } + + response := &apiv2pb.ListMemoResourcesResponse{ + Resources: []*apiv2pb.Resource{}, + } + for _, resource := range resources { + response.Resources = append(response.Resources, s.convertResourceFromStore(ctx, resource)) + } + return response, nil +} diff --git a/server/route/api/v2/memo_service.go b/server/route/api/v2/memo_service.go new file mode 100644 index 0000000000000..8eb732fc694c5 --- /dev/null +++ b/server/route/api/v2/memo_service.go @@ -0,0 +1,855 @@ +package v2 + +import ( + "archive/zip" + "bytes" + "context" + "encoding/json" + "fmt" + "log/slog" + "time" + + "github.com/google/cel-go/cel" + "github.com/lithammer/shortuuid/v4" + "github.com/pkg/errors" + expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/usememos/memos/internal/util" + "github.com/usememos/memos/plugin/webhook" + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" + storepb "github.com/usememos/memos/proto/gen/store" + apiv1 "github.com/usememos/memos/server/route/api/v1" + "github.com/usememos/memos/store" +) + +const ( + DefaultPageSize = 10 + MaxContentLength = 8 * 1024 + ChunkSize = 64 * 1024 // 64 KiB +) + +func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMemoRequest) (*apiv2pb.CreateMemoResponse, error) { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if len(request.Content) > MaxContentLength { + return nil, status.Errorf(codes.InvalidArgument, "content too long") + } + + create := &store.Memo{ + ResourceName: shortuuid.New(), + CreatorID: user.ID, + Content: request.Content, + Visibility: convertVisibilityToStore(request.Visibility), + } + // Find disable public memos system setting. + disablePublicMemosSystem, err := s.getDisablePublicMemosSystemSettingValue(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get system setting") + } + if disablePublicMemosSystem && create.Visibility == store.Public { + return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled") + } + + memo, err := s.Store.CreateMemo(ctx, create) + if err != nil { + return nil, err + } + + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + // Try to dispatch webhook when memo is created. + if err := s.DispatchMemoCreatedWebhook(ctx, memoMessage); err != nil { + slog.Warn("Failed to dispatch memo created webhook", err) + } + + response := &apiv2pb.CreateMemoResponse{ + Memo: memoMessage, + } + return response, nil +} + +func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemosRequest) (*apiv2pb.ListMemosResponse, error) { + memoFind := &store.FindMemo{ + // Exclude comments by default. + ExcludeComments: true, + } + if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter") + } + + var limit, offset int + if request.PageToken != "" { + var pageToken apiv2pb.PageToken + if err := unmarshalPageToken(request.PageToken, &pageToken); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err) + } + limit = int(pageToken.Limit) + offset = int(pageToken.Offset) + } else { + limit = int(request.PageSize) + } + if limit <= 0 { + limit = DefaultPageSize + } + limitPlusOne := limit + 1 + memoFind.Limit = &limitPlusOne + memoFind.Offset = &offset + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos") + } + + memoMessages := []*apiv2pb.Memo{} + nextPageToken := "" + if len(memos) == limitPlusOne { + memos = memos[:limit] + nextPageToken, err = getPageToken(limit, offset+limit) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get next page token, error: %v", err) + } + } + for _, memo := range memos { + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + memoMessages = append(memoMessages, memoMessage) + } + + response := &apiv2pb.ListMemosResponse{ + Memos: memoMessages, + NextPageToken: nextPageToken, + } + return response, nil +} + +func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv2pb.GetMemoRequest) (*apiv2pb.GetMemoResponse, error) { + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: &request.Id, + }) + if err != nil { + return nil, err + } + if memo == nil { + return nil, status.Errorf(codes.NotFound, "memo not found") + } + if memo.Visibility != store.Public { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if memo.Visibility == store.Private && memo.CreatorID != user.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + } + + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + response := &apiv2pb.GetMemoResponse{ + Memo: memoMessage, + } + return response, nil +} + +func (s *APIV2Service) GetMemoByName(ctx context.Context, request *apiv2pb.GetMemoByNameRequest) (*apiv2pb.GetMemoByNameResponse, error) { + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ResourceName: &request.Name, + }) + if err != nil { + return nil, err + } + if memo == nil { + return nil, status.Errorf(codes.NotFound, "memo not found") + } + if memo.Visibility != store.Public { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if memo.Visibility == store.Private && memo.CreatorID != user.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + } + + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + response := &apiv2pb.GetMemoByNameResponse{ + Memo: memoMessage, + } + return response, nil +} + +func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMemoRequest) (*apiv2pb.UpdateMemoResponse, error) { + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update mask is required") + } + + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: &request.Memo.Id, + }) + if err != nil { + return nil, err + } + if memo == nil { + return nil, status.Errorf(codes.NotFound, "memo not found") + } + + user, _ := getCurrentUser(ctx, s.Store) + if memo.CreatorID != user.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + currentTs := time.Now().Unix() + update := &store.UpdateMemo{ + ID: request.Memo.Id, + UpdatedTs: ¤tTs, + } + for _, path := range request.UpdateMask.Paths { + if path == "content" { + update.Content = &request.Memo.Content + } else if path == "resource_name" { + update.ResourceName = &request.Memo.Name + if !util.ResourceNameMatcher.MatchString(*update.ResourceName) { + return nil, status.Errorf(codes.InvalidArgument, "invalid resource name") + } + } else if path == "visibility" { + visibility := convertVisibilityToStore(request.Memo.Visibility) + // Find disable public memos system setting. + disablePublicMemosSystem, err := s.getDisablePublicMemosSystemSettingValue(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get system setting") + } + if disablePublicMemosSystem && visibility == store.Public { + return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled") + } + update.Visibility = &visibility + } else if path == "row_status" { + rowStatus := convertRowStatusToStore(request.Memo.RowStatus) + update.RowStatus = &rowStatus + } else if path == "created_ts" { + createdTs := request.Memo.CreateTime.AsTime().Unix() + update.CreatedTs = &createdTs + } else if path == "pinned" { + if _, err := s.Store.UpsertMemoOrganizer(ctx, &store.MemoOrganizer{ + MemoID: request.Memo.Id, + UserID: user.ID, + Pinned: request.Memo.Pinned, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert memo organizer") + } + } + } + if update.Content != nil && len(*update.Content) > MaxContentLength { + return nil, status.Errorf(codes.InvalidArgument, "content too long") + } + + if err = s.Store.UpdateMemo(ctx, update); err != nil { + return nil, status.Errorf(codes.Internal, "failed to update memo") + } + + memo, err = s.Store.GetMemo(ctx, &store.FindMemo{ + ID: &request.Memo.Id, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get memo") + } + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + // Try to dispatch webhook when memo is updated. + if err := s.DispatchMemoUpdatedWebhook(ctx, memoMessage); err != nil { + slog.Warn("Failed to dispatch memo updated webhook", err) + } + + return &apiv2pb.UpdateMemoResponse{ + Memo: memoMessage, + }, nil +} + +func (s *APIV2Service) DeleteMemo(ctx context.Context, request *apiv2pb.DeleteMemoRequest) (*apiv2pb.DeleteMemoResponse, error) { + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: &request.Id, + }) + if err != nil { + return nil, err + } + if memo == nil { + return nil, status.Errorf(codes.NotFound, "memo not found") + } + + user, _ := getCurrentUser(ctx, s.Store) + if memo.CreatorID != user.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil { + // Try to dispatch webhook when memo is deleted. + if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil { + slog.Warn("Failed to dispatch memo deleted webhook", err) + } + } + + if err = s.Store.DeleteMemo(ctx, &store.DeleteMemo{ + ID: request.Id, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete memo") + } + + return &apiv2pb.DeleteMemoResponse{}, nil +} + +func (s *APIV2Service) CreateMemoComment(ctx context.Context, request *apiv2pb.CreateMemoCommentRequest) (*apiv2pb.CreateMemoCommentResponse, error) { + relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &request.Id}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo") + } + + // Create the comment memo first. + createMemoResponse, err := s.CreateMemo(ctx, request.Create) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create memo") + } + + // Build the relation between the comment memo and the original memo. + memo := createMemoResponse.Memo + _, err = s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{ + MemoID: memo.Id, + RelatedMemoID: request.Id, + Type: store.MemoRelationComment, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create memo relation") + } + if memo.Visibility != apiv2pb.Visibility_PRIVATE && memo.CreatorId != relatedMemo.CreatorID { + activity, err := s.Store.CreateActivity(ctx, &store.Activity{ + CreatorID: memo.CreatorId, + Type: store.ActivityTypeMemoComment, + Level: store.ActivityLevelInfo, + Payload: &storepb.ActivityPayload{ + MemoComment: &storepb.ActivityMemoCommentPayload{ + MemoId: memo.Id, + RelatedMemoId: request.Id, + }, + }, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create activity") + } + if _, err := s.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: memo.CreatorId, + ReceiverID: relatedMemo.CreatorID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_TYPE_MEMO_COMMENT, + ActivityId: &activity.ID, + }, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to create inbox") + } + } + + response := &apiv2pb.CreateMemoCommentResponse{ + Memo: memo, + } + return response, nil +} + +func (s *APIV2Service) ListMemoComments(ctx context.Context, request *apiv2pb.ListMemoCommentsRequest) (*apiv2pb.ListMemoCommentsResponse, error) { + memoRelationComment := store.MemoRelationComment + memoRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ + RelatedMemoID: &request.Id, + Type: &memoRelationComment, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memo relations") + } + + var memos []*apiv2pb.Memo + for _, memoRelation := range memoRelations { + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: &memoRelation.MemoID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo") + } + if memo != nil { + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + memos = append(memos, memoMessage) + } + } + + response := &apiv2pb.ListMemoCommentsResponse{ + Memos: memos, + } + return response, nil +} + +func (s *APIV2Service) GetUserMemosStats(ctx context.Context, request *apiv2pb.GetUserMemosStatsRequest) (*apiv2pb.GetUserMemosStatsResponse, error) { + username, err := ExtractUsernameFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid username") + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &username, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + + normalRowStatus := store.Normal + memoFind := &store.FindMemo{ + CreatorID: &user.ID, + RowStatus: &normalRowStatus, + ExcludeComments: true, + ExcludeContent: true, + } + if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter") + } + + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos") + } + + location, err := time.LoadLocation(request.Timezone) + if err != nil { + return nil, status.Errorf(codes.Internal, "invalid timezone location") + } + + displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") + } + stats := make(map[string]int32) + for _, memo := range memos { + displayTs := memo.CreatedTs + if displayWithUpdatedTs { + displayTs = memo.UpdatedTs + } + stats[time.Unix(displayTs, 0).In(location).Format("2006-01-02")]++ + } + + response := &apiv2pb.GetUserMemosStatsResponse{ + Stats: stats, + } + return response, nil +} + +func (s *APIV2Service) ExportMemos(ctx context.Context, request *apiv2pb.ExportMemosRequest) (*apiv2pb.ExportMemosResponse, error) { + normalRowStatus := store.Normal + memoFind := &store.FindMemo{ + RowStatus: &normalRowStatus, + // Exclude comments by default. + ExcludeComments: true, + } + if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil { + return nil, status.Errorf(codes.Internal, "failed to build find memos with filter") + } + + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos") + } + + buf := new(bytes.Buffer) + writer := zip.NewWriter(buf) + for _, memo := range memos { + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + file, err := writer.Create(time.Unix(memo.CreatedTs, 0).Format(time.RFC3339) + ".md") + if err != nil { + return nil, status.Errorf(codes.Internal, "Failed to create memo file") + } + _, err = file.Write([]byte(memoMessage.Content)) + if err != nil { + return nil, status.Errorf(codes.Internal, "Failed to write to memo file") + } + } + if err := writer.Close(); err != nil { + return nil, status.Errorf(codes.Internal, "Failed to close zip file writer") + } + + return &apiv2pb.ExportMemosResponse{ + Content: buf.Bytes(), + }, nil +} + +func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*apiv2pb.Memo, error) { + displayTs := memo.CreatedTs + if displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx); err == nil && displayWithUpdatedTs { + displayTs = memo.UpdatedTs + } + + creator, err := s.Store.GetUser(ctx, &store.FindUser{ID: &memo.CreatorID}) + if err != nil { + return nil, errors.Wrap(err, "failed to get creator") + } + + listMemoRelationsResponse, err := s.ListMemoRelations(ctx, &apiv2pb.ListMemoRelationsRequest{Id: memo.ID}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo relations") + } + + listMemoResourcesResponse, err := s.ListMemoResources(ctx, &apiv2pb.ListMemoResourcesRequest{Id: memo.ID}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo resources") + } + + listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &apiv2pb.ListMemoReactionsRequest{Id: memo.ID}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo reactions") + } + + return &apiv2pb.Memo{ + Id: int32(memo.ID), + Name: memo.ResourceName, + RowStatus: convertRowStatusFromStore(memo.RowStatus), + Creator: fmt.Sprintf("%s%s", UserNamePrefix, creator.Username), + CreatorId: int32(memo.CreatorID), + CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)), + UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)), + DisplayTime: timestamppb.New(time.Unix(displayTs, 0)), + Content: memo.Content, + Visibility: convertVisibilityFromStore(memo.Visibility), + Pinned: memo.Pinned, + ParentId: memo.ParentID, + Relations: listMemoRelationsResponse.Relations, + Resources: listMemoResourcesResponse.Resources, + Reactions: listMemoReactionsResponse.Reactions, + }, nil +} + +func (s *APIV2Service) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) { + memoDisplayWithUpdatedTsSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ + Name: apiv1.SystemSettingMemoDisplayWithUpdatedTsName.String(), + }) + if err != nil { + return false, errors.Wrap(err, "failed to find system setting") + } + if memoDisplayWithUpdatedTsSetting == nil { + return false, nil + } + + memoDisplayWithUpdatedTs := false + if err := json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs); err != nil { + return false, errors.Wrap(err, "failed to unmarshal system setting value") + } + return memoDisplayWithUpdatedTs, nil +} + +func (s *APIV2Service) getDisablePublicMemosSystemSettingValue(ctx context.Context) (bool, error) { + disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ + Name: apiv1.SystemSettingDisablePublicMemosName.String(), + }) + if err != nil { + return false, errors.Wrap(err, "failed to find system setting") + } + if disablePublicMemosSystemSetting == nil { + return false, nil + } + + disablePublicMemos := false + if err := json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos); err != nil { + return false, errors.Wrap(err, "failed to unmarshal system setting value") + } + return disablePublicMemos, nil +} + +func convertVisibilityFromStore(visibility store.Visibility) apiv2pb.Visibility { + switch visibility { + case store.Private: + return apiv2pb.Visibility_PRIVATE + case store.Protected: + return apiv2pb.Visibility_PROTECTED + case store.Public: + return apiv2pb.Visibility_PUBLIC + default: + return apiv2pb.Visibility_VISIBILITY_UNSPECIFIED + } +} + +func convertVisibilityToStore(visibility apiv2pb.Visibility) store.Visibility { + switch visibility { + case apiv2pb.Visibility_PRIVATE: + return store.Private + case apiv2pb.Visibility_PROTECTED: + return store.Protected + case apiv2pb.Visibility_PUBLIC: + return store.Public + default: + return store.Private + } +} + +func (s *APIV2Service) buildMemoFindWithFilter(ctx context.Context, find *store.FindMemo, filter string) error { + user, _ := getCurrentUser(ctx, s.Store) + if find == nil { + find = &store.FindMemo{} + } + if filter != "" { + filter, err := parseListMemosFilter(filter) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) + } + if len(filter.ContentSearch) > 0 { + find.ContentSearch = filter.ContentSearch + } + if len(filter.Visibilities) > 0 { + find.VisibilityList = filter.Visibilities + } + if filter.OrderByPinned { + find.OrderByPinned = filter.OrderByPinned + } + if filter.DisplayTimeAfter != nil { + displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) + if err != nil { + return status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") + } + if displayWithUpdatedTs { + find.UpdatedTsAfter = filter.DisplayTimeAfter + } else { + find.CreatedTsAfter = filter.DisplayTimeAfter + } + } + if filter.DisplayTimeBefore != nil { + displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) + if err != nil { + return status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") + } + if displayWithUpdatedTs { + find.UpdatedTsBefore = filter.DisplayTimeBefore + } else { + find.CreatedTsBefore = filter.DisplayTimeBefore + } + } + if filter.Creator != nil { + username, err := ExtractUsernameFromName(*filter.Creator) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid creator name") + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &username, + }) + if err != nil { + return status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return status.Errorf(codes.NotFound, "user not found") + } + find.CreatorID = &user.ID + } + if filter.RowStatus != nil { + find.RowStatus = filter.RowStatus + } + } + + // If the user is not authenticated, only public memos are visible. + if user == nil { + if filter == "" { + // If no filter is provided, return an error. + return status.Errorf(codes.InvalidArgument, "filter is required") + } + + find.VisibilityList = []store.Visibility{store.Public} + } else if find.CreatorID != nil && *find.CreatorID != user.ID { + find.VisibilityList = []store.Visibility{store.Public, store.Protected} + } + + displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) + if err != nil { + return status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") + } + if displayWithUpdatedTs { + find.OrderByUpdatedTs = true + } + return nil +} + +// ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter. +var ListMemosFilterCELAttributes = []cel.EnvOption{ + cel.Variable("content_search", cel.ListType(cel.StringType)), + cel.Variable("visibilities", cel.ListType(cel.StringType)), + cel.Variable("order_by_pinned", cel.BoolType), + cel.Variable("display_time_before", cel.IntType), + cel.Variable("display_time_after", cel.IntType), + cel.Variable("creator", cel.StringType), + cel.Variable("row_status", cel.StringType), +} + +type ListMemosFilter struct { + ContentSearch []string + Visibilities []store.Visibility + OrderByPinned bool + DisplayTimeBefore *int64 + DisplayTimeAfter *int64 + Creator *string + RowStatus *store.RowStatus +} + +func parseListMemosFilter(expression string) (*ListMemosFilter, error) { + e, err := cel.NewEnv(ListMemosFilterCELAttributes...) + if err != nil { + return nil, err + } + ast, issues := e.Compile(expression) + if issues != nil { + return nil, errors.Errorf("found issue %v", issues) + } + filter := &ListMemosFilter{} + expr, err := cel.AstToParsedExpr(ast) + if err != nil { + return nil, err + } + callExpr := expr.GetExpr().GetCallExpr() + findField(callExpr, filter) + return filter, nil +} + +func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) { + if len(callExpr.Args) == 2 { + idExpr := callExpr.Args[0].GetIdentExpr() + if idExpr != nil { + if idExpr.Name == "content_search" { + contentSearch := []string{} + for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { + value := expr.GetConstExpr().GetStringValue() + contentSearch = append(contentSearch, value) + } + filter.ContentSearch = contentSearch + } else if idExpr.Name == "visibilities" { + visibilities := []store.Visibility{} + for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { + value := expr.GetConstExpr().GetStringValue() + visibilities = append(visibilities, store.Visibility(value)) + } + filter.Visibilities = visibilities + } else if idExpr.Name == "order_by_pinned" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.OrderByPinned = value + } else if idExpr.Name == "display_time_before" { + displayTimeBefore := callExpr.Args[1].GetConstExpr().GetInt64Value() + filter.DisplayTimeBefore = &displayTimeBefore + } else if idExpr.Name == "display_time_after" { + displayTimeAfter := callExpr.Args[1].GetConstExpr().GetInt64Value() + filter.DisplayTimeAfter = &displayTimeAfter + } else if idExpr.Name == "creator" { + creator := callExpr.Args[1].GetConstExpr().GetStringValue() + filter.Creator = &creator + } else if idExpr.Name == "row_status" { + rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue()) + filter.RowStatus = &rowStatus + } + return + } + } + for _, arg := range callExpr.Args { + callExpr := arg.GetCallExpr() + if callExpr != nil { + findField(callExpr, filter) + } + } +} + +// DispatchMemoCreatedWebhook dispatches webhook when memo is created. +func (s *APIV2Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *apiv2pb.Memo) error { + return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created") +} + +// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated. +func (s *APIV2Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *apiv2pb.Memo) error { + return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated") +} + +// DispatchMemoDeletedWebhook dispatches webhook when memo is deleted. +func (s *APIV2Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *apiv2pb.Memo) error { + return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted") +} + +func (s *APIV2Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *apiv2pb.Memo, activityType string) error { + webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{ + CreatorID: &memo.CreatorId, + }) + if err != nil { + return err + } + for _, hook := range webhooks { + payload := convertMemoToWebhookPayload(memo) + payload.ActivityType = activityType + payload.URL = hook.Url + err := webhook.Post(*payload) + if err != nil { + return errors.Wrap(err, "failed to post webhook") + } + } + return nil +} + +func convertMemoToWebhookPayload(memo *apiv2pb.Memo) *webhook.WebhookPayload { + return &webhook.WebhookPayload{ + CreatorID: memo.CreatorId, + CreatedTs: time.Now().Unix(), + Memo: &webhook.Memo{ + ID: memo.Id, + CreatorID: memo.CreatorId, + CreatedTs: memo.CreateTime.Seconds, + UpdatedTs: memo.UpdateTime.Seconds, + Content: memo.Content, + Visibility: memo.Visibility.String(), + Pinned: memo.Pinned, + ResourceList: func() []*webhook.Resource { + resources := []*webhook.Resource{} + for _, resource := range memo.Resources { + resources = append(resources, &webhook.Resource{ + ID: resource.Id, + Filename: resource.Filename, + ExternalLink: resource.ExternalLink, + Type: resource.Type, + Size: resource.Size, + }) + } + return resources + }(), + RelationList: func() []*webhook.MemoRelation { + relations := []*webhook.MemoRelation{} + for _, relation := range memo.Relations { + relations = append(relations, &webhook.MemoRelation{ + MemoID: relation.MemoId, + RelatedMemoID: relation.RelatedMemoId, + Type: relation.Type.String(), + }) + } + return relations + }(), + }, + } +} diff --git a/server/route/api/v2/reaction_service.go b/server/route/api/v2/reaction_service.go new file mode 100644 index 0000000000000..8a507bc2d2d84 --- /dev/null +++ b/server/route/api/v2/reaction_service.go @@ -0,0 +1,83 @@ +package v2 + +import ( + "context" + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (s *APIV2Service) ListMemoReactions(ctx context.Context, request *apiv2pb.ListMemoReactionsRequest) (*apiv2pb.ListMemoReactionsResponse, error) { + contentID := fmt.Sprintf("memos/%d", request.Id) + reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ + ContentID: &contentID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list reactions") + } + + response := &apiv2pb.ListMemoReactionsResponse{ + Reactions: []*apiv2pb.Reaction{}, + } + for _, reaction := range reactions { + reactionMessage, err := s.convertReactionFromStore(ctx, reaction) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert reaction") + } + response.Reactions = append(response.Reactions, reactionMessage) + } + return response, nil +} + +func (s *APIV2Service) UpsertMemoReaction(ctx context.Context, request *apiv2pb.UpsertMemoReactionRequest) (*apiv2pb.UpsertMemoReactionResponse, error) { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + reaction, err := s.Store.UpsertReaction(ctx, &storepb.Reaction{ + CreatorId: user.ID, + ContentId: request.Reaction.ContentId, + ReactionType: storepb.Reaction_Type(request.Reaction.ReactionType), + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert reaction") + } + + reactionMessage, err := s.convertReactionFromStore(ctx, reaction) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert reaction") + } + return &apiv2pb.UpsertMemoReactionResponse{ + Reaction: reactionMessage, + }, nil +} + +func (s *APIV2Service) DeleteMemoReaction(ctx context.Context, request *apiv2pb.DeleteMemoReactionRequest) (*apiv2pb.DeleteMemoReactionResponse, error) { + if err := s.Store.DeleteReaction(ctx, &store.DeleteReaction{ + ID: request.Id, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete reaction") + } + + return &apiv2pb.DeleteMemoReactionResponse{}, nil +} + +func (s *APIV2Service) convertReactionFromStore(ctx context.Context, reaction *storepb.Reaction) (*apiv2pb.Reaction, error) { + creator, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &reaction.CreatorId, + }) + if err != nil { + return nil, err + } + return &apiv2pb.Reaction{ + Id: reaction.Id, + Creator: fmt.Sprintf("%s%s", UserNamePrefix, creator.Username), + ContentId: reaction.ContentId, + ReactionType: apiv2pb.Reaction_Type(reaction.ReactionType), + }, nil +} diff --git a/api/v2/resource_name.go b/server/route/api/v2/resource_name.go similarity index 59% rename from api/v2/resource_name.go rename to server/route/api/v2/resource_name.go index a12231f58bb7d..a2987dfbd3bbb 100644 --- a/api/v2/resource_name.go +++ b/server/route/api/v2/resource_name.go @@ -10,7 +10,9 @@ import ( ) const ( - InboxNamePrefix = "inboxes/" + WorkspaceSettingNamePrefix = "settings/" + UserNamePrefix = "users/" + InboxNamePrefix = "inboxes/" ) // GetNameParentTokens returns the tokens from a resource name. @@ -33,8 +35,25 @@ func GetNameParentTokens(name string, tokenPrefixes ...string) ([]string, error) return tokens, nil } -// GetInboxID returns the inbox ID from a resource name. -func GetInboxID(name string) (int32, error) { +func ExtractWorkspaceSettingKeyFromName(name string) (string, error) { + tokens, err := GetNameParentTokens(name, WorkspaceSettingNamePrefix) + if err != nil { + return "", err + } + return tokens[0], nil +} + +// ExtractUsernameFromName returns the username from a resource name. +func ExtractUsernameFromName(name string) (string, error) { + tokens, err := GetNameParentTokens(name, UserNamePrefix) + if err != nil { + return "", err + } + return tokens[0], nil +} + +// ExtractInboxIDFromName returns the inbox ID from a resource name. +func ExtractInboxIDFromName(name string) (int32, error) { tokens, err := GetNameParentTokens(name, InboxNamePrefix) if err != nil { return 0, err diff --git a/api/v2/resource_service.go b/server/route/api/v2/resource_service.go similarity index 56% rename from api/v2/resource_service.go rename to server/route/api/v2/resource_service.go index 2ad7ad0e522d6..88c25690f59db 100644 --- a/api/v2/resource_service.go +++ b/server/route/api/v2/resource_service.go @@ -2,8 +2,10 @@ package v2 import ( "context" + "net/url" "time" + "github.com/lithammer/shortuuid/v4" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" @@ -12,6 +14,42 @@ import ( "github.com/usememos/memos/store" ) +func (s *APIV2Service) CreateResource(ctx context.Context, request *apiv2pb.CreateResourceRequest) (*apiv2pb.CreateResourceResponse, error) { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if request.ExternalLink != "" { + // Only allow those external links scheme with http/https + linkURL, err := url.Parse(request.ExternalLink) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid external link: %v", err) + } + if linkURL.Scheme != "http" && linkURL.Scheme != "https" { + return nil, status.Errorf(codes.InvalidArgument, "invalid external link scheme: %v", linkURL.Scheme) + } + } + + create := &store.Resource{ + ResourceName: shortuuid.New(), + CreatorID: user.ID, + Filename: request.Filename, + ExternalLink: request.ExternalLink, + Type: request.Type, + } + if request.MemoId != nil { + create.MemoID = request.MemoId + } + resource, err := s.Store.CreateResource(ctx, create) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create resource: %v", err) + } + + return &apiv2pb.CreateResourceResponse{ + Resource: s.convertResourceFromStore(ctx, resource), + }, nil +} + func (s *APIV2Service) ListResources(ctx context.Context, _ *apiv2pb.ListResourcesRequest) (*apiv2pb.ListResourcesResponse, error) { user, err := getCurrentUser(ctx, s.Store) if err != nil { @@ -31,6 +69,38 @@ func (s *APIV2Service) ListResources(ctx context.Context, _ *apiv2pb.ListResourc return response, nil } +func (s *APIV2Service) GetResource(ctx context.Context, request *apiv2pb.GetResourceRequest) (*apiv2pb.GetResourceResponse, error) { + resource, err := s.Store.GetResource(ctx, &store.FindResource{ + ID: &request.Id, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err) + } + if resource == nil { + return nil, status.Errorf(codes.NotFound, "resource not found") + } + + return &apiv2pb.GetResourceResponse{ + Resource: s.convertResourceFromStore(ctx, resource), + }, nil +} + +func (s *APIV2Service) GetResourceByName(ctx context.Context, request *apiv2pb.GetResourceByNameRequest) (*apiv2pb.GetResourceByNameResponse, error) { + resource, err := s.Store.GetResource(ctx, &store.FindResource{ + ResourceName: &request.Name, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err) + } + if resource == nil { + return nil, status.Errorf(codes.NotFound, "resource not found") + } + + return &apiv2pb.GetResourceByNameResponse{ + Resource: s.convertResourceFromStore(ctx, resource), + }, nil +} + func (s *APIV2Service) UpdateResource(ctx context.Context, request *apiv2pb.UpdateResourceRequest) (*apiv2pb.UpdateResourceResponse, error) { if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { return nil, status.Errorf(codes.InvalidArgument, "update mask is required") @@ -95,7 +165,8 @@ func (s *APIV2Service) convertResourceFromStore(ctx context.Context, resource *s return &apiv2pb.Resource{ Id: resource.ID, - CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)), + Name: resource.ResourceName, + CreateTime: timestamppb.New(time.Unix(resource.CreatedTs, 0)), Filename: resource.Filename, ExternalLink: resource.ExternalLink, Type: resource.Type, diff --git a/server/route/api/v2/tag_service.go b/server/route/api/v2/tag_service.go new file mode 100644 index 0000000000000..3e888a86a74c0 --- /dev/null +++ b/server/route/api/v2/tag_service.go @@ -0,0 +1,271 @@ +package v2 + +import ( + "context" + "fmt" + "slices" + "sort" + + "github.com/pkg/errors" + "github.com/yourselfhosted/gomark/ast" + "github.com/yourselfhosted/gomark/parser" + "github.com/yourselfhosted/gomark/parser/tokenizer" + "github.com/yourselfhosted/gomark/restore" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" + "github.com/usememos/memos/store" +) + +func (s *APIV2Service) UpsertTag(ctx context.Context, request *apiv2pb.UpsertTagRequest) (*apiv2pb.UpsertTagResponse, error) { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + + tag, err := s.Store.UpsertTag(ctx, &store.Tag{ + Name: request.Name, + CreatorID: user.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err) + } + + t, err := s.convertTagFromStore(ctx, tag) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err) + } + return &apiv2pb.UpsertTagResponse{ + Tag: t, + }, nil +} + +func (s *APIV2Service) BatchUpsertTag(ctx context.Context, request *apiv2pb.BatchUpsertTagRequest) (*apiv2pb.BatchUpsertTagResponse, error) { + for _, r := range request.Requests { + if _, err := s.UpsertTag(ctx, r); err != nil { + return nil, status.Errorf(codes.Internal, "failed to batch upsert tags: %v", err) + } + } + return &apiv2pb.BatchUpsertTagResponse{}, nil +} + +func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRequest) (*apiv2pb.ListTagsResponse, error) { + username, err := ExtractUsernameFromName(request.User) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err) + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &username, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + tags, err := s.Store.ListTags(ctx, &store.FindTag{ + CreatorID: user.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err) + } + + response := &apiv2pb.ListTagsResponse{} + for _, tag := range tags { + t, err := s.convertTagFromStore(ctx, tag) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err) + } + response.Tags = append(response.Tags, t) + } + return response, nil +} + +func (s *APIV2Service) RenameTag(ctx context.Context, request *apiv2pb.RenameTagRequest) (*apiv2pb.RenameTagResponse, error) { + username, err := ExtractUsernameFromName(request.User) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err) + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &username, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + + // Find all related memos. + memos, err := s.Store.ListMemos(ctx, &store.FindMemo{ + CreatorID: &user.ID, + ContentSearch: []string{fmt.Sprintf("#%s", request.OldName)}, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) + } + // Replace tag name in memo content. + for _, memo := range memos { + nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to parse memo: %v", err) + } + TraverseASTNodes(nodes, func(node ast.Node) { + if tag, ok := node.(*ast.Tag); ok && tag.Content == request.OldName { + tag.Content = request.NewName + } + }) + content := restore.Restore(nodes) + if err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{ + ID: memo.ID, + Content: &content, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to update memo: %v", err) + } + } + + // Delete old tag and create new tag. + if err := s.Store.DeleteTag(ctx, &store.DeleteTag{ + CreatorID: user.ID, + Name: request.OldName, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err) + } + tag, err := s.Store.UpsertTag(ctx, &store.Tag{ + CreatorID: user.ID, + Name: request.NewName, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err) + } + + tagMessage, err := s.convertTagFromStore(ctx, tag) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err) + } + return &apiv2pb.RenameTagResponse{Tag: tagMessage}, nil +} + +func (s *APIV2Service) DeleteTag(ctx context.Context, request *apiv2pb.DeleteTagRequest) (*apiv2pb.DeleteTagResponse, error) { + username, err := ExtractUsernameFromName(request.Tag.Creator) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err) + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &username, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + if err := s.Store.DeleteTag(ctx, &store.DeleteTag{ + Name: request.Tag.Name, + CreatorID: user.ID, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err) + } + + return &apiv2pb.DeleteTagResponse{}, nil +} + +func (s *APIV2Service) GetTagSuggestions(ctx context.Context, request *apiv2pb.GetTagSuggestionsRequest) (*apiv2pb.GetTagSuggestionsResponse, error) { + username, err := ExtractUsernameFromName(request.User) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err) + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &username, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + normalRowStatus := store.Normal + memoFind := &store.FindMemo{ + CreatorID: &user.ID, + ContentSearch: []string{"#"}, + RowStatus: &normalRowStatus, + } + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) + } + + tagList, err := s.Store.ListTags(ctx, &store.FindTag{ + CreatorID: user.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err) + } + + tagNameList := []string{} + for _, tag := range tagList { + tagNameList = append(tagNameList, tag.Name) + } + tagMapSet := make(map[string]bool) + for _, memo := range memos { + nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse memo content") + } + + // Dynamically upsert tags from memo content. + TraverseASTNodes(nodes, func(node ast.Node) { + if tagNode, ok := node.(*ast.Tag); ok { + tag := tagNode.Content + if !slices.Contains(tagNameList, tag) { + tagMapSet[tag] = true + } + } + }) + } + suggestions := []string{} + for tag := range tagMapSet { + suggestions = append(suggestions, tag) + } + sort.Strings(suggestions) + + return &apiv2pb.GetTagSuggestionsResponse{ + Tags: suggestions, + }, nil +} + +func (s *APIV2Service) convertTagFromStore(ctx context.Context, tag *store.Tag) (*apiv2pb.Tag, error) { + user, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &tag.CreatorID, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get user") + } + return &apiv2pb.Tag{ + Name: tag.Name, + Creator: fmt.Sprintf("%s%s", UserNamePrefix, user.Username), + }, nil +} + +func TraverseASTNodes(nodes []ast.Node, fn func(ast.Node)) { + for _, node := range nodes { + fn(node) + switch n := node.(type) { + case *ast.Paragraph: + TraverseASTNodes(n.Children, fn) + case *ast.Heading: + TraverseASTNodes(n.Children, fn) + case *ast.Blockquote: + TraverseASTNodes(n.Children, fn) + case *ast.OrderedList: + TraverseASTNodes(n.Children, fn) + case *ast.UnorderedList: + TraverseASTNodes(n.Children, fn) + case *ast.TaskList: + TraverseASTNodes(n.Children, fn) + case *ast.Bold: + TraverseASTNodes(n.Children, fn) + } + } +} diff --git a/api/v2/user_service.go b/server/route/api/v2/user_service.go similarity index 57% rename from api/v2/user_service.go rename to server/route/api/v2/user_service.go index 7ec9ff26c4420..16f3732798a83 100644 --- a/api/v2/user_service.go +++ b/server/route/api/v2/user_service.go @@ -2,12 +2,12 @@ package v2 import ( "context" + "fmt" "net/http" - "regexp" "strings" "time" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" "github.com/labstack/echo/v4" "github.com/pkg/errors" "golang.org/x/crypto/bcrypt" @@ -16,19 +16,43 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/usememos/memos/api/auth" + "github.com/usememos/memos/internal/util" apiv2pb "github.com/usememos/memos/proto/gen/api/v2" storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/server/route/api/auth" "github.com/usememos/memos/store" ) -var ( - usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$") -) +func (s *APIV2Service) ListUsers(ctx context.Context, _ *apiv2pb.ListUsersRequest) (*apiv2pb.ListUsersResponse, error) { + currentUser, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + users, err := s.Store.ListUsers(ctx, &store.FindUser{}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list users: %v", err) + } + + response := &apiv2pb.ListUsersResponse{ + Users: []*apiv2pb.User{}, + } + for _, user := range users { + response.Users = append(response.Users, convertUserFromStore(user)) + } + return response, nil +} func (s *APIV2Service) GetUser(ctx context.Context, request *apiv2pb.GetUserRequest) (*apiv2pb.GetUserResponse, error) { + username, err := ExtractUsernameFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "name is required") + } user, err := s.Store.GetUser(ctx, &store.FindUser{ - Username: &request.Username, + Username: &username, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) @@ -53,8 +77,12 @@ func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUs return nil, status.Errorf(codes.PermissionDenied, "permission denied") } - if !usernameMatcher.MatchString(strings.ToLower(request.User.Username)) { - return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username) + username, err := ExtractUsernameFromName(request.User.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "name is required") + } + if !util.ResourceNameMatcher.MatchString(strings.ToLower(username)) { + return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", username) } passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost) if err != nil { @@ -62,7 +90,7 @@ func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUs } user, err := s.Store.CreateUser(ctx, &store.User{ - Username: request.User.Username, + Username: username, Role: convertUserRoleToStore(request.User.Role), Email: request.User.Email, Nickname: request.User.Nickname, @@ -79,25 +107,41 @@ func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUs } func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUserRequest) (*apiv2pb.UpdateUserResponse, error) { + username, err := ExtractUsernameFromName(request.User.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "name is required") + } currentUser, err := getCurrentUser(ctx, s.Store) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) } - if currentUser.Username != request.User.Username && currentUser.Role != store.RoleAdmin { + if currentUser.Username != username && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { return nil, status.Errorf(codes.InvalidArgument, "update mask is empty") } + user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + + if s.Profile.Mode == "demo" && user.Username == "memos-demo" { + return nil, status.Errorf(codes.PermissionDenied, "unauthorized to update user in demo mode") + } + currentTs := time.Now().Unix() update := &store.UpdateUser{ - ID: currentUser.ID, + ID: user.ID, UpdatedTs: ¤tTs, } for _, field := range request.UpdateMask.Paths { if field == "username" { - if !usernameMatcher.MatchString(strings.ToLower(request.User.Username)) { + if !util.ResourceNameMatcher.MatchString(strings.ToLower(request.User.Username)) { return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username) } update.Username = &request.User.Username @@ -125,17 +169,153 @@ func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUs } } - user, err := s.Store.UpdateUser(ctx, update) + updatedUser, err := s.Store.UpdateUser(ctx, update) if err != nil { return nil, status.Errorf(codes.Internal, "failed to update user: %v", err) } response := &apiv2pb.UpdateUserResponse{ - User: convertUserFromStore(user), + User: convertUserFromStore(updatedUser), } return response, nil } +func (s *APIV2Service) DeleteUser(ctx context.Context, request *apiv2pb.DeleteUserRequest) (*apiv2pb.DeleteUserResponse, error) { + username, err := ExtractUsernameFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "name is required") + } + currentUser, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser.Username != username && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + + if s.Profile.Mode == "demo" && user.Username == "memos-demo" { + return nil, status.Errorf(codes.PermissionDenied, "unauthorized to delete this user in demo mode") + } + + if err := s.Store.DeleteUser(ctx, &store.DeleteUser{ + ID: user.ID, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err) + } + + return &apiv2pb.DeleteUserResponse{}, nil +} + +func getDefaultUserSetting() *apiv2pb.UserSetting { + return &apiv2pb.UserSetting{ + Locale: "en", + Appearance: "system", + MemoVisibility: "PRIVATE", + } +} + +func (s *APIV2Service) GetUserSetting(ctx context.Context, _ *apiv2pb.GetUserSettingRequest) (*apiv2pb.GetUserSettingResponse, error) { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + + userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{ + UserID: &user.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list user settings: %v", err) + } + userSettingMessage := getDefaultUserSetting() + for _, setting := range userSettings { + if setting.Key == storepb.UserSettingKey_USER_SETTING_LOCALE { + userSettingMessage.Locale = setting.GetLocale() + } else if setting.Key == storepb.UserSettingKey_USER_SETTING_APPEARANCE { + userSettingMessage.Appearance = setting.GetAppearance() + } else if setting.Key == storepb.UserSettingKey_USER_SETTING_MEMO_VISIBILITY { + userSettingMessage.MemoVisibility = setting.GetMemoVisibility() + } else if setting.Key == storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID { + userSettingMessage.TelegramUserId = setting.GetTelegramUserId() + } + } + return &apiv2pb.GetUserSettingResponse{ + Setting: userSettingMessage, + }, nil +} + +func (s *APIV2Service) UpdateUserSetting(ctx context.Context, request *apiv2pb.UpdateUserSettingRequest) (*apiv2pb.UpdateUserSettingResponse, error) { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update mask is empty") + } + + for _, field := range request.UpdateMask.Paths { + if field == "locale" { + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: user.ID, + Key: storepb.UserSettingKey_USER_SETTING_LOCALE, + Value: &storepb.UserSetting_Locale{ + Locale: request.Setting.Locale, + }, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err) + } + } else if field == "appearance" { + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: user.ID, + Key: storepb.UserSettingKey_USER_SETTING_APPEARANCE, + Value: &storepb.UserSetting_Appearance{ + Appearance: request.Setting.Appearance, + }, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err) + } + } else if field == "memo_visibility" { + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: user.ID, + Key: storepb.UserSettingKey_USER_SETTING_MEMO_VISIBILITY, + Value: &storepb.UserSetting_MemoVisibility{ + MemoVisibility: request.Setting.MemoVisibility, + }, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err) + } + } else if field == "telegram_user_id" { + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: user.ID, + Key: storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID, + Value: &storepb.UserSetting_TelegramUserId{ + TelegramUserId: request.Setting.TelegramUserId, + }, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err) + } + } else { + return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field) + } + } + + userSettingResponse, err := s.GetUserSetting(ctx, &apiv2pb.GetUserSettingRequest{}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user setting: %v", err) + } + return &apiv2pb.UpdateUserSettingResponse{ + Setting: userSettingResponse.Setting, + }, nil +} + func (s *APIV2Service) ListUserAccessTokens(ctx context.Context, request *apiv2pb.ListUserAccessTokensRequest) (*apiv2pb.ListUserAccessTokensResponse, error) { user, err := getCurrentUser(ctx, s.Store) if err != nil { @@ -146,17 +326,21 @@ func (s *APIV2Service) ListUserAccessTokens(ctx context.Context, request *apiv2p } userID := user.ID + username, err := ExtractUsernameFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "name is required") + } // List access token for other users need to be verified. - if user.Username != request.Username { + if user.Username != username { // Normal users can only list their access tokens. if user.Role == store.RoleUser { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // The request user must be exist. - requestUser, err := s.Store.GetUser(ctx, &store.FindUser{Username: &request.Username}) + requestUser, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username}) if requestUser == nil || err != nil { - return nil, status.Errorf(codes.NotFound, "fail to find user %s", request.Username) + return nil, status.Errorf(codes.NotFound, "fail to find user %s", username) } userID = requestUser.ID } @@ -198,8 +382,14 @@ func (s *APIV2Service) ListUserAccessTokens(ctx context.Context, request *apiv2p // Sort by issued time in descending order. slices.SortFunc(accessTokens, func(i, j *apiv2pb.UserAccessToken) int { - return int(i.IssuedAt.Seconds - j.IssuedAt.Seconds) + return int(j.IssuedAt.Seconds - i.IssuedAt.Seconds) }) + + // limit only the first 200 access tokens. + if len(accessTokens) > 200 { + accessTokens = accessTokens[:200] + } + response := &apiv2pb.ListUserAccessTokensResponse{ AccessTokens: accessTokens, } @@ -217,21 +407,7 @@ func (s *APIV2Service) CreateUserAccessToken(ctx context.Context, request *apiv2 expiresAt = request.ExpiresAt.AsTime() } - // Create access token for other users need to be verified. - if user.Username != request.Username { - // Normal users can only create access tokens for others. - if user.Role == store.RoleUser { - return nil, status.Errorf(codes.PermissionDenied, "permission denied") - } - - // The request user must be exist. - requestUser, err := s.Store.GetUser(ctx, &store.FindUser{Username: &request.Username}) - if requestUser == nil || err != nil { - return nil, status.Errorf(codes.NotFound, "fail to find user %s", request.Username) - } - } - - accessToken, err := auth.GenerateAccessToken(request.Username, user.ID, expiresAt, []byte(s.Secret)) + accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, expiresAt, []byte(s.Secret)) if err != nil { return nil, status.Errorf(codes.Internal, "failed to generate access token: %v", err) } @@ -288,7 +464,7 @@ func (s *APIV2Service) DeleteUserAccessToken(ctx context.Context, request *apiv2 } updatedUserAccessTokens = append(updatedUserAccessTokens, userAccessToken) } - if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{ + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS, Value: &storepb.UserSetting_AccessTokens{ @@ -313,7 +489,7 @@ func (s *APIV2Service) UpsertAccessTokenToStore(ctx context.Context, user *store Description: description, } userAccessTokens = append(userAccessTokens, &userAccessToken) - if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{ + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ UserId: user.ID, Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS, Value: &storepb.UserSetting_AccessTokens{ @@ -329,12 +505,13 @@ func (s *APIV2Service) UpsertAccessTokenToStore(ctx context.Context, user *store func convertUserFromStore(user *store.User) *apiv2pb.User { return &apiv2pb.User{ - Id: int32(user.ID), + Name: fmt.Sprintf("%s%s", UserNamePrefix, user.Username), + Id: user.ID, RowStatus: convertRowStatusFromStore(user.RowStatus), CreateTime: timestamppb.New(time.Unix(user.CreatedTs, 0)), UpdateTime: timestamppb.New(time.Unix(user.UpdatedTs, 0)), - Username: user.Username, Role: convertUserRoleFromStore(user.Role), + Username: user.Username, Email: user.Email, Nickname: user.Nickname, AvatarUrl: user.AvatarURL, diff --git a/api/v2/v2.go b/server/route/api/v2/v2.go similarity index 68% rename from api/v2/v2.go rename to server/route/api/v2/v2.go index 5becfb2087045..ced8b2f2494b6 100644 --- a/api/v2/v2.go +++ b/server/route/api/v2/v2.go @@ -3,10 +3,13 @@ package v2 import ( "context" "fmt" + "log/slog" + "net" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/improbable-eng/grpc-web/go/grpcweb" "github.com/labstack/echo/v4" + "github.com/pkg/errors" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection" @@ -17,13 +20,17 @@ import ( ) type APIV2Service struct { - apiv2pb.UnimplementedSystemServiceServer + apiv2pb.UnimplementedWorkspaceServiceServer + apiv2pb.UnimplementedWorkspaceSettingServiceServer + apiv2pb.UnimplementedAuthServiceServer apiv2pb.UnimplementedUserServiceServer apiv2pb.UnimplementedMemoServiceServer apiv2pb.UnimplementedResourceServiceServer apiv2pb.UnimplementedTagServiceServer apiv2pb.UnimplementedInboxServiceServer apiv2pb.UnimplementedActivityServiceServer + apiv2pb.UnimplementedWebhookServiceServer + apiv2pb.UnimplementedLinkServiceServer Secret string Profile *profile.Profile @@ -38,6 +45,7 @@ func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store authProvider := NewGRPCAuthInterceptor(store, secret) grpcServer := grpc.NewServer( grpc.ChainUnaryInterceptor( + NewLoggerInterceptor().LoggerInterceptor, authProvider.AuthenticationInterceptor, ), ) @@ -49,13 +57,17 @@ func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store grpcServerPort: grpcServerPort, } - apiv2pb.RegisterSystemServiceServer(grpcServer, apiv2Service) + apiv2pb.RegisterWorkspaceServiceServer(grpcServer, apiv2Service) + apiv2pb.RegisterWorkspaceSettingServiceServer(grpcServer, apiv2Service) + apiv2pb.RegisterAuthServiceServer(grpcServer, apiv2Service) apiv2pb.RegisterUserServiceServer(grpcServer, apiv2Service) apiv2pb.RegisterMemoServiceServer(grpcServer, apiv2Service) apiv2pb.RegisterTagServiceServer(grpcServer, apiv2Service) apiv2pb.RegisterResourceServiceServer(grpcServer, apiv2Service) apiv2pb.RegisterInboxServiceServer(grpcServer, apiv2Service) apiv2pb.RegisterActivityServiceServer(grpcServer, apiv2Service) + apiv2pb.RegisterWebhookServiceServer(grpcServer, apiv2Service) + apiv2pb.RegisterLinkServiceServer(grpcServer, apiv2Service) reflection.Register(grpcServer) return apiv2Service @@ -79,7 +91,13 @@ func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error } gwMux := runtime.NewServeMux() - if err := apiv2pb.RegisterSystemServiceHandler(context.Background(), gwMux, conn); err != nil { + if err := apiv2pb.RegisterWorkspaceServiceHandler(context.Background(), gwMux, conn); err != nil { + return err + } + if err := apiv2pb.RegisterWorkspaceSettingServiceHandler(context.Background(), gwMux, conn); err != nil { + return err + } + if err := apiv2pb.RegisterAuthServiceHandler(context.Background(), gwMux, conn); err != nil { return err } if err := apiv2pb.RegisterUserServiceHandler(context.Background(), gwMux, conn); err != nil { @@ -100,6 +118,12 @@ func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error if err := apiv2pb.RegisterActivityServiceHandler(context.Background(), gwMux, conn); err != nil { return err } + if err := apiv2pb.RegisterWebhookServiceHandler(context.Background(), gwMux, conn); err != nil { + return err + } + if err := apiv2pb.RegisterLinkServiceHandler(context.Background(), gwMux, conn); err != nil { + return err + } e.Any("/api/v2/*", echo.WrapHandler(gwMux)) // GRPC web proxy. @@ -112,5 +136,16 @@ func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error wrappedGrpc := grpcweb.WrapServer(s.grpcServer, options...) e.Any("/memos.api.v2.*", echo.WrapHandler(wrappedGrpc)) + // Start gRPC server. + listen, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Profile.Addr, s.grpcServerPort)) + if err != nil { + return errors.Wrap(err, "failed to start gRPC server") + } + go func() { + if err := s.grpcServer.Serve(listen); err != nil { + slog.Error("failed to start gRPC server", err) + } + }() + return nil } diff --git a/server/route/api/v2/webhook_service.go b/server/route/api/v2/webhook_service.go new file mode 100644 index 0000000000000..3beb84068271b --- /dev/null +++ b/server/route/api/v2/webhook_service.go @@ -0,0 +1,120 @@ +package v2 + +import ( + "context" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (s *APIV2Service) CreateWebhook(ctx context.Context, request *apiv2pb.CreateWebhookRequest) (*apiv2pb.CreateWebhookResponse, error) { + currentUser, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + + webhook, err := s.Store.CreateWebhook(ctx, &storepb.Webhook{ + CreatorId: currentUser.ID, + Name: request.Name, + Url: request.Url, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create webhook, error: %+v", err) + } + return &apiv2pb.CreateWebhookResponse{ + Webhook: convertWebhookFromStore(webhook), + }, nil +} + +func (s *APIV2Service) ListWebhooks(ctx context.Context, request *apiv2pb.ListWebhooksRequest) (*apiv2pb.ListWebhooksResponse, error) { + webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{ + CreatorID: &request.CreatorId, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list webhooks, error: %+v", err) + } + + response := &apiv2pb.ListWebhooksResponse{ + Webhooks: []*apiv2pb.Webhook{}, + } + for _, webhook := range webhooks { + response.Webhooks = append(response.Webhooks, convertWebhookFromStore(webhook)) + } + return response, nil +} + +func (s *APIV2Service) GetWebhook(ctx context.Context, request *apiv2pb.GetWebhookRequest) (*apiv2pb.GetWebhookResponse, error) { + currentUser, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + + webhook, err := s.Store.GetWebhooks(ctx, &store.FindWebhook{ + ID: &request.Id, + CreatorID: ¤tUser.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get webhook, error: %+v", err) + } + if webhook == nil { + return nil, status.Errorf(codes.NotFound, "webhook not found") + } + return &apiv2pb.GetWebhookResponse{ + Webhook: convertWebhookFromStore(webhook), + }, nil +} + +func (s *APIV2Service) UpdateWebhook(ctx context.Context, request *apiv2pb.UpdateWebhookRequest) (*apiv2pb.UpdateWebhookResponse, error) { + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update_mask is required") + } + + update := &store.UpdateWebhook{} + for _, field := range request.UpdateMask.Paths { + switch field { + case "row_status": + rowStatus := storepb.RowStatus(storepb.RowStatus_value[request.Webhook.RowStatus.String()]) + update.RowStatus = &rowStatus + case "name": + update.Name = &request.Webhook.Name + case "url": + update.URL = &request.Webhook.Url + } + } + + webhook, err := s.Store.UpdateWebhook(ctx, update) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update webhook, error: %+v", err) + } + return &apiv2pb.UpdateWebhookResponse{ + Webhook: convertWebhookFromStore(webhook), + }, nil +} + +func (s *APIV2Service) DeleteWebhook(ctx context.Context, request *apiv2pb.DeleteWebhookRequest) (*apiv2pb.DeleteWebhookResponse, error) { + err := s.Store.DeleteWebhook(ctx, &store.DeleteWebhook{ + ID: request.Id, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete webhook, error: %+v", err) + } + return &apiv2pb.DeleteWebhookResponse{}, nil +} + +func convertWebhookFromStore(webhook *storepb.Webhook) *apiv2pb.Webhook { + return &apiv2pb.Webhook{ + Id: webhook.Id, + CreatedTime: timestamppb.New(time.Unix(webhook.CreatedTs, 0)), + UpdatedTime: timestamppb.New(time.Unix(webhook.UpdatedTs, 0)), + RowStatus: apiv2pb.RowStatus(webhook.RowStatus), + CreatorId: webhook.CreatorId, + Name: webhook.Name, + Url: webhook.Url, + } +} diff --git a/server/route/api/v2/workspace_service.go b/server/route/api/v2/workspace_service.go new file mode 100644 index 0000000000000..78b31a7e2689b --- /dev/null +++ b/server/route/api/v2/workspace_service.go @@ -0,0 +1,17 @@ +package v2 + +import ( + "context" + + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" +) + +func (s *APIV2Service) GetWorkspaceProfile(_ context.Context, _ *apiv2pb.GetWorkspaceProfileRequest) (*apiv2pb.GetWorkspaceProfileResponse, error) { + workspaceProfile := &apiv2pb.WorkspaceProfile{ + Version: s.Profile.Version, + Mode: s.Profile.Mode, + } + return &apiv2pb.GetWorkspaceProfileResponse{ + WorkspaceProfile: workspaceProfile, + }, nil +} diff --git a/server/route/api/v2/workspace_setting_service.go b/server/route/api/v2/workspace_setting_service.go new file mode 100644 index 0000000000000..8b3941997d332 --- /dev/null +++ b/server/route/api/v2/workspace_setting_service.go @@ -0,0 +1,95 @@ +package v2 + +import ( + "context" + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + apiv2pb "github.com/usememos/memos/proto/gen/api/v2" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (s *APIV2Service) GetWorkspaceSetting(ctx context.Context, request *apiv2pb.GetWorkspaceSettingRequest) (*apiv2pb.GetWorkspaceSettingResponse, error) { + settingKeyString, err := ExtractWorkspaceSettingKeyFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid workspace setting name: %v", err) + } + settingKey := storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[settingKeyString]) + workspaceSetting, err := s.Store.GetWorkspaceSettingV1(ctx, &store.FindWorkspaceSettingV1{ + Key: settingKey, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace setting: %v", err) + } + if workspaceSetting == nil { + return nil, status.Errorf(codes.NotFound, "workspace setting not found") + } + + return &apiv2pb.GetWorkspaceSettingResponse{ + Setting: convertWorkspaceSettingFromStore(workspaceSetting), + }, nil +} + +func (s *APIV2Service) SetWorkspaceSetting(ctx context.Context, request *apiv2pb.SetWorkspaceSettingRequest) (*apiv2pb.SetWorkspaceSettingResponse, error) { + user, err := getCurrentUser(ctx, s.Store) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if user.Role != store.RoleHost { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + if _, err := s.Store.UpsertWorkspaceSettingV1(ctx, convertWorkspaceSettingToStore(request.Setting)); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert workspace setting: %v", err) + } + + return &apiv2pb.SetWorkspaceSettingResponse{}, nil +} + +func convertWorkspaceSettingFromStore(setting *storepb.WorkspaceSetting) *apiv2pb.WorkspaceSetting { + return &apiv2pb.WorkspaceSetting{ + Name: fmt.Sprintf("%s%s", WorkspaceSettingNamePrefix, setting.Key.String()), + Value: &apiv2pb.WorkspaceSetting_GeneralSetting{ + GeneralSetting: convertWorkspaceGeneralSettingFromStore(setting.GetGeneral()), + }, + } +} + +func convertWorkspaceSettingToStore(setting *apiv2pb.WorkspaceSetting) *storepb.WorkspaceSetting { + settingKeyString, _ := ExtractWorkspaceSettingKeyFromName(setting.Name) + return &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[settingKeyString]), + Value: &storepb.WorkspaceSetting_General{ + General: convertWorkspaceGeneralSettingToStore(setting.GetGeneralSetting()), + }, + } +} + +func convertWorkspaceGeneralSettingFromStore(setting *storepb.WorkspaceGeneralSetting) *apiv2pb.WorkspaceGeneralSetting { + if setting == nil { + return nil + } + return &apiv2pb.WorkspaceGeneralSetting{ + InstanceUrl: setting.InstanceUrl, + DisallowSignup: setting.DisallowSignup, + DisallowPasswordLogin: setting.DisallowPasswordLogin, + AdditionalScript: setting.AdditionalScript, + AdditionalStyle: setting.AdditionalStyle, + } +} + +func convertWorkspaceGeneralSettingToStore(setting *apiv2pb.WorkspaceGeneralSetting) *storepb.WorkspaceGeneralSetting { + if setting == nil { + return nil + } + return &storepb.WorkspaceGeneralSetting{ + InstanceUrl: setting.InstanceUrl, + DisallowSignup: setting.DisallowSignup, + DisallowPasswordLogin: setting.DisallowPasswordLogin, + AdditionalScript: setting.AdditionalScript, + AdditionalStyle: setting.AdditionalStyle, + } +} diff --git a/server/route/frontend/frontend.go b/server/route/frontend/frontend.go new file mode 100644 index 0000000000000..02c56860d504c --- /dev/null +++ b/server/route/frontend/frontend.go @@ -0,0 +1,171 @@ +package frontend + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/yourselfhosted/gomark/parser" + "github.com/yourselfhosted/gomark/parser/tokenizer" + "github.com/yourselfhosted/gomark/renderer" + + "github.com/usememos/memos/internal/util" + "github.com/usememos/memos/server/profile" + "github.com/usememos/memos/store" +) + +const ( + // maxMetadataDescriptionLength is the maximum length of metadata description. + maxMetadataDescriptionLength = 256 +) + +type FrontendService struct { + Profile *profile.Profile + Store *store.Store +} + +func NewFrontendService(profile *profile.Profile, store *store.Store) *FrontendService { + return &FrontendService{ + Profile: profile, + Store: store, + } +} + +func (s *FrontendService) Serve(ctx context.Context, 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{ + Root: "dist", + HTML5: true, + Skipper: func(c echo.Context) bool { + return util.HasPrefixes(c.Path(), "/api", "/memos.api.v2", "/robots.txt", "/sitemap.xml", "/m/:name") + }, + })) + + s.registerRoutes(e) + s.registerFileRoutes(ctx, e) +} + +func (s *FrontendService) registerRoutes(e *echo.Echo) { + rawIndexHTML := getRawIndexHTML() + + e.GET("/m/:name", func(c echo.Context) error { + ctx := c.Request().Context() + resourceName := c.Param("name") + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ResourceName: &resourceName, + }) + if err != nil { + return c.HTML(http.StatusOK, rawIndexHTML) + } + if memo == nil { + return c.HTML(http.StatusOK, rawIndexHTML) + } + creator, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &memo.CreatorID, + }) + if err != nil { + return c.HTML(http.StatusOK, rawIndexHTML) + } + + // Inject memo metadata into `index.html`. + indexHTML := strings.ReplaceAll(rawIndexHTML, "", generateMemoMetadata(memo, creator).String()) + indexHTML = strings.ReplaceAll(indexHTML, "", fmt.Sprintf("", memo.ID)) + return c.HTML(http.StatusOK, indexHTML) + }) +} + +func (s *FrontendService) registerFileRoutes(ctx context.Context, e *echo.Echo) { + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) + if err != nil { + return + } + instanceURL := workspaceGeneralSetting.GetInstanceUrl() + if instanceURL == "" { + return + } + + e.GET("/robots.txt", func(c echo.Context) error { + robotsTxt := fmt.Sprintf(`User-agent: * +Allow: / +Host: %s +Sitemap: %s/sitemap.xml`, instanceURL, instanceURL) + return c.String(http.StatusOK, robotsTxt) + }) + + e.GET("/sitemap.xml", func(c echo.Context) error { + ctx := c.Request().Context() + urlsets := []string{} + // Append memo list. + memoList, err := s.Store.ListMemos(ctx, &store.FindMemo{ + VisibilityList: []store.Visibility{store.Public}, + }) + if err != nil { + return err + } + for _, memo := range memoList { + urlsets = append(urlsets, fmt.Sprintf(`- {t("common.about")} {customizedProfile.name} -
- -{t("about.memos-description")}
-{customizedProfile.description || t("about.no-server-description")}
-
-
- v{profile.version}
- {t("memo.archived-memos")}
- -{t("memo.fetching-data")}
-{t("memo.no-archived-memos")}
-- {t("setting.account-section.change-password")} ({propsUser.username}) + {t("setting.account-section.change-password")} ({user.nickname})
- +{t("auth.new-password")}
-{t("auth.repeat-new-password")}
- -{t("message.change-memo-created-time")}
- +{t("setting.account-section.change-password")}
- +{t("auth.new-password")}
-{t("auth.repeat-new-password")}
- -Create access token
- +{t(isCreating ? "setting.sso-section.create-sso" : "setting.sso-section.update-sso")}
- +{"Add references"}
- +{getDateTimeString(option.displayTime)}
++ {searchText ? getHighlightedContent(option.content) : option.content} +
+{getDateTimeString(memo.displayTime)}
+ {memo.content}{t("resource.create-dialog.title")}
-{t("tag-list.create-tag")}
-{t("tag.create-tag")}
+{t("tag-list.all-tags")}
+{t("tag.all-tags")}
{isCreating ? "Create webhook" : "Edit webhook"}
+{title}
-{content}
@@ -86,6 +86,6 @@ export const showCommonDialog = (props: CommonDialogProps) => { dialogName: `common-dialog ${props?.className ?? ""}`, }, CommonDialog, - props + props, ); }; diff --git a/web/src/components/DisablePasswordLoginDialog.tsx b/web/src/components/DisablePasswordLoginDialog.tsx index 061586df36dbd..18499324c6c32 100644 --- a/web/src/components/DisablePasswordLoginDialog.tsx +++ b/web/src/components/DisablePasswordLoginDialog.tsx @@ -1,4 +1,4 @@ -import { Button } from "@mui/joy"; +import { Button, IconButton, Input } from "@mui/joy"; import { useState } from "react"; import { toast } from "react-hot-toast"; import * as api from "@/helpers/api"; @@ -45,7 +45,7 @@ const DisablePasswordLoginDialog: React.FC{t("setting.system-section.disable-password-login")}
-{t("setting.system-section.disable-password-login-final-warning")}
- +{t("setting.system-section.disable-password-login-final-warning")}
+ > ) : ( -{t("setting.system-section.disable-password-login-warning")}
+{t("setting.system-section.disable-password-login-warning")}
)}{t("embed-memo.title")}
-{t("embed-memo.text")}
-
- {memoEmbeddedCode()}
-
- - {t("embed-memo.only-public-supported")} - - {t("embed-memo.copy")} - -
-+ {t("inbox.version-update", { + version: activity?.payload?.versionUpdate?.version, + })} +
++ {children.map((child, index) => ( ++ ); +}; + +export default Blockquote; diff --git a/web/src/components/MemoContent/Bold.tsx b/web/src/components/MemoContent/Bold.tsx new file mode 100644 index 0000000000000..b7651d11e11d2 --- /dev/null +++ b/web/src/components/MemoContent/Bold.tsx @@ -0,0 +1,19 @@ +import { Node } from "@/types/node"; +import Renderer from "./Renderer"; + +interface Props { + symbol: string; + children: Node[]; +} + +const Bold: React.FC+ ))} +
{content};
+};
+
+export default Code;
diff --git a/web/src/components/MemoContent/CodeBlock.tsx b/web/src/components/MemoContent/CodeBlock.tsx
new file mode 100644
index 0000000000000..3165b01000172
--- /dev/null
+++ b/web/src/components/MemoContent/CodeBlock.tsx
@@ -0,0 +1,64 @@
+import classNames from "classnames";
+import copy from "copy-to-clipboard";
+import hljs from "highlight.js";
+import toast from "react-hot-toast";
+import Icon from "../Icon";
+import MermaidBlock from "./MermaidBlock";
+import { BaseProps } from "./types";
+
+// Special languages that are rendered differently.
+enum SpecialLanguage {
+ HTML = "__html",
+ MERMAID = "mermaid",
+}
+
+interface Props extends BaseProps {
+ language: string;
+ content: string;
+}
+
+const CodeBlock: React.FC
+
+
+ {message}
; +}; + +export default Error; diff --git a/web/src/components/MemoContent/EmbeddedContent/index.tsx b/web/src/components/MemoContent/EmbeddedContent/index.tsx new file mode 100644 index 0000000000000..b44db2d714ee4 --- /dev/null +++ b/web/src/components/MemoContent/EmbeddedContent/index.tsx @@ -0,0 +1,25 @@ +import EmbeddedMemo from "./EmbeddedMemo"; +import EmbeddedResource from "./EmbeddedResource"; +import Error from "./Error"; + +interface Props { + resourceName: string; + params: string; +} + +const extractResourceTypeAndId = (resourceName: string) => { + const [resourceType, resourceId] = resourceName.split("/"); + return { resourceType, resourceId }; +}; + +const EmbeddedContent = ({ resourceName, params }: Props) => { + const { resourceType, resourceId } = extractResourceTypeAndId(resourceName); + if (resourceType === "memos") { + return
+ {content}
+
+ );
+};
+
+export default MermaidBlock;
diff --git a/web/src/components/MemoContent/OrderedList.tsx b/web/src/components/MemoContent/OrderedList.tsx
new file mode 100644
index 0000000000000..75d5f99b6d7dd
--- /dev/null
+++ b/web/src/components/MemoContent/OrderedList.tsx
@@ -0,0 +1,36 @@
+import { repeat } from "lodash-es";
+import { Node } from "@/types/node";
+import Renderer from "./Renderer";
+import { BaseProps } from "./types";
+
+interface Props extends BaseProps {
+ number: string;
+ indent: number;
+ children: Node[];
+}
+
+const OrderedList: React.FC
+ {children.map((child, index) => (
+
{message}
; +}; + +export default Error; diff --git a/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx b/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx new file mode 100644 index 0000000000000..5112ef5936a1a --- /dev/null +++ b/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx @@ -0,0 +1,47 @@ +import { useEffect } from "react"; +import useLoading from "@/hooks/useLoading"; +import useNavigateTo from "@/hooks/useNavigateTo"; +import { useMemoStore } from "@/store/v1"; +import Error from "./Error"; + +interface Props { + resourceId: string; + params: string; +} + +const ReferencedMemo = ({ resourceId, params: paramsStr }: Props) => { + const navigateTo = useNavigateTo(); + const loadingState = useLoading(); + const memoStore = useMemoStore(); + const memo = memoStore.getMemoByName(resourceId); + const params = new URLSearchParams(paramsStr); + + useEffect(() => { + memoStore.getOrFetchMemoByName(resourceId).finally(() => loadingState.setFinish()); + }, [resourceId]); + + if (loadingState.isLoading) { + return null; + } + if (!memo) { + return;
+ case NodeType.IMAGE:
+ return | + {h} + | + ))} +
|---|
| + {r} + | + ))} +
e.stopPropagation()}> - No tags found -
- )} -