commit bb402d4ccc767dc179f41dbf399940d867bc29ae Author: gugus Date: Wed Mar 4 06:30:47 2026 +0000 first commit diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..0bdc2f9 --- /dev/null +++ b/.air.toml @@ -0,0 +1,24 @@ +root = "." +tmp_dir = "tmp" + +[build] + entrypoint = ["./tmp/main", "--port", "8081"] + cmd = "go build -o ./tmp/main ./cmd/memos" + delay = 2000 + exclude_dir = ["assets", "tmp", "vendor", "web", "proto/gen", "arms_cache", "store/migration"] + include_ext = ["go", "yaml", "yml", "toml"] + exclude_regex = ["_test.go"] + +[log] + time = true + +[color] + app = "cyan" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "blue" + +[screen] + clear_on_rebuild = true + keep_scroll = true \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d465134 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +web/node_modules +web/dist +.git +.github +build/ +tmp/ +memos +*.md +.gitignore +.golangci.yaml +.dockerignore +docs/ +.DS_Store \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f9af238 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: usememos diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..89e12f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,82 @@ +name: Bug Report +description: If something isn't working as expected +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to report a bug! Please complete the form below to help us understand and fix the issue. + + - type: checkboxes + id: pre-check + attributes: + label: Pre-submission Checklist + description: Please confirm you have completed the following steps before submitting + options: + - label: I have searched the existing issues and this bug has not been reported yet + required: true + - label: I have tested this issue on the [demo site](https://demo.usememos.com) or the latest version + required: true + + - type: dropdown + id: issue-location + attributes: + label: Where did you encounter this bug? + description: Select where you tested and confirmed this issue + options: + - Latest stable version (self-hosted) + - Latest development version (self-hosted) + - Demo site (demo.usememos.com) + - Older version (please specify below) + default: 0 + validations: + required: true + + - type: input + id: version + attributes: + label: Memos Version + description: Provide the exact version (e.g., `v0.25.2`). Find this in Settings → About or via `--version` flag + placeholder: v0.25.2 + validations: + required: true + + - type: textarea + id: bug-description + attributes: + label: Bug Description + description: A clear and concise description of what the bug is + placeholder: When I try to..., the application... + validations: + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: Steps to Reproduce + description: Detailed steps to reproduce the behavior + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: What did you expect to happen? + placeholder: I expected... + + - type: textarea + id: additional-context + attributes: + label: Screenshots & Additional Context + description: Add screenshots, browser/OS info, deployment method (Docker/binary), or any other relevant details + placeholder: | + - Browser: Chrome 120 + - OS: macOS 14 + - Deployment: Docker + - Database: SQLite diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..f76b573 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,76 @@ +name: Feature Request +description: If you have a suggestion for a new feature +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + Thank you for suggesting a new feature! Please complete the form below to help us understand your idea. + + - type: checkboxes + id: pre-check + attributes: + label: Pre-submission Checklist + description: Please confirm you have completed the following steps before submitting + options: + - label: I have searched the existing issues and this feature has not been requested yet + required: true + + - type: dropdown + id: feature-type + attributes: + label: Type of Feature + description: What type of feature is this? + options: + - User Interface (UI) + - User Experience (UX) + - API / Backend + - Documentation + - Integrations / Plugins + - Security / Privacy + - Performance + - Other + default: 0 + validations: + required: true + + - type: textarea + id: problem-statement + attributes: + label: Problem or Use Case + description: What problem does this feature solve? What are you trying to accomplish? + placeholder: | + I often need to... but currently there's no way to... + This would help me/users to... + validations: + required: true + + - type: textarea + id: proposed-solution + attributes: + label: Proposed Solution + description: A clear and concise description of what you want to happen + placeholder: | + It would be great if Memos could... + For example, a button/feature that... + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Have you considered any alternative solutions or workarounds? + placeholder: | + I've tried... but it doesn't work well because... + An alternative could be... + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Add any other context, mockups, screenshots, or examples about the feature request + placeholder: | + - Similar feature in other apps: ... + - Mockups or screenshots: ... + - Related discussions: ... diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml new file mode 100644 index 0000000..a3e3163 --- /dev/null +++ b/.github/workflows/backend-tests.yml @@ -0,0 +1,91 @@ +name: Backend Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + paths: + - "go.mod" + - "go.sum" + - "**.go" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + GO_VERSION: "1.25.7" + +jobs: + static-checks: + name: Static Checks + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: go.sum + + - name: Verify go.mod is tidy + run: | + go mod tidy -go=${{ env.GO_VERSION }} + git diff --exit-code + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.4.0 + args: --timeout=3m + + tests: + name: Tests (${{ matrix.test-group }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + test-group: [store, server, plugin, other] + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: go.sum + + - name: Run tests + run: | + case "${{ matrix.test-group }}" in + store) + # Run store tests for all drivers (sqlite, mysql, postgres) + go test -v -coverprofile=coverage.out -covermode=atomic ./store/... + ;; + server) + go test -v -race -coverprofile=coverage.out -covermode=atomic ./server/... + ;; + plugin) + go test -v -race -coverprofile=coverage.out -covermode=atomic ./plugin/... + ;; + other) + go test -v -race -coverprofile=coverage.out -covermode=atomic \ + ./cmd/... ./internal/... ./proto/... + ;; + esac + env: + DRIVER: ${{ matrix.test-group == 'store' && '' || 'sqlite' }} + + - name: Upload coverage + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: codecov/codecov-action@v5 + with: + files: ./coverage.out + flags: ${{ matrix.test-group }} + fail_ci_if_error: false diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml new file mode 100644 index 0000000..64a0ac6 --- /dev/null +++ b/.github/workflows/build-binaries.yml @@ -0,0 +1,231 @@ +name: Build Binaries + +# Build multi-platform binaries on release or manual trigger +# Produces distributable packages for Linux, macOS, and Windows +on: + release: + types: [published] + workflow_dispatch: + +# Environment variables for build configuration +env: + GO_VERSION: "1.25.7" + NODE_VERSION: "22" + PNPM_VERSION: "10" + ARTIFACT_RETENTION_DAYS: 60 + # Artifact naming: {ARTIFACT_PREFIX}_{version}_{os}_{arch}.tar.gz|zip + ARTIFACT_PREFIX: memos + +jobs: + # Job 1: Extract version information + # - For git tags: use tag version (e.g., v0.28.1 -> 0.28.1) + # - For branches: use branch-name-shortSHA format + prepare: + name: Extract Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 # Full history for git describe + + - name: Extract version + id: version + run: | + # Try to get version from git tag + TAG=$(git describe --tags --exact-match 2>/dev/null || echo "") + if [ -n "$TAG" ]; then + echo "version=${TAG#v}" >> $GITHUB_OUTPUT + echo "Version from tag: ${TAG#v}" + else + # Use branch name + short SHA + BRANCH="${GITHUB_REF_NAME//\//-}" + SHORT_SHA="${GITHUB_SHA:0:7}" + echo "version=${BRANCH}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "Version from branch: ${BRANCH}-${SHORT_SHA}" + fi + + # Job 2: Build frontend assets + # - Builds React frontend with Vite + # - Produces static files that will be embedded in Go binary + # - Shared across all platform builds + build-frontend: + name: Build Frontend + needs: prepare + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4.2.0 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: web/pnpm-lock.yaml + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + working-directory: web + run: pnpm install --frozen-lockfile + + - name: Build frontend + working-directory: web + run: pnpm release + + - name: Upload frontend artifacts + uses: actions/upload-artifact@v6 + with: + name: frontend-dist + path: server/router/frontend/dist + retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} + + # Job 3: Build Go binaries for multiple platforms + # - Cross-compiles using native Go toolchain + # - Embeds frontend assets built in previous job + # - Produces static binaries with no external dependencies + # - Packages as tar.gz (Unix) or zip (Windows) + build-binaries: + name: Build ${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }} + needs: [prepare, build-frontend] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # Linux targets + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: linux + goarch: arm + goarm: "7" + # macOS targets + - goos: darwin + goarch: amd64 # Intel Macs + - goos: darwin + goarch: arm64 # Apple Silicon + # Windows targets + - goos: windows + goarch: amd64 + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Download frontend artifacts + uses: actions/download-artifact@v7 + with: + name: frontend-dist + path: server/router/frontend/dist + + - name: Build binary + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOARM: ${{ matrix.goarm }} + CGO_ENABLED: "0" + run: | + # Determine output binary name + OUTPUT_NAME="memos" + if [ "$GOOS" = "windows" ]; then + OUTPUT_NAME="memos.exe" + fi + + mkdir -p build + + # Build static binary with optimizations + go build \ + -trimpath \ + -ldflags="-s -w -extldflags '-static'" \ + -tags netgo,osusergo \ + -o "build/${OUTPUT_NAME}" \ + ./cmd/memos + + echo "✓ Built: build/${OUTPUT_NAME}" + ls -lh build/ + + - name: Package binary + id: package + env: + VERSION: ${{ needs.prepare.outputs.version }} + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOARM: ${{ matrix.goarm }} + run: | + cd build + + # Construct package name: {prefix}_{version}_{os}_{arch}[v{arm_version}] + PACKAGE_NAME="${ARTIFACT_PREFIX}_${VERSION}_${GOOS}_${GOARCH}" + if [ -n "$GOARM" ]; then + PACKAGE_NAME="${PACKAGE_NAME}v${GOARM}" + fi + + # Package based on platform + if [ "$GOOS" = "windows" ]; then + ARTIFACT_NAME="${PACKAGE_NAME}.zip" + zip -q "${ARTIFACT_NAME}" memos.exe + else + ARTIFACT_NAME="${PACKAGE_NAME}.tar.gz" + tar czf "${ARTIFACT_NAME}" memos + fi + + # Output for next step + echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV + echo "✓ Package created: ${ARTIFACT_NAME} ($(du -h "${ARTIFACT_NAME}" | cut -f1))" + + - name: Upload binary artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ env.ARTIFACT_NAME }} + path: build/${{ env.ARTIFACT_NAME }} + retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} + + # Job 4: Upload artifacts to GitHub Release + # - Only runs when triggered by a release publish event + # - Downloads all built artifacts and attaches them to the release + release: + name: Upload Release Assets + needs: build-binaries + if: github.event_name == 'release' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + pattern: ${{ env.ARTIFACT_PREFIX }}_* + merge-multiple: true + + - name: Upload to GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: artifacts/* + diff --git a/.github/workflows/build-canary-image.yml b/.github/workflows/build-canary-image.yml new file mode 100644 index 0000000..16f6ab3 --- /dev/null +++ b/.github/workflows/build-canary-image.yml @@ -0,0 +1,166 @@ +name: Build Canary Image + +on: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.repository }} + cancel-in-progress: true + +jobs: + build-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v4.2.0 + with: + version: 10 + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: pnpm + cache-dependency-path: "web/pnpm-lock.yaml" + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + - name: Setup pnpm cache + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-store- + - run: pnpm install --frozen-lockfile + working-directory: web + - name: Run frontend build + run: pnpm release + working-directory: web + + - name: Upload frontend artifacts + uses: actions/upload-artifact@v6 + with: + name: frontend-dist + path: server/router/frontend/dist + retention-days: 1 + + build-push: + needs: build-frontend + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + steps: + - uses: actions/checkout@v6 + + - name: Download frontend artifacts + uses: actions/download-artifact@v7 + with: + name: frontend-dist + path: server/router/frontend/dist + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: ./scripts/Dockerfile + platforms: ${{ matrix.platform }} + cache-from: type=gha,scope=build-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} + outputs: type=image,name=neosmemo/memos,push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v6 + with: + name: digests-${{ strategy.job-index }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + needs: build-push + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@v7 + with: + pattern: digests-* + merge-multiple: true + path: /tmp/digests + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + neosmemo/memos + ghcr.io/usememos/memos + flavor: | + latest=false + tags: | + type=raw,value=canary + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'neosmemo/memos@sha256:%s ' *) + env: + DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} + + - name: Inspect images + run: | + docker buildx imagetools inspect neosmemo/memos:canary + docker buildx imagetools inspect ghcr.io/usememos/memos:canary diff --git a/.github/workflows/build-stable-image.yml b/.github/workflows/build-stable-image.yml new file mode 100644 index 0000000..4ec106b --- /dev/null +++ b/.github/workflows/build-stable-image.yml @@ -0,0 +1,184 @@ +name: Build Stable Image + +on: + push: + branches: + - "release/**" + tags: + - "v*.*.*" + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Extract version + id: version + run: | + if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then + echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT + else + echo "version=${GITHUB_REF_NAME#release/}" >> $GITHUB_OUTPUT + fi + + build-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v4.2.0 + with: + version: 10 + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: pnpm + cache-dependency-path: "web/pnpm-lock.yaml" + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + - name: Setup pnpm cache + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-store- + - run: pnpm install --frozen-lockfile + working-directory: web + - name: Run frontend build + run: pnpm release + working-directory: web + + - name: Upload frontend artifacts + uses: actions/upload-artifact@v6 + with: + name: frontend-dist + path: server/router/frontend/dist + retention-days: 1 + + build-push: + needs: [prepare, build-frontend] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm/v7 + - linux/arm64 + steps: + - uses: actions/checkout@v6 + + - name: Download frontend artifacts + uses: actions/download-artifact@v7 + with: + name: frontend-dist + path: server/router/frontend/dist + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: ./scripts/Dockerfile + platforms: ${{ matrix.platform }} + cache-from: type=gha,scope=build-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} + outputs: type=image,name=neosmemo/memos,push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v6 + with: + name: digests-${{ strategy.job-index }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + needs: [prepare, build-push] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@v7 + with: + pattern: digests-* + merge-multiple: true + path: /tmp/digests + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + neosmemo/memos + ghcr.io/usememos/memos + tags: | + type=semver,pattern={{version}},value=${{ needs.prepare.outputs.version }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.prepare.outputs.version }} + type=raw,value=stable + flavor: | + latest=false + labels: | + org.opencontainers.image.version=${{ needs.prepare.outputs.version }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'neosmemo/memos@sha256:%s ' *) + env: + DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} + + - name: Inspect images + run: | + docker buildx imagetools inspect neosmemo/memos:stable + docker buildx imagetools inspect ghcr.io/usememos/memos:stable diff --git a/.github/workflows/demo-deploy.yml b/.github/workflows/demo-deploy.yml new file mode 100644 index 0000000..4ff6a37 --- /dev/null +++ b/.github/workflows/demo-deploy.yml @@ -0,0 +1,17 @@ +name: Demo Deploy + +on: + workflow_dispatch: + +jobs: + deploy-demo: + runs-on: ubuntu-latest + steps: + - name: Trigger Render Deploy + run: | + curl -X POST "${{ secrets.RENDER_DEPLOY_HOOK }}" \ + -H "Content-Type: application/json" \ + -d '{"trigger": "github_action"}' + + - name: Deployment Status + run: echo "Demo deployment triggered successfully on Render" diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml new file mode 100644 index 0000000..c296f5e --- /dev/null +++ b/.github/workflows/frontend-tests.yml @@ -0,0 +1,72 @@ +name: Frontend Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + paths: + - "web/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: "22" + PNPM_VERSION: "10" + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4.2.0 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: web/pnpm-lock.yaml + + - name: Install dependencies + working-directory: web + run: pnpm install --frozen-lockfile + + - name: Run lint + working-directory: web + run: pnpm lint + + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4.2.0 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: web/pnpm-lock.yaml + + - name: Install dependencies + working-directory: web + run: pnpm install --frozen-lockfile + + - name: Build frontend + working-directory: web + run: pnpm build diff --git a/.github/workflows/proto-linter.yml b/.github/workflows/proto-linter.yml new file mode 100644 index 0000000..a642234 --- /dev/null +++ b/.github/workflows/proto-linter.yml @@ -0,0 +1,40 @@ +name: Proto Linter + +on: + push: + branches: [main] + pull_request: + branches: [main] + paths: + - "proto/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint Protos + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup buf + uses: bufbuild/buf-setup-action@v1 + with: + github_token: ${{ github.token }} + + - name: Run buf lint + uses: bufbuild/buf-lint-action@v1 + with: + input: proto + + - name: Check buf format + run: | + if [[ $(buf format -d) ]]; then + echo "❌ Proto files are not formatted. Run 'buf format -w' to fix." + exit 1 + fi diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..18991fe --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,24 @@ +name: Close Stale + +on: + schedule: + - cron: "0 */8 * * *" # Every 8 hours + +jobs: + close-stale: + name: Close Stale Issues and PRs + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Mark and close stale issues and PRs + uses: actions/stale@v10.1.1 + with: + # Issues: mark stale after 14 days of inactivity, close after 3 more days + days-before-issue-stale: 14 + days-before-issue-close: 3 + + # Pull requests: mark stale after 14 days of inactivity, close after 3 more days + days-before-pr-stale: 14 + days-before-pr-close: 3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abd9d94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# temp folder +tmp + +# Frontend asset +web/dist + +# Build artifacts +build/ +bin/ +memos + +# Plan/design documents +docs/plans/ + +.DS_Store + +# Jetbrains +.idea + +# Docker Compose Environment File +.env + +dist + +# VSCode settings +.vscode + +# Git worktrees +.worktrees/ diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..b97938f --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,101 @@ +version: "2" + +linters: + enable: + - revive + - govet + - staticcheck + - misspell + - gocritic + - sqlclosecheck + - rowserrcheck + - nilerr + - godot + - forbidigo + - mirror + - bodyclose + disable: + - errcheck + settings: + exhaustive: + explicit-exhaustive-switch: false + staticcheck: + checks: + - all + - -ST1000 + - -ST1003 + - -ST1021 + - -QF1003 + revive: + # Default to run all linters so that new rules in the future could automatically be added to the static check. + enable-all-rules: true + rules: + # The following rules are too strict and make coding harder. We do not enable them for now. + - name: file-header + disabled: true + - name: line-length-limit + disabled: true + - name: function-length + disabled: true + - name: max-public-structs + disabled: true + - name: function-result-limit + disabled: true + - name: banned-characters + disabled: true + - name: argument-limit + disabled: true + - name: cognitive-complexity + disabled: true + - name: cyclomatic + disabled: true + - name: confusing-results + disabled: true + - name: add-constant + disabled: true + - name: flag-parameter + disabled: true + - name: nested-structs + disabled: true + - name: import-shadowing + disabled: true + - name: early-return + disabled: true + - name: use-any + disabled: true + - name: exported + disabled: true + - name: unhandled-error + disabled: true + - name: if-return + disabled: true + - name: max-control-nesting + disabled: true + - name: redefines-builtin-id + disabled: true + - name: package-comments + disabled: true + gocritic: + disabled-checks: + - ifElseChain + govet: + settings: + printf: # The name of the analyzer, run `go tool vet help` to see the list of all analyzers + funcs: # Run `go tool vet help printf` to see the full configuration of `printf`. + - common.Errorf + enable-all: true + disable: + - fieldalignment + - shadow + forbidigo: + forbid: + - pattern: 'fmt\.Errorf(# Please use errors\.Wrap\|Wrapf\|Errorf instead)?' + - pattern: 'ioutil\.ReadDir(# Please use os\.ReadDir)?' + +formatters: + enable: + - goimports + settings: + goimports: + local-prefixes: + - github.com/usememos/memos diff --git a/.thumbnail_cache/1.png b/.thumbnail_cache/1.png new file mode 100644 index 0000000..6a2af58 Binary files /dev/null and b/.thumbnail_cache/1.png differ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3ed1a61 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,572 @@ +# Memos Codebase Guide for AI Agents + +This document provides comprehensive guidance for AI agents working with the Memos codebase. It covers architecture, workflows, conventions, and key patterns. + +## Project Overview + +Memos is a self-hosted knowledge management platform built with: +- **Backend:** Go 1.25 with gRPC + Connect RPC +- **Frontend:** React 18.3 + TypeScript + Vite 7 +- **Databases:** SQLite (default), MySQL, PostgreSQL +- **Protocol:** Protocol Buffers (v2) with buf for code generation +- **API Layer:** Dual protocol - Connect RPC (browsers) + gRPC-Gateway (REST) + +## Architecture + +### Backend Architecture + +``` +cmd/memos/ # Entry point +└── main.go # Cobra CLI, profile setup, server initialization + +server/ +├── server.go # Echo HTTP server, healthz, background runners +├── auth/ # Authentication (JWT, PAT, session) +├── router/ +│ ├── api/v1/ # gRPC service implementations +│ │ ├── v1.go # Service registration, gateway & Connect setup +│ │ ├── acl_config.go # Public endpoints whitelist +│ │ ├── connect_services.go # Connect RPC handlers +│ │ ├── connect_interceptors.go # Auth, logging, recovery +│ │ └── *_service.go # Individual services (memo, user, etc.) +│ ├── frontend/ # Static file serving (SPA) +│ ├── fileserver/ # Native HTTP file serving for media +│ └── rss/ # RSS feed generation +└── runner/ + ├── memopayload/ # Memo payload processing (tags, links, tasks) + └── s3presign/ # S3 presigned URL management + +store/ # Data layer with caching +├── driver.go # Driver interface (database operations) +├── store.go # Store wrapper with cache layer +├── cache.go # In-memory caching (instance settings, users) +├── migrator.go # Database migrations +├── db/ +│ ├── db.go # Driver factory +│ ├── sqlite/ # SQLite implementation +│ ├── mysql/ # MySQL implementation +│ └── postgres/ # PostgreSQL implementation +└── migration/ # SQL migration files (embedded) + +proto/ # Protocol Buffer definitions +├── api/v1/ # API v1 service definitions +└── gen/ # Generated Go & TypeScript code +``` + +### Frontend Architecture + +``` +web/ +├── src/ +│ ├── components/ # React components +│ ├── contexts/ # React Context (client state) +│ │ ├── AuthContext.tsx # Current user, auth state +│ │ ├── ViewContext.tsx # Layout, sort order +│ │ └── MemoFilterContext.tsx # Filters, shortcuts +│ ├── hooks/ # React Query hooks (server state) +│ │ ├── useMemoQueries.ts # Memo CRUD, pagination +│ │ ├── useUserQueries.ts # User operations +│ │ ├── useAttachmentQueries.ts # Attachment operations +│ │ └── ... +│ ├── lib/ # Utilities +│ │ ├── query-client.ts # React Query v5 client +│ │ └── connect.ts # Connect RPC client setup +│ ├── pages/ # Page components +│ └── types/proto/ # Generated TypeScript from .proto +├── package.json # Dependencies +└── vite.config.mts # Vite config with dev proxy + +plugin/ # Backend plugins +├── scheduler/ # Cron jobs +├── email/ # Email delivery +├── filter/ # CEL filter expressions +├── webhook/ # Webhook dispatch +├── markdown/ # Markdown parsing & rendering +├── httpgetter/ # HTTP fetching (metadata, images) +└── storage/s3/ # S3 storage backend +``` + +## Key Architectural Patterns + +### 1. API Layer: Dual Protocol + +**Connect RPC (Browser Clients):** +- Protocol: `connectrpc.com/connect` +- Base path: `/memos.api.v1.*` +- Interceptor chain: Metadata → Logging → Recovery → Auth +- Returns type-safe responses to React frontend +- See: `server/router/api/v1/connect_interceptors.go:177-227` + +**gRPC-Gateway (REST API):** +- Protocol: Standard HTTP/JSON +- Base path: `/api/v1/*` +- Uses same service implementations as Connect +- Useful for external tools, CLI clients +- See: `server/router/api/v1/v1.go:52-96` + +**Authentication:** +- JWT Access Tokens (V2): Stateless, 15-min expiration, verified via `AuthenticateByAccessTokenV2` +- Personal Access Tokens (PAT): Stateful, long-lived, validated against database +- Both use `Authorization: Bearer ` header +- See: `server/auth/authenticator.go:17-166` + +### 2. Store Layer: Interface Pattern + +All database operations go through the `Driver` interface: +```go +type Driver interface { + GetDB() *sql.DB + Close() error + + IsInitialized(ctx context.Context) (bool, error) + + CreateMemo(ctx context.Context, create *Memo) (*Memo, error) + ListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error) + UpdateMemo(ctx context.Context, update *UpdateMemo) error + DeleteMemo(ctx context.Context, delete *DeleteMemo) error + + // ... similar methods for all resources +} +``` + +**Three Implementations:** +- `store/db/sqlite/` - SQLite (modernc.org/sqlite) +- `store/db/mysql/` - MySQL (go-sql-driver/mysql) +- `store/db/postgres/` - PostgreSQL (lib/pq) + +**Caching Strategy:** +- Store wrapper maintains in-memory caches for: + - Instance settings (`instanceSettingCache`) + - Users (`userCache`) + - User settings (`userSettingCache`) +- Config: Default TTL 10 min, cleanup interval 5 min, max 1000 items +- See: `store/store.go:10-57` + +### 3. Frontend State Management + +**React Query v5 (Server State):** +- All API calls go through custom hooks in `web/src/hooks/` +- Query keys organized by resource: `memoKeys`, `userKeys`, `attachmentKeys` +- Default staleTime: 30s, gcTime: 5min +- Automatic refetch on window focus, reconnect +- See: `web/src/lib/query-client.ts` + +**React Context (Client State):** +- `AuthContext`: Current user, auth initialization, logout +- `ViewContext`: Layout mode (LIST/MASONRY), sort order +- `MemoFilterContext`: Active filters, shortcut selection, URL sync + +### 4. Database Migration System + +**Migration Flow:** +1. `preMigrate`: Check if DB exists. If not, apply `LATEST.sql` +2. `checkMinimumUpgradeVersion`: Reject pre-0.22 installations +3. `applyMigrations`: Apply incremental migrations in single transaction +4. Demo mode: Seed with demo data + +**Schema Versioning:** +- Stored in `system_setting` table +- Format: `major.minor.patch` +- Migration files: `store/migration/{driver}/{version}/NN__description.sql` +- See: `store/migrator.go:21-414` + +### 5. Protocol Buffer Code Generation + +**Definition Location:** `proto/api/v1/*.proto` + +**Regeneration:** +```bash +cd proto && buf generate +``` + +**Generated Outputs:** +- Go: `proto/gen/api/v1/` (used by backend services) +- TypeScript: `web/src/types/proto/api/v1/` (used by frontend) + +**Linting:** `proto/buf.yaml` - BASIC lint rules, FILE breaking changes + +## Development Commands + +### Backend + +```bash +# Start dev server +go run ./cmd/memos --port 8081 + +# Run all tests +go test ./... + +# Run tests for specific package +go test ./store/... +go test ./server/router/api/v1/test/... + +# Lint (golangci-lint) +golangci-lint run + +# Format imports +goimports -w . + +# Run with MySQL/Postgres +DRIVER=mysql go run ./cmd/memos +DRIVER=postgres go run ./cmd/memos +``` + +### Frontend + +```bash +# Install dependencies +cd web && pnpm install + +# Start dev server (proxies API to localhost:8081) +pnpm dev + +# Type checking +pnpm lint + +# Auto-fix lint issues +pnpm lint:fix + +# Format code +pnpm format + +# Build for production +pnpm build + +# Build and copy to backend +pnpm release +``` + +### Protocol Buffers + +```bash +# Regenerate Go and TypeScript from .proto files +cd proto && buf generate + +# Lint proto files +cd proto && buf lint + +# Check for breaking changes +cd proto && buf breaking --against .git#main +``` + +## Key Workflows + +### Adding a New API Endpoint + +1. **Define in Protocol Buffer:** + - Edit `proto/api/v1/*_service.proto` + - Add request/response messages + - Add RPC method to service + +2. **Regenerate Code:** + ```bash + cd proto && buf generate + ``` + +3. **Implement Service (Backend):** + - Add method to `server/router/api/v1/*_service.go` + - Follow existing patterns: fetch user, validate, call store + - Add Connect wrapper to `server/router/api/v1/connect_services.go` (optional, same implementation) + +4. **If Public Endpoint:** + - Add to `server/router/api/v1/acl_config.go:11-34` + +5. **Create Frontend Hook (if needed):** + - Add query/mutation to `web/src/hooks/use*Queries.ts` + - Use existing query key factories + +### Database Schema Changes + +1. **Create Migration Files:** + ``` + store/migration/sqlite/0.28/1__add_new_column.sql + store/migration/mysql/0.28/1__add_new_column.sql + store/migration/postgres/0.28/1__add_new_column.sql + ``` + +2. **Update LATEST.sql:** + - Add change to `store/migration/{driver}/LATEST.sql` + +3. **Update Store Interface (if new table/model):** + - Add methods to `store/driver.go:8-71` + - Implement in `store/db/{driver}/*.go` + +4. **Test Migration:** + - Run `go test ./store/test/...` to verify + +### Adding a New Frontend Page + +1. **Create Page Component:** + - Add to `web/src/pages/NewPage.tsx` + - Use existing hooks for data fetching + +2. **Add Route:** + - Edit `web/src/App.tsx` (or router configuration) + +3. **Use React Query:** + ```typescript + import { useMemos } from "@/hooks/useMemoQueries"; + const { data, isLoading } = useMemos({ filter: "..." }); + ``` + +4. **Use Context for Client State:** + ```typescript + import { useView } from "@/contexts/ViewContext"; + const { layout, toggleSortOrder } = useView(); + ``` + +## Testing + +### Backend Tests + +**Test Pattern:** +```go +func TestMemoCreation(t *testing.T) { + ctx := context.Background() + store := test.NewTestingStore(ctx, t) + + // Create test user + user, _ := createTestUser(ctx, store, t) + + // Execute operation + memo, err := store.CreateMemo(ctx, &store.Memo{ + CreatorID: user.ID, + Content: "Test memo", + // ... + }) + require.NoError(t, err) + assert.NotNil(t, memo) +} +``` + +**Test Utilities:** +- `store/test/store.go:22-35` - `NewTestingStore()` creates isolated DB +- `store/test/store.go:37-77` - `resetTestingDB()` cleans tables +- Test DB determined by `DRIVER` env var (default: sqlite) + +**Running Tests:** +```bash +# All tests +go test ./... + +# Specific package +go test ./store/... +go test ./server/router/api/v1/test/... + +# With coverage +go test -cover ./... +``` + +### Frontend Testing + +**TypeScript Checking:** +```bash +cd web && pnpm lint +``` + +**No Automated Tests:** +- Frontend relies on TypeScript checking and manual validation +- React Query DevTools available in dev mode (bottom-left) + +## Code Conventions + +### Go + +**Error Handling:** +- Use `github.com/pkg/errors` for wrapping: `errors.Wrap(err, "context")` +- Return structured gRPC errors: `status.Errorf(codes.NotFound, "message")` + +**Naming:** +- Package names: lowercase, single word (e.g., `store`, `server`) +- Interfaces: `Driver`, `Store`, `Service` +- Methods: PascalCase for exported, camelCase for internal + +**Comments:** +- Public exported functions must have comments (godot enforces) +- Use `//` for single-line, `/* */` for multi-line + +**Imports:** +- Grouped: stdlib, third-party, local +- Sorted alphabetically within groups +- Use `goimports -w .` to format + +### TypeScript/React + +**Components:** +- Functional components with hooks +- Use `useMemo`, `useCallback` for optimization +- Props interfaces: `interface Props { ... }` + +**State Management:** +- Server state: React Query hooks +- Client state: React Context +- Avoid direct useState for server data + +**Styling:** +- Tailwind CSS v4 via `@tailwindcss/vite` +- Use `clsx` and `tailwind-merge` for conditional classes + +**Imports:** +- Absolute imports with `@/` alias +- Group: React, third-party, local +- Auto-organized by Biome + +## Important Files Reference + +### Backend Entry Points + +| File | Purpose | +|------|---------| +| `cmd/memos/main.go` | Server entry point, CLI setup | +| `server/server.go` | Echo server initialization, background runners | +| `store/store.go` | Store wrapper with caching | +| `store/driver.go` | Database driver interface | + +### API Layer + +| File | Purpose | +|------|---------| +| `server/router/api/v1/v1.go` | Service registration, gateway setup | +| `server/router/api/v1/acl_config.go` | Public endpoints whitelist | +| `server/router/api/v1/connect_interceptors.go` | Connect interceptors | +| `server/auth/authenticator.go` | Authentication logic | + +### Frontend Core + +| File | Purpose | +|------|---------| +| `web/src/lib/query-client.ts` | React Query client configuration | +| `web/src/contexts/AuthContext.tsx` | User authentication state | +| `web/src/contexts/ViewContext.tsx` | UI preferences | +| `web/src/contexts/MemoFilterContext.tsx` | Filter state | +| `web/src/hooks/useMemoQueries.ts` | Memo queries/mutations | + +### Data Layer + +| File | Purpose | +|------|---------| +| `store/memo.go` | Memo model definitions, store methods | +| `store/user.go` | User model definitions | +| `store/attachment.go` | Attachment model definitions | +| `store/migrator.go` | Migration logic | +| `store/db/db.go` | Driver factory | +| `store/db/sqlite/sqlite.go` | SQLite driver implementation | + +## Configuration + +### Backend Environment Variables + +| Variable | Default | Description | +|----------|----------|-------------| +| `MEMOS_DEMO` | `false` | Enable demo mode | +| `MEMOS_PORT` | `8081` | HTTP port | +| `MEMOS_ADDR` | `` | Bind address (empty = all) | +| `MEMOS_DATA` | `~/.memos` | Data directory | +| `MEMOS_DRIVER` | `sqlite` | Database: `sqlite`, `mysql`, `postgres` | +| `MEMOS_DSN` | `` | Database connection string | +| `MEMOS_INSTANCE_URL` | `` | Instance base URL | + +### Frontend Environment Variables + +| Variable | Default | Description | +|----------|----------|-------------| +| `DEV_PROXY_SERVER` | `http://localhost:8081` | Backend proxy target | + +## CI/CD + +### GitHub Workflows + +**Backend Tests** (`.github/workflows/backend-tests.yml`): +- Runs on `go.mod`, `go.sum`, `**.go` changes +- Steps: verify `go mod tidy`, golangci-lint, all tests + +**Frontend Tests** (`.github/workflows/frontend-tests.yml`): +- Runs on `web/**` changes +- Steps: pnpm install, lint, build + +**Proto Lint** (`.github/workflows/proto-linter.yml`): +- Runs on `.proto` changes +- Steps: buf lint, buf breaking check + +### Linting Configuration + +**Go** (`.golangci.yaml`): +- Linters: revive, govet, staticcheck, misspell, gocritic, etc. +- Formatter: goimports +- Forbidden: `fmt.Errorf`, `ioutil.ReadDir` + +**TypeScript** (`web/biome.json`): +- Linting: Biome (ESLint replacement) +- Formatting: Biome (Prettier replacement) +- Line width: 140 characters +- Semicolons: always + +## Common Tasks + +### Debugging API Issues + +1. Check Connect interceptor logs: `server/router/api/v1/connect_interceptors.go:79-105` +2. Verify endpoint is in `acl_config.go` if public +3. Check authentication via `auth/authenticator.go:133-165` +4. Test with curl: `curl -H "Authorization: Bearer " http://localhost:8081/api/v1/...` + +### Debugging Frontend State + +1. Open React Query DevTools (bottom-left in dev) +2. Inspect query cache, mutations, refetch behavior +3. Check Context state via React DevTools +4. Verify filter state in MemoFilterContext + +### Running Tests Against Multiple Databases + +```bash +# SQLite (default) +DRIVER=sqlite go test ./... + +# MySQL (requires running MySQL server) +DRIVER=mysql DSN="user:pass@tcp(localhost:3306)/memos" go test ./... + +# PostgreSQL (requires running PostgreSQL server) +DRIVER=postgres DSN="postgres://user:pass@localhost:5432/memos" go test ./... +``` + +## Plugin System + +Backend supports pluggable components in `plugin/`: + +| Plugin | Purpose | +|--------|----------| +| `scheduler` | Cron-based job scheduling | +| `email` | SMTP email delivery | +| `filter` | CEL expression filtering | +| `webhook` | HTTP webhook dispatch | +| `markdown` | Markdown parsing (goldmark) | +| `httpgetter` | HTTP content fetching | +| `storage/s3` | S3-compatible storage | + +Each plugin has its own README with usage examples. + +## Performance Considerations + +### Backend + +- Database queries use pagination (`limit`, `offset`) +- In-memory caching reduces DB hits for frequently accessed data +- WAL journal mode for SQLite (reduces locking) +- Thumbnail generation limited to 3 concurrent operations + +### Frontend + +- React Query reduces redundant API calls +- Infinite queries for large lists (pagination) +- Manual chunks: `utils-vendor`, `mermaid-vendor`, `leaflet-vendor` +- Lazy loading for heavy components + +## Security Notes + +- JWT secrets must be kept secret (generated on first run in production mode) +- Personal Access Tokens stored as SHA-256 hashes in database +- CSRF protection via SameSite cookies +- CORS enabled for all origins (configure for production) +- Input validation at service layer +- SQL injection prevention via parameterized queries diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..473833b --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# These owners will be the default owners for everything in the repo. +* @boojack @johnnyjoygh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ad324ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Memos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3105cf --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# Memos + +Memos + +An open-source, self-hosted note-taking service. Your thoughts, your data, your control — no tracking, no ads, no subscription fees. + +[![Home](https://img.shields.io/badge/🏠-usememos.com-blue?style=flat-square)](https://usememos.com) +[![Live Demo](https://img.shields.io/badge/✨-Try%20Demo-orange?style=flat-square)](https://demo.usememos.com/) +[![Docs](https://img.shields.io/badge/📚-Documentation-green?style=flat-square)](https://usememos.com/docs) +[![Discord](https://img.shields.io/badge/💬-Discord-5865f2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/tfPJa4UmAv) +[![Docker Pulls](https://img.shields.io/docker/pulls/neosmemo/memos?style=flat-square&logo=docker)](https://hub.docker.com/r/neosmemo/memos) + +Memos Demo Screenshot + +### 💎 Featured Sponsors + +[**Warp** — The AI-powered terminal built for speed and collaboration](https://go.warp.dev/memos) + + + Warp - The AI-powered terminal built for speed and collaboration + + +

+ +[**TestMu AI** - The world’s first full-stack Agentic AI Quality Engineering platform](https://www.testmuai.com/?utm_medium=sponsor&utm_source=memos) + + + TestMu AI + + +

+ +[**SSD Nodes** - Affordable VPS hosting for self-hosters](https://ssdnodes.com/?utm_source=memos&utm_medium=sponsor) + + + SSD Nodes + + +## Overview + +Memos is a privacy-first, self-hosted knowledge base for personal notes, team wikis, and knowledge management. Built with Go and React, it runs as a single binary with minimal resource usage. + +## Features + +- **Privacy-First** — Self-hosted on your infrastructure with zero telemetry, no tracking, and no ads. +- **Markdown Native** — Full markdown support with plain text storage. Your data is always portable. +- **Lightweight** — Single Go binary with a React frontend. Low memory footprint, starts in seconds. +- **Easy to Deploy** — One-line Docker install. Supports SQLite, MySQL, and PostgreSQL. +- **Developer-Friendly** — Full REST and gRPC APIs for integration with existing workflows. +- **Clean Interface** — Minimal design with dark mode and mobile-responsive layout. + +## Quick Start + +### Docker (Recommended) + +```bash +docker run -d \ + --name memos \ + -p 5230:5230 \ + -v ~/.memos:/var/opt/memos \ + neosmemo/memos:stable +``` + +Open `http://localhost:5230` and start writing! + +### Try the Live Demo + +Don't want to install yet? Try our [live demo](https://demo.usememos.com/) first! + +### Other Installation Methods + +- **Docker Compose** - Recommended for production deployments +- **Pre-built Binaries** - Available for Linux, macOS, and Windows +- **Kubernetes** - Helm charts and manifests available +- **Build from Source** - For development and customization + +See our [installation guide](https://usememos.com/docs/deploy) for detailed instructions. + +## Contributing + +Contributions are welcome — bug reports, feature suggestions, pull requests, documentation, and translations. + +- [Report bugs](https://github.com/usememos/memos/issues/new?template=bug_report.md) +- [Suggest features](https://github.com/usememos/memos/issues/new?template=feature_request.md) +- [Submit pull requests](https://github.com/usememos/memos/pulls) +- [Improve documentation](https://github.com/usememos/dotcom) +- [Help with translations](https://github.com/usememos/memos/tree/main/web/src/locales) + +## Sponsors + +Love Memos? [Sponsor us on GitHub](https://github.com/sponsors/usememos) to help keep the project growing! + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=usememos/memos&type=Date)](https://star-history.com/#usememos/memos&Date) + +## License + +Memos is open-source software licensed under the [MIT License](LICENSE). + +## Privacy Policy + +Memos is built with privacy as a core principle. As a self-hosted application, all your data stays on your infrastructure. There is no telemetry, no tracking, and no data collection. See our [Privacy Policy](https://usememos.com/privacy) for details. + +--- + +**[Website](https://usememos.com)** • **[Documentation](https://usememos.com/docs)** • **[Demo](https://demo.usememos.com/)** • **[Discord](https://discord.gg/tfPJa4UmAv)** • **[X/Twitter](https://x.com/usememos)** + + + Vercel OSS Program + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1818991 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,46 @@ +# Security Policy + +## Project Status + +Memos is currently in beta (v0.x). While we take security seriously, we are not yet ready for formal CVE assignments or coordinated disclosure programs. + +## Reporting Security Issues + +### For All Security Concerns: +Please report via **email only**: dev@usememos.com + +**DO NOT open public GitHub issues for security vulnerabilities.** + +Include in your report: +- Description of the issue +- Steps to reproduce +- Affected versions +- Your assessment of severity + +### What to Expect: +- We will acknowledge your report as soon as we can +- Fixes will be included in regular releases without special security advisories +- No CVEs will be assigned during the beta phase +- Credit will be given in release notes if you wish + +### For Non-Security Bugs: +Use GitHub issues for functionality bugs, feature requests, and general questions. + +## Philosophy + +As a beta project, we prioritize: +1. **Rapid iteration** over lengthy disclosure timelines +2. **Quick patches** over formal security processes +3. **Transparency** about our beta status + +We plan to implement formal vulnerability disclosure and CVE handling after reaching v1.0 stable. + +## Self-Hosting Security + +Since Memos is self-hosted software: +- Keep your instance updated to the latest release +- Don't expose your instance directly to the internet without authentication +- Use reverse proxies (nginx, Caddy) with rate limiting +- Review the deployment documentation for security best practices + +Thank you for helping improve Memos! diff --git a/arms_cache/config/arms.biz.trace.fit0dygksm-5deca4ebd39a6e7@@ap-southeast-1@@c845a7b4-23a1-4f28-a380-5ab30d8a280f b/arms_cache/config/arms.biz.trace.fit0dygksm-5deca4ebd39a6e7@@ap-southeast-1@@c845a7b4-23a1-4f28-a380-5ab30d8a280f new file mode 100644 index 0000000..e69de29 diff --git a/arms_cache/config/arms.converge.config.arms_agent.apm_1576452733059910.fit0dygksm_5deca4ebd39a6e7.system.config@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f b/arms_cache/config/arms.converge.config.arms_agent.apm_1576452733059910.fit0dygksm_5deca4ebd39a6e7.system.config@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f new file mode 100644 index 0000000..e69de29 diff --git a/arms_cache/config/arms.converge.config.arms_agent.apm_1576452733059910.fit0dygksm_5deca4ebd39a6e7@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f b/arms_cache/config/arms.converge.config.arms_agent.apm_1576452733059910.fit0dygksm_5deca4ebd39a6e7@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f new file mode 100644 index 0000000..d75eb86 --- /dev/null +++ b/arms_cache/config/arms.converge.config.arms_agent.apm_1576452733059910.fit0dygksm_5deca4ebd39a6e7@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f @@ -0,0 +1 @@ +{"id":3255,"version":1,"configs":{"raw_sql":{"pri":5,"over":true,"type":"unknown","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"th":50,"globalLimit":200,"ignWhenFail":false,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"raw_slow_sql":{"pri":5,"over":true,"type":"unknown","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"th":50,"globalLimit":200,"ignWhenFail":false,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false}},"appId":"fit0dygksm@5deca4ebd39a6e7","enable":true,"extraLabels":{}} \ No newline at end of file diff --git a/arms_cache/config/arms.converge.config.arms_agent.apm_1576452733059910.global@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f b/arms_cache/config/arms.converge.config.arms_agent.apm_1576452733059910.global@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f new file mode 100644 index 0000000..e69de29 diff --git a/arms_cache/config/arms.converge.config.arms_agent@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f b/arms_cache/config/arms.converge.config.arms_agent@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f new file mode 100644 index 0000000..ee88aa7 --- /dev/null +++ b/arms_cache/config/arms.converge.config.arms_agent@@arms.convergence.config@@c845a7b4-23a1-4f28-a380-5ab30d8a280f @@ -0,0 +1 @@ +{"templates":{"prpc":{"type":"url","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":400,"limit":200,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"method":{"type":"method","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":2000,"limit":1000,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"rpc":{"type":"other","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":2000,"limit":1000,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"dest_id":{"type":"ip","enable":true,"converge":true,"samplerCfg":{"enable":true,"th":100,"period":3600,"maxTh":5000,"maxCltSeconds":1800,"firstTh":1000},"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":100,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"slow_nosql":{"type":"sql","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"url":{"type":"url","enable":true,"converge":true,"samplerCfg":{"enable":true,"th":100,"period":3600,"maxTh":50000,"maxCltSeconds":1800,"firstTh":10000},"before":{"url_fmt":{"staticConfig":{"enable":true,"richResult":false,"postFixs":".mp3,.wav,.log,.gif,.css,.sh,.properties,.xlsx,.tar.bz2,.eot,.ppt,.7z,.pdf,.woff2,.txt,.yaml,.tar.gz,.svg,.gz,.rar,.jpg,.text,.zip,.jpeg,.png,.ttf,.tgz,.ico,.js,.word,.woff,.yml,.xml"},"paramedCfg":{"enable":true,"asOne":true},"atkCfg":{"enable":true},"mlCfg":{"enable":true,"mixedNumberLen":10,"wordLen":64,"numberLen":8},"type":"url_fmt","enable":true,"stopWhExclude":false}},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":2000,"limit":1000,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"sql":{"type":"sql","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"nosql":{"type":"sql","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"raw_sql":{"type":"sql","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"external_address":{"type":"ip","enable":true,"converge":true,"samplerCfg":{"enable":true,"th":100,"period":3600,"maxTh":5000,"maxCltSeconds":1800,"firstTh":1000},"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":200,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"endpoint":{"type":"ip","enable":true,"converge":true,"samplerCfg":{"enable":true,"th":100,"period":3600,"maxTh":5000,"maxCltSeconds":1800,"firstTh":1000},"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":200,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"external_url":{"type":"url","enable":true,"converge":true,"samplerCfg":{"enable":true,"th":100,"period":3600,"maxTh":10000,"maxCltSeconds":1800,"firstTh":2000},"before":{"url_fmt":{"staticConfig":{"enable":true,"richResult":false,"postFixs":".mp3,.wav,.log,.gif,.css,.sh,.properties,.xlsx,.tar.bz2,.eot,.ppt,.7z,.pdf,.woff2,.txt,.yaml,.tar.gz,.svg,.gz,.rar,.jpg,.text,.zip,.jpeg,.png,.ttf,.tgz,.ico,.js,.word,.woff,.yml,.xml"},"paramedCfg":{"enable":true,"asOne":true},"atkCfg":{"enable":true},"mlCfg":{"enable":true,"mixedNumberLen":4,"wordLen":32,"numberLen":8},"type":"url_fmt","enable":true,"stopWhExclude":false}},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":400,"limit":200,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"__default__":{"type":"other","enable":true,"converge":true,"samplerCfg":{"enable":true,"th":100,"period":3600,"maxTh":50000,"maxCltSeconds":1800,"firstTh":10000},"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":1000,"limit":500,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"raw_slow_nosql":{"type":"sql","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"raw_nosql":{"type":"sql","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"slow_sql":{"type":"sql","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"other_rpc":{"type":"other","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":600,"limit":300,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false},"raw_slow_sql":{"type":"sql","enable":true,"converge":true,"before":{},"after":{},"lmtCfg":{"wnd":3600,"delaySec":1800,"th":50,"globalLimit":200,"limit":100,"type":"s_lmt","enable":true,"stopWhExclude":true},"monRAW":false,"monRS":false}},"glReplaceConfig":{},"enable":true,"extraLabels":{}} \ No newline at end of file diff --git a/arms_cache/config/arms.trace.fit0dygksm-5deca4ebd39a6e7@@ap-southeast-1@@c845a7b4-23a1-4f28-a380-5ab30d8a280f b/arms_cache/config/arms.trace.fit0dygksm-5deca4ebd39a6e7@@ap-southeast-1@@c845a7b4-23a1-4f28-a380-5ab30d8a280f new file mode 100644 index 0000000..b0ee4fc --- /dev/null +++ b/arms_cache/config/arms.trace.fit0dygksm-5deca4ebd39a6e7@@ap-southeast-1@@c845a7b4-23a1-4f28-a380-5ab30d8a280f @@ -0,0 +1 @@ +{"otel":{"instrumentation":{"panic":{"enabled":true}}},"profiler":{"enable":true,"metrics":{"report":{"interval":15},"jvm":{"captureGcCause":false}},"quantile":{"enable":true},"exception":{"whitelist":"","filterByParentClass":false,"fromConstructor":{"enable":false},"fromInstrumentedMethod":{"enable":true},"advancedWhitelist":"[]","stacktrace":2},"SLS":{"regionId":"pub-cn-hangzhou-staging","bindType":"logstore","index":""},"compress":{"enable":false},"logging":{"injectTraceId2Log":{"enable":false},"injectSpanId2Log":{"enable":false},"enable":true},"agent":{"logger":{"level":"WARN"}},"thresholds":{"interface":500,"sql":500,"limit":1000},"error":{"skip":""},"param":{"maxLength":1024},"http":{"metrics":{"recordHttpCode":false}},"jdbc":{"tracesqlraw":false,"tracesqlbindvalue":false},"callsql":{"maxLength":1024},"threadpoolmonitor":{"enable":true},"cp":{"enable":true,"cpuEnable":true,"allocEnable":true,"wallClockEnable":false},"span":{"exporter":{"enable":true}},"sampling":{"rate":100,"useSamplingStrategyV2":false,"v2config":{"spanNames4FullSampleStr":"","spanNamePrefixes4FullSampleStr":"","spanNameSuffixes4FullSampleStr":""}},"trace":{"protocol":{"name":"W3C"}},"responseInject":{"enable":false},"defined":{"excludeurl":"SELECT,UPDATE,func cron,select"},"metricsAndSpan":{"entranceless":{"enable":false}},"dubbo":{"enable":true},"elasticsearch":{"enable":true},"grpc":{"enable":true},"liberty":{"enable":true},"mongodb":{"enable":true},"mysql":{"enable":true},"postgresql":{"enable":true},"redis":{"enable":true},"rabbitmq":{"client":{"enable":true}},"kafka":{"enable":true},"go":{"sampling":{"useSamplingStrategyV2":false,"rate":100,"v2config":{"spanNames4FullSampleStr":"","spanNamePrefixes4FullSampleStr":"","spanNameSuffixes4FullSampleStr":""}},"opentelemetry":{"enable":false},"opentracing":{"enable":false},"runtime":true,"cp":{"enable":true,"cpuEnable":true,"allocEnable":true,"wallClockEnable":false,"goroutineEnable":true,"blockEnable":false,"mutexEnable":false},"httpRequestBody":{"enable":false,"size":1024},"httpRequestHeader":{"enable":false,"key":""},"httpResponseBody":{"enable":false,"size":1024},"httpResponseHeader":{"enable":false,"key":""},"span":{"names":""},"exception":{"length":4096},"enable":true},"gin":{"enable":true},"kratos":{"enable":true},"fasthttp":{"enable":true},"restful":{"enable":false},"micro":{"enable":false},"iris":{"enable":true},"echo":{"enable":false},"mux":{"enable":false},"fiber":{"enable":true},"thrift":{"enable":true},"kitex":{"enable":true},"hertz":{"enable":true},"fc":{"enable":true},"gorm":{"enable":false},"k8sclient":{"enable":true},"nethttp":{"enable":true},"mcp":{"enable":true},"langchain":{"enable":true}}} \ No newline at end of file diff --git a/assets/1772551944_download.png b/assets/1772551944_download.png new file mode 100644 index 0000000..e4c18dd Binary files /dev/null and b/assets/1772551944_download.png differ diff --git a/docapi.txt b/docapi.txt new file mode 100644 index 0000000..fd4bfab --- /dev/null +++ b/docapi.txt @@ -0,0 +1,644 @@ +# Memos API Documentation + +## Overview +Memos is a self-hosted knowledge management platform with a RESTful API built using Protocol Buffers and gRPC. The API supports both gRPC and HTTP/JSON protocols. + +## Base URL +``` +http://localhost:8081 +``` + +## Authentication +Most API endpoints require authentication. Memos uses JWT-based authentication with short-lived access tokens. + +### Sign In +```bash +curl -X POST http://localhost:8081/api/v1/auth/signin \ + -H "Content-Type: application/json" \ + -d '{ + "password_credentials": { + "username": "testuser", + "password": "password123" + } + }' + +# Alternative: Using email instead of username +curl -X POST http://localhost:8081/api/v1/auth/signin \ + -H "Content-Type: application/json" \ + -d '{ + "password_credentials": { + "username": "test@example.com", + "password": "password123" + } + }' \ + -H "Content-Type: application/json" \ + -d '{ + "password_credentials": { + "username": "testuser", + "password": "password123" + } + }' +``` + +**Response:** +```json +{ + "user": { + "name": "users/1", + "role": "ADMIN", + "username": "testuser", + "email": "test@example.com", + "displayName": "", + "avatarUrl": "", + "description": "", + "state": "NORMAL", + "createTime": "2026-03-03T13:03:20Z", + "updateTime": "2026-03-03T13:03:20Z" + }, + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "accessTokenExpiresAt": "2026-03-03T13:20:35.055464409Z" +}``` +``` + +### Using Authentication Token +Include the access token in the Authorization header: +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/users + +# Example with actual token +curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJ0eXBlIjoiYWNjZXNzIiwicm9sZSI6IkFETUlOIiwic3RhdHVzIjoiTk9STUFMIiwidXNlcm5hbWUiOiJ0ZXN0dXNlciIsImlzcyI6Im1lbW9zIiwic3ViIjoiMSIsImF1ZCI6WyJ1c2VyLmFjY2Vzcy10b2tlbiJdLCJleHAiOjE3NzI1NDQwMzUsImlhdCI6MTc3MjU0MzEzNX0.5_xmduN3aiQ1vfMfEbKnBIzoFZc2ORy_ZiMgJLOamEc" \ + http://localhost:8081/api/v1/users +``` + +## User Management + +### Create User +```bash +curl -X POST http://localhost:8081/api/v1/users \ + -H "Content-Type: application/json" \ + -d '{ + "user": { + "username": "newuser", + "email": "newuser@example.com", + "password": "securepassword", + "role": "USER" + } + }' +``` + +### List Users +```bash +# Requires authentication +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/users + +# Example response: +{ + "users": [ + { + "name": "users/1", + "role": "ADMIN", + "username": "testuser", + "email": "test@example.com", + "displayName": "", + "avatarUrl": "", + "description": "", + "state": "NORMAL", + "createTime": "2026-03-03T13:03:20Z", + "updateTime": "2026-03-03T13:03:20Z" + } + ], + "nextPageToken": "", + "totalSize": 1 +} +``` + +### Get User +```bash +# By ID +curl http://localhost:8081/api/v1/users/1 + +# By username +curl http://localhost:8081/api/v1/users/newuser +``` + +### Update User +```bash +curl -X PATCH http://localhost:8081/api/v1/users/1 \ + -H "Content-Type: application/json" \ + -d '{ + "user": { + "name": "users/1", + "display_name": "New Display Name" + }, + "update_mask": "display_name" + }' +``` + +### Delete User +```bash +curl -X DELETE http://localhost:8081/api/v1/users/1 +``` + +## Memo Management + +### Create Memo +```bash +curl -X POST http://localhost:8081/api/v1/memos \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "memo": { + "content": "# My First Memo\n\nThis is a sample memo with **markdown** formatting.", + "visibility": "PRIVATE" + } + }' + +# Example with actual token +curl -X POST http://localhost:8081/api/v1/memos \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJ0eXBlIjoiYWNjZXNzIiwicm9sZSI6IkFETUlOIiwic3RhdHVzIjoiTk9STUFMIiwidXNlcm5hbWUiOiJ0ZXN0dXNlciIsImlzcyI6Im1lbW9zIiwic3ViIjoiMSIsImF1ZCI6WyJ1c2VyLmFjY2Vzcy10b2tlbiJdLCJleHAiOjE3NzI1NDQwMzUsImlhdCI6MTc3MjU0MzEzNX0.5_xmduN3aiQ1vfMfEbKnBIzoFZc2ORy_ZiMgJLOamEc" \ + -H "Content-Type: application/json" \ + -d '{ + "memo": { + "content": "# Welcome to Memos\n\nThis is my first memo created via API! 🚀\n\n## Features\n- Markdown support\n- Easy organization\n- Fast search", + "visibility": "PRIVATE" + } + }' +``` + +### List Memos +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + "http://localhost:8081/api/v1/memos?page_size=10" +``` + +### Get Memo +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/memos/1 +``` + +### Update Memo +```bash +curl -X PATCH http://localhost:8081/api/v1/memos/1 \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "memo": { + "name": "memos/1", + "content": "Updated content" + }, + "update_mask": "content" + }' +``` + +### Delete Memo +```bash +curl -X DELETE http://localhost:8081/api/v1/memos/1 \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +## Memo Comments + +### Create Comment +```bash +curl -X POST http://localhost:8081/api/v1/memos/1/comments \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "comment": { + "content": "This is a comment on the memo" + } + }' +``` + +### List Comments +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/memos/1/comments +``` + +## Memo Reactions + +### Add Reaction +```bash +curl -X POST http://localhost:8081/api/v1/memos/1/reactions \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "reaction_type": "HEART" + }' +``` + +### List Reactions +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/memos/1/reactions +``` + +## Attachments + +### Upload Attachment +First, upload the file: +```bash +curl -X POST http://localhost:8081/api/v1/attachments \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -F "file=@/path/to/your/file.jpg" \ + -F "type=image/jpeg" +``` + +### List Attachments +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/attachments +``` + +## Identity Providers (SSO) + +### List Identity Providers +```bash +curl http://localhost:8081/api/v1/identity-providers +``` + +### Create Identity Provider +```bash +curl -X POST http://localhost:8081/api/v1/identity-providers \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "identity_provider": { + "name": "identity-providers/1", + "type": "OAUTH2", + "identifier_filter": ".*@company.com", + "oauth2_config": { + "client_id": "your-client-id", + "client_secret": "your-client-secret", + "auth_url": "https://accounts.google.com/o/oauth2/auth", + "token_url": "https://oauth2.googleapis.com/token", + "user_info_url": "https://www.googleapis.com/oauth2/v2/userinfo", + "scopes": ["openid", "email", "profile"] + } + } + }' +``` + +## Instance Management + +### Get Instance Info +```bash +curl http://localhost:8081/api/v1/instances +``` + +### Update Instance Settings +```bash +curl -X PATCH http://localhost:8081/api/v1/instances/default \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "instance": { + "name": "instances/default", + "custom_style": "body { font-family: Arial, sans-serif; }" + }, + "update_mask": "custom_style" + }' +``` + +## User Settings + +### Get User Settings +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/users/1/settings +``` + +### Update User Settings +```bash +curl -X PATCH http://localhost:8081/api/v1/users/1/settings/GENERAL \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "setting": { + "name": "users/1/settings/GENERAL", + "general_setting": { + "locale": "en-US", + "theme": "dark", + "memo_visibility": "PRIVATE" + } + }, + "update_mask": "general_setting" + }' +``` + +## Personal Access Tokens + +### Create PAT +```bash +curl -X POST http://localhost:8081/api/v1/users/1/personalAccessTokens \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "description": "API access for automation", + "expires_in_days": 30 + }' +``` + +### List PATs +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/users/1/personalAccessTokens +``` + +### Delete PAT +```bash +curl -X DELETE http://localhost:8081/api/v1/users/1/personalAccessTokens/TOKEN_ID \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +## Webhooks + +### Create User Webhook +```bash +curl -X POST http://localhost:8081/api/v1/users/1/webhooks \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "webhook": { + "url": "https://your-webhook-endpoint.com/memos", + "display_name": "Memo Notifications" + } + }' +``` + +### List Webhooks +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/users/1/webhooks +``` + +## Activities + +### List Activities +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/activities +``` + +## Shortcuts + +### Create Shortcut +```bash +curl -X POST http://localhost:8081/api/v1/shortcuts \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "shortcut": { + "title": "Quick Meeting Notes", + "payload": "# Meeting Notes\n\n## Attendees\n\n## Agenda\n\n## Action Items" + } + }' +``` + +### List Shortcuts +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8081/api/v1/shortcuts +``` + +## Utility Endpoints + +### Health Check +```bash +curl http://localhost:8081/healthz +``` + +**Response:** `Service ready.` + +### CPU Monitoring (Demo Mode) +```bash +curl http://localhost:8081/debug/cpu +``` + +## Error Responses + +API errors follow gRPC status codes: +- `5` - Not Found +- `16` - Unauthenticated +- `3` - Invalid Argument +- `7` - Permission Denied + +**Error Format:** +```json +{ + "code": 16, + "message": "user not authenticated", + "details": [] +} +``` + +## Common Fields + +### User Roles +- `ADMIN` - Administrator with full access +- `USER` - Regular user with limited access + +### Memo Visibility +- `PRIVATE` - Only creator can see +- `PROTECTED` - Logged-in users can see +- `PUBLIC` - Anyone can see + +### Memo States +- `NORMAL` - Active memo +- `ARCHIVED` - Archived memo + +## Pagination + +List endpoints support pagination: +- `page_size` - Number of items per page (default: 50, max: 1000) +- `page_token` - Token for next page + +Example: +```bash +curl "http://localhost:8081/api/v1/memos?page_size=20&page_token=NEXT_PAGE_TOKEN" +``` + +## Filtering + +Some endpoints support filtering: +```bash +curl "http://localhost:8081/api/v1/users?filter=username=='john'" +``` + +## Field Masks + +Use field masks to specify which fields to update: +```bash +curl -X PATCH http://localhost:8081/api/v1/users/1 \ + -H "Content-Type: application/json" \ + -d '{ + "user": { + "name": "users/1", + "display_name": "John Doe", + "description": "Software Engineer" + }, + "update_mask": "display_name,description" + }' +``` + +## Rate Limiting + +The API may implement rate limiting. Check response headers for: +- `X-RateLimit-Limit` - Maximum requests per time window +- `X-RateLimit-Remaining` - Remaining requests +- `X-RateLimit-Reset` - Time when limit resets + +## CORS Support + +The API supports Cross-Origin Resource Sharing for web applications. + +## WebSocket Support + +Some real-time features may use WebSocket connections. + +## Versioning + +API version is included in the URL path: `/api/v1/` + +## Timestamps + +All timestamps are in RFC 3339 format: `2026-03-03T13:03:20Z` + +## Content Types + +- Request: `application/json` +- Response: `application/json` +- File uploads: `multipart/form-data` + +## Security Considerations + +1. Always use HTTPS in production +2. Store access tokens securely (not in localStorage) +3. Implement proper token refresh mechanisms +4. Validate all input data +5. Use appropriate CORS headers +6. Implement rate limiting +7. Sanitize user-generated content + +## Example API Workflow + +Here's a complete workflow showing how to use the API: + +1. **Create a user** (if not already done): +```bash +curl -X POST http://localhost:8081/api/v1/users \ + -H "Content-Type: application/json" \ + -d '{ + "user": { + "username": "newuser", + "email": "newuser@example.com", + "password": "securepassword", + "role": "USER" + } + }' +``` + +2. **Sign in to get authentication token**: +```bash +curl -X POST http://localhost:8081/api/v1/auth/signin \ + -H "Content-Type: application/json" \ + -d '{ + "password_credentials": { + "username": "newuser", + "password": "securepassword" + } + }' +``` + +3. **Use the token for subsequent requests**: +```bash +# Store the token from step 2 +TOKEN="your-access-token-here" + +# Create a memo +curl -X POST http://localhost:8081/api/v1/memos \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "memo": { + "content": "# My Daily Notes\n\n## Today's Tasks\n- [ ] Review PRs\n- [ ] Update documentation\n- [ ] Team meeting at 2 PM", + "visibility": "PRIVATE" + } + }' + +# List all memos +curl -H "Authorization: Bearer $TOKEN" \ + "http://localhost:8081/api/v1/memos?page_size=20" +``` + +## Troubleshooting + +**Common Issues:** + +1. **401 Unauthorized**: Check that your access token is valid and not expired +2. **404 Not Found**: Verify the endpoint URL and resource ID +3. **400 Bad Request**: Check request format and required fields +4. **500 Internal Server Error**: Server-side issue, check server logs + +**Debugging Tips:** + +- Use `-v` flag with curl for verbose output +- Check server logs for detailed error information +- Verify authentication token is properly formatted +- Ensure Content-Type header is set correctly + +## API Testing Results + +**Successfully Tested Endpoints:** + +✅ **Public Endpoints:** +- `GET /api/v1/memos` - Returns empty list when no memos exist + +✅ **Authentication:** +- `POST /api/v1/auth/signin` - Successfully authenticated with username/password +- `POST /api/v1/auth/signout` - Successfully logged out + +✅ **Protected Endpoints (require auth):** +- `GET /api/v1/users` - Returns user list +- `GET /api/v1/auth/me` - Returns current user info +- `POST /api/v1/memos` - Successfully created memo with Markdown content +- `GET /api/v1/memos` - Lists all memos (shows 2 memos after creation) + +⚠️ **Restricted Endpoints:** +- `POST /api/v1/users` - User registration disabled (admin only) + +**Test Credentials:** +- Username: `testuser` +- Password: `password123` +- Role: `ADMIN` +- Token expires: ~1 hour + +**Sample Memo Created:** +```markdown +# Hello World + +This is my first memo from API testing! + +- Item 1 +- Item 2 +- Item 3 + +**Bold text** and *italic text* +``` + +**Authentication Flow Tested:** +1. Sign in → Receive JWT token +2. Use token for authenticated requests +3. Access protected endpoints successfully +4. Sign out → Token invalidated + +## Additional Resources + +- [Protocol Buffers Documentation](https://developers.google.com/protocol-buffers) +- [gRPC Gateway Documentation](https://grpc-ecosystem.github.io/grpc-gateway/) +- [Memos GitHub Repository](https://github.com/usememos/memos) +- [Memos Official Documentation](https://usememos.com/docs) \ No newline at end of file diff --git a/frontend.txt b/frontend.txt new file mode 100644 index 0000000..ecfc47e --- /dev/null +++ b/frontend.txt @@ -0,0 +1,658 @@ +# Memos Frontend Documentation + +## Authentication and Middleware System + +### Frontend Authentication Architecture + +The frontend authentication system uses a combination of: + +1. **React Context** for state management +2. **Route-based guards** for access control +3. **Connect RPC interceptors** for API authentication +4. **Layout-based protection** for UI components + +### Core Authentication Components + +#### 1. Auth Context (`web/src/contexts/AuthContext.tsx`) + +```typescript +// Manages global authentication state +const AuthContext = createContext({ + currentUser: undefined, + userGeneralSetting: undefined, + isInitialized: false, + isLoading: true, + initialize: async () => {}, + logout: () => {}, + refetchSettings: async () => {} +}); + +// Provides authentication state to entire app +function AuthProvider({ children }: { children: React.ReactNode }) { + // Handles token initialization, user fetching, logout logic +} +``` + +#### 2. Route Protection System + +**Public Routes** (no auth required): + +- `/auth` - Authentication pages +- `/auth/signup` - User registration +- `/auth/callback` - OAuth callback +- `/explore` - Public explore page +- `/u/:username` - User profiles +- `/memos/:uid` - Individual memo details + +**Private Routes** (auth required): + +- `/` (root) - Main dashboard +- `/attachments` - File attachments +- `/inbox` - Notifications +- `/archived` - Archived memos +- `/setting` - User settings + +#### 3. Layout-Based Authentication Guards + +**RootLayout** (`web/src/layouts/RootLayout.tsx`): + +```typescript +const RootLayout = () => { + const currentUser = useCurrentUser(); + + useEffect(() => { + if (!currentUser) { + redirectOnAuthFailure(); // Redirects to /auth + } + }, [currentUser]); + + // Renders navigation and main content + return ( +
+ +
+ +
+
+ ); +}; +``` + +**Route Configuration** (`web/src/router/index.tsx`): + +```typescript +const router = createBrowserRouter([ + { + path: "/", + element: , // Handles instance initialization + children: [ + { + path: "/auth", // Public routes + children: [ + { path: "", element: }, + { path: "signup", element: }, + { path: "callback", element: } + ] + }, + { + path: "/", // Protected routes (wrapped in RootLayout) + element: , // Auth guard here + children: [ + { + element: , // Main app layout + children: [ + { path: "", element: }, + { path: "explore", element: } + // ... other protected routes + ] + } + ] + } + ] + } +]); +``` + +### API Authentication Interceptors + +#### Connect RPC Interceptor (`web/src/connect.ts`): + +```typescript +const authInterceptor: Interceptor = (next) => async (req) => { + const isRetryAttempt = req.header.get(RETRY_HEADER) === RETRY_HEADER_VALUE; + const token = await getRequestToken(); + setAuthorizationHeader(req, token); + + try { + return await next(req); + } catch (error) { + // Handle 401 Unauthorized + if (error.code === Code.Unauthenticated && !isRetryAttempt) { + try { + // Attempt token refresh + const newToken = await refreshAndGetAccessToken(); + setAuthorizationHeader(req, newToken); + req.header.set(RETRY_HEADER, RETRY_HEADER_VALUE); + return await next(req); + } catch (refreshError) { + redirectOnAuthFailure(); // Redirect to login + throw refreshError; + } + } + throw error; + } +}; +``` + +#### Token Management + +- **Storage**: Access tokens stored in memory (not localStorage) +- **Refresh**: Automatic token refresh on 401 errors +- **Expiration**: Proactive refresh on tab focus +- **Cleanup**: Tokens cleared on logout/auth failure + +### Authentication Flow + +#### 1. App Initialization (`web/src/main.tsx`) + +```typescript +// Early theme/locale setup to prevent flash +applyThemeEarly(); +applyLocaleEarly(); + +// Initialize auth and instance contexts + + + + + +``` + +#### 2. Authentication Check (`web/src/utils/auth-redirect.ts`) + +```typescript +export function redirectOnAuthFailure(): void { + const currentPath = window.location.pathname; + + // Allow public routes + if (isPublicRoute(currentPath)) return; + + // Redirect private routes to auth + if (isPrivateRoute(currentPath)) { + clearAccessToken(); + window.location.replace(ROUTES.AUTH); + } +} +``` + +#### 3. User Session Management + +- Token refresh on window focus +- Automatic logout on token expiration +- Context cleanup on logout +- Query cache invalidation + +### Key Security Features + +- **Token Storage**: In-memory only (security best practice) +- **Automatic Refresh**: Handles token rotation seamlessly +- **Route Guards**: Prevent unauthorized access to protected routes +- **Context Isolation**: Auth state managed centrally +- **Error Handling**: Graceful degradation on auth failures +- **Session Cleanup**: Complete state reset on logout + +### Related Files + +- `web/src/contexts/AuthContext.tsx` - Authentication state management +- `web/src/layouts/RootLayout.tsx` - Main authentication guard +- `web/src/utils/auth-redirect.ts` - Route protection logic +- `web/src/connect.ts` - API authentication interceptors +- `web/src/router/index.tsx` - Route configuration +- `web/src/main.tsx` - App initialization + +## Theme System Implementation + +### Vite Build Process for Themes + +The CSS themes are processed and built by **Vite** during the build process: + +1. **Vite Configuration** (`web/vite.config.mts`) + - Uses `@tailwindcss/vite` plugin for CSS processing + - Tailwind CSS v4 handles theme token compilation + - Themes are bundled during `pnpm build` process + +2. **CSS Processing Pipeline** + - Base styles imported in `web/src/index.css` + - Theme-specific CSS files located in `web/src/themes/` + - Vite processes `@theme inline` directives at build time + - Dynamic theme switching handled via JavaScript at runtime + +### Theme Architecture + +#### Core Theme Files + +- `web/src/index.css` - Main CSS entry point +- `web/src/themes/default.css` - Base theme with Tailwind token mappings +- `web/src/themes/default-dark.css` - Dark theme variables +- `web/src/themes/paper.css` - Paper-style theme +- `web/src/utils/theme.ts` - Theme loading and management logic + +#### How Themes Are Built + +##### 1. Build Time Processing (Vite + Tailwind) + +```css +/* In web/src/themes/default.css */ +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + /* ... other CSS variables */ +} +``` + +- Tailwind compiles these into static CSS classes +- Shared across all themes +- Optimized during Vite build process + +##### 2. Runtime Theme Switching + +- JavaScript dynamically injects theme CSS +- Uses `?raw` import to get CSS as string +- Injects `