diff --git a/.dockerignore b/.dockerignore index e4dd9af..62c5264 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,6 @@ data .env *.log .git +.github +*.md +compose*.yaml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c52ff9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e2d7a31 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.png binary +*.ico binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..611d430 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,40 @@ +name: Bug Report +description: Report something that is not working as expected +labels: [bug] +body: + - type: textarea + id: description + attributes: + label: Description + description: What happened, and what did you expect to happen? + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + placeholder: | + 1. Go to ... + 2. Click on ... + 3. See error + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Image tag or commit hash + placeholder: ghcr.io/onebacklog/onearctic:2.2.0 + validations: + required: true + - type: input + id: browser + attributes: + label: Browser and Device + placeholder: Firefox 130 on Android, installed as PWA + - type: textarea + id: logs + attributes: + label: Logs + description: Relevant output from `docker compose logs app` or the browser console + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..83ce661 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Report a Security Vulnerability + url: https://github.com/OneBacklog/OneArctic/blob/master/SECURITY.md + about: Please report security issues privately by email, not as a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..192cad4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,20 @@ +name: Feature Request +description: Suggest an idea or improvement +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What problem would this solve? Describe your use case. + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: How do you imagine it working? + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a1d8ba4 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## Summary + + + +## How Was This Tested? + + + +## Checklist + +- [ ] `npm run lint` passes +- [ ] `npm run typecheck` passes +- [ ] `npm run build` passes +- [ ] Migration generated and committed (if `server/database/schema.ts` changed) +- [ ] Docs updated (if behavior or configuration changed) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..7d96a98 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Lint, Typecheck and Build + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install Dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Typecheck + run: npm run typecheck + + - name: Build + run: npm run build diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 149d18d..4b28d73 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -37,6 +37,9 @@ jobs: type=semver,pattern={{major}} type=raw,value=latest + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -44,6 +47,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..35b8e57 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +**hello@irfanfadilah.com**. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..835b0a0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,56 @@ +# Contributing to OneArctic + +Thanks for your interest in contributing! Bug reports, feature ideas and pull requests are all welcome. + +## Reporting Bugs and Requesting Features + +- Search [existing issues](https://github.com/OneBacklog/OneArctic/issues) first. +- Use the issue templates and include your version (image tag or commit) and steps to reproduce. +- For security issues, follow [SECURITY.md](./SECURITY.md) instead of opening a public issue. + +## Development Setup + +See [DEVELOPMENT.md](./DEVELOPMENT.md) for running the app with Docker or directly with Node.js. + +## Project Structure + +``` +app/ Nuxt frontend (components, composables, pages) +server/ Nitro backend (API routes, middleware, database, services) +shared/ Types and helpers used by both app/ and server/ +scripts/ Standalone CLI scripts (e.g. OTP setup) +``` + +## Pull Requests + +1. Fork the repo and create a branch from `master`. +2. Keep pull requests focused on one change. +3. If you change `server/database/schema.ts`, generate a migration with `npm run db:generate -- --name=` and commit it. +4. Make sure these pass locally before opening the PR: + + ```bash + npm run lint + npm run typecheck + npm run build + ``` + +5. Fill in the pull request template and describe how you tested the change. + +## Commit Messages + +Use a short, imperative, Title Case subject line, for example: + +``` +Add Label Reordering +Fix Upload Size Check +``` + +## Code Style + +- TypeScript everywhere; avoid `any` (the linter enforces this). +- Match the style of the surrounding code. Run `npm run lint:fix` to auto-fix what ESLint can. +- Put types and constants used by both the client and the server in `shared/`. + +## License + +By contributing, you agree that your contributions will be licensed under the [AGPL-3.0](./LICENSE) license. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index c673c85..f5297a6 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,6 +1,9 @@ +# Development + ## Requirements -- Docker and Docker Compose v2 +- Docker and Docker Compose v2, **or** +- Node.js 24 (see `.nvmrc`) and a running Meilisearch instance ## Development (Docker) @@ -45,6 +48,50 @@ docker compose -f compose.dev.yaml logs -f app docker compose -f compose.dev.yaml down ``` +## Development (Node.js) + +1. Install dependencies: + +```bash +npm install +``` + +2. Start Meilisearch: + +```bash +docker run -d --name onearctic-meili -p 7700:7700 \ + -e MEILI_MASTER_KEY=dev-master-key -e MEILI_NO_ANALYTICS=true \ + getmeili/meilisearch:v1.38.2 +``` + +3. Create `.env` from `.env.example` with local paths: + +```bash +JWT_SECRET= +MEILI_MASTER_KEY=dev-master-key +COOKIE_SECURE=false +DATABASE_PATH=./data/db/onearctic.db +FILES_PATH=./data/files +MEILISEARCH_HOST=http://localhost:7700 +``` + +4. Start the dev server, then create your OTP secret in another terminal: + +```bash +npm run dev +node --env-file=.env scripts/setup-auth.mjs +``` + +## Checks + +Run these before opening a pull request (CI runs them too): + +```bash +npm run lint # ESLint (use lint:fix to auto-fix) +npm run typecheck # vue-tsc +npm run build # production build +``` + ## Database Migrations Migrations in `server/database/migrations/` run automatically on startup. @@ -52,11 +99,11 @@ Migrations in `server/database/migrations/` run automatically on startup. Generate a migration after editing `server/database/schema.ts`: ```bash -npx drizzle-kit generate --name= +npm run db:generate -- --name= ``` Open the DB inspector: ```bash -npx drizzle-kit studio +npm run db:studio ``` diff --git a/Dockerfile b/Dockerfile index 984f44c..cca957f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,14 +17,21 @@ WORKDIR /app ENV NODE_ENV=production LABEL org.opencontainers.image.title="OneArctic" \ org.opencontainers.image.description="Personal Note-Taking App" \ - org.opencontainers.image.authors="Irfan Fadiilah " + org.opencontainers.image.authors="Irfan Fadilah " \ + org.opencontainers.image.source="https://github.com/OneBacklog/OneArctic" \ + org.opencontainers.image.licenses="AGPL-3.0-only" -RUN mkdir -p /app/data/db /app/data/files +RUN mkdir -p /app/data/db /app/data/files && chown -R node:node /app/data COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/.output ./.output COPY --from=build /app/server/database/migrations ./server/database/migrations COPY scripts/ ./scripts/ +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/api/health').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))" + +ENTRYPOINT ["docker-entrypoint.sh"] CMD ["node", ".output/server/index.mjs"] diff --git a/README.md b/README.md index 50326ee..442e7f7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,22 @@ # OneArctic +[![CI](https://github.com/OneBacklog/OneArctic/actions/workflows/ci.yaml/badge.svg)](https://github.com/OneBacklog/OneArctic/actions/workflows/ci.yaml) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](./LICENSE) + OneArctic is a self-hosted and single-user Google Keep-style note app. Built with Nuxt, SQLite, and Meilisearch. +## Features + +- Text and checklist notes in a masonry grid +- Labels with drag-and-drop ordering +- Archive and trash +- Full-text search powered by Meilisearch +- Image and file attachments with thumbnails and a lightbox +- Real-time sync across open tabs and devices +- Installable as a PWA +- Login with a TOTP authenticator app (no password to leak) +- All data stored locally in SQLite and plain files + ## Screenshots TBA @@ -25,9 +40,8 @@ cd OneArctic APP_ENV=production bash setup.sh ``` -### IMPORTANT! - -It is recommended to serve the app over HTTPS, but if somehow you don't, change `COOKIE_SECURE` to `false` in `.env` file. +> [!IMPORTANT] +> It is recommended to serve the app over HTTPS. If you don't, change `COOKIE_SECURE` to `false` in the `.env` file. 3. Start the services: @@ -35,10 +49,6 @@ It is recommended to serve the app over HTTPS, but if somehow you don't, change docker compose up -d ``` -### REVERSE PROXY! - -See [compose.traefik.yaml](./compose.traefik.yaml) for Traefik compose file example. - The app will be available at `http://localhost:3000`. 4. After the app is running, generate your OTP secret: @@ -53,6 +63,10 @@ docker compose run --rm setup-auth docker compose run --rm -e RESET=1 setup-auth ``` +### Reverse Proxy + +See [compose.traefik.yaml](./compose.traefik.yaml) for a Traefik example. + ## Configuration (Optional) The app reads secrets from `.env` (created by `setup.sh`). @@ -78,7 +92,17 @@ data/ meili/ — Meilisearch Index ``` -Back up the entire `data/` directory to migrate your instance. +## Backup and Restore + +Everything lives in `data/`, so a backup is a copy of that directory. Stop the app first so the SQLite database is in a consistent state: + +```bash +docker compose stop app +tar czf onearctic-backup-$(date +%F).tar.gz data/ +docker compose start app +``` + +To restore, stop the services, replace `data/` with the contents of the archive, and start them again. Keep `data/meili/` in the backup too: without it, search only finds notes edited after the restore. ## Update @@ -87,6 +111,10 @@ docker compose pull docker compose up -d ``` -## Local Development +## Contributing + +See [DEVELOPMENT.md](./DEVELOPMENT.md) for local setup and [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. Please report security issues privately as described in [SECURITY.md](./SECURITY.md). + +## License -See [DEVELOPMENT.md](./DEVELOPMENT.md) +OneArctic is licensed under the [GNU Affero General Public License v3.0](./LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7061624 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,28 @@ +# Security Policy + +## Supported Versions + +Only the latest release receives security fixes. Please make sure you are running the latest image (`ghcr.io/onebacklog/onearctic:latest`) before reporting. + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Email **hello@irfanfadilah.com** with: + +- A description of the vulnerability and its impact +- Steps to reproduce, or a proof of concept +- The affected version (image tag or commit) + +You should receive a reply within 7 days. Once the issue is confirmed, a fix will be prepared and released, and you will be credited in the release notes unless you prefer to stay anonymous. + +## Scope + +OneArctic is a self-hosted, single-user app. Reports are especially welcome for: + +- Authentication or session bypass +- Access to notes or attachments without a valid session +- Stored XSS or code execution through notes or uploaded files +- Path traversal or arbitrary file read/write + +Issues that require an already-authenticated session to attack your own data, or that depend on a misconfigured deployment (for example running without HTTPS), are generally out of scope. diff --git a/app/components/attachment/FileList.vue b/app/components/attachment/FileList.vue index e2bfdb3..3aad163 100644 --- a/app/components/attachment/FileList.vue +++ b/app/components/attachment/FileList.vue @@ -93,6 +93,7 @@ diff --git a/app/components/auth/PinLogin.vue b/app/components/auth/PinLogin.vue index 1a185dd..693ecf2 100644 --- a/app/components/auth/PinLogin.vue +++ b/app/components/auth/PinLogin.vue @@ -108,18 +108,18 @@ const submitIfComplete = async () => { try { await login(code.value) attemptsLeft.value = null - } catch (e: any) { - const status = e?.status ?? e?.statusCode - const data = e?.data?.data ?? e?.data + } catch (e) { + const status = getErrorStatus(e) + const data = getErrorData<{ secondsLeft: number; attemptsLeft: number }>(e) if (status === 429) { code.value = '' - startCooldown(data?.secondsLeft ?? 60) + startCooldown(data.secondsLeft ?? 60) } else if (status === 404) { error.value = 'OTP Not Configured' } else { error.value = 'Wrong OTP. Try again.' - attemptsLeft.value = data?.attemptsLeft ?? null + attemptsLeft.value = data.attemptsLeft ?? null shake.value = true setTimeout(() => { shake.value = false diff --git a/app/components/note/Checklist.vue b/app/components/note/Checklist.vue index 033c5e8..d8f5ec3 100644 --- a/app/components/note/Checklist.vue +++ b/app/components/note/Checklist.vue @@ -73,6 +73,7 @@ diff --git a/app/components/note/NoteModal.vue b/app/components/note/NoteModal.vue index 105813e..4971651 100644 --- a/app/components/note/NoteModal.vue +++ b/app/components/note/NoteModal.vue @@ -118,6 +118,7 @@