0. Эхлэх — локал ажиллуулах
$ git clone https://github.com/gerege-systems/nexus-mini && cd nexus-mini
$ psql -v owner_pw=… -v app_pw=… -v admin_pw=… -v auth_pw=… -f deploy/01-roles.sql # нэг удаа, superuser
$ cp .env.example backend/nexus-mini.env # 4 DB URL + ADMIN_EMAIL/NAME/PASSWORD + PORTAL_URL
$ make migrate && make serve # API :8084, анхны платформ админ env-ээс
$ cd frontend && pnpm install && cd .. && make web # portal :3020; make admin — :3021
$ docker compose up -d # эсвэл бүгд нэг коммандаар
Бүх команд Makefile-аар (make help). Push-ийн өмнө make check — linux build + vet + test + SDK-ийн хил. Хөгжүүлэлтийн төрлүүд:
- Модуль — бизнес функц (энэ репогийн apps/ эсвэл өөрийн репо); доорх §1–8
- Дистрибуц — өөрийн компанид nexus-mini ажиллуулах, модулиуд сонгох (nexus CLI)
- Интеграц — гадны систем nexus-mini-ээр нэвтрэх / nexus-mini Google, SSO-оор нэвтрэх (OIDC)
- Цөм — backend/internal, frontend, admin — PR-аар (доод хэсэг)
Хэн юу хариуцдаг вэ
| МОДУЛЬ | ПЛАТФОРМ |
|---|
| Permission-оо тунхаглана | Tenant тусгаарлалт (RLS) |
| Цэсээ зарлана | Нэвтрэлт, session |
| Route-уудаа бүртгэнэ | Суулгалт, хамаарлын шийдэл |
| Өөрийн хүснэгт, миграц | RBAC default оноолт, шалгалт |
| Бизнес логик | Audit гинж, app store |
| Хувилбар + манифест (make manifest) | Registry, гарын үсэг, nexus add/upgrade |
| — | Түдгэлзүүлэлт / зөвхөн-унших, impersonation, lockout |
Файлын бүтэц
Жижиг модуль нэг файлаас эхэлж болно; өсөхөөрөө ингэж хуваана:
backend/apps/<name>/
module.go модулийн ГЭРЭЭ: ID, permission, цэс, миграц, route↔permission холболт
types.go хүсэлт/хариултын struct + validation
handlers.go HTTP handler-ууд (нэг resource = нэг файл)
migrations/ модулийн goose миграцууд
ui/pages/ portal хуудсууд → build үед app/(portal)/<нэр>/ руу хуулагдана
ui/i18n.ts модулийн толь (en: {...}) — цөмийн толинд нэгдэнэ
Олон resource-тэй бол handler файлыг resource тус бүрээр салгана (organisation: departments.go, people.go; ui/pages/departments/, ui/pages/people/).
Алхамууд
1. Package үүсгэх
func (m *Module) ID() string { return "mn.yourorg.name" } // reverse-DNS, глобал давтагдашгүй
func (m *Module) ShortID() string { return "name" } // permission prefix + URL зам
func (m *Module) Name() string { return "Хүний нэр" }
func (m *Module) Version() string { return "1.0.0" }
// заавал биш — store/registry-ийн тайлбар кодоос (make manifest авдаг):
func (m *Module) Description() string { return "Юу хийдэг вэ…" }
func (m *Module) Publisher() string { return "your-org" }
Version нь semver; registry-д нийтлэх git tag нь v<Version>. Permission нэмсэн/өргөсгөсөн бол minor-оо өсгө — дистрибуц nexus upgrade хийхэд -approve асуух шалтгаан нь энэ.
2. Permission тунхаглах
func (m *Module) Permissions() []nexus.PermissionDefinition {
return []nexus.PermissionDefinition{
{Code: "name.read", DefaultRoles: []string{"manager", "user"}},
{Code: "name.manage", OwnScope: true, DefaultRoles: []string{"manager", "user:own"}},
}
}
Дүрмүүд (зөрчвөл бинари асахгүй):
- Код заавал
<ShortID>.-ээр эхэлнэ — өөр модулийн эрхийг булааж чадахгүй DefaultRoles нь суулгах үед хэн авахыг тунхагладаг: admin үргэлж бүгдийг авна, жагсаалтад бичсэн нь нэмж авна, "user:own" нь зөвхөн өөрийн мөрийн эрхDefaultRoles хоосон = зөвхөн admin (аюулгүй default)"role:own" бичихийн тулд permission OwnScope: true байх ёстой (үгүй бол panic). Runtime-д ч own_scope=false permission-д хэн ч «own» өгч чадахгүй- Нөөцөлсөн ShortID:
core api admin platform store apps developers login signup dashboard members roles audit settings org - Шинэ хувилбарт permission нэмбэл цөм асахдаа суусан tenant бүрийн admin-д (+DefaultRoles) автоматаар оноодог (backfill); байгаа кодод хүрэхгүй
3. Миграц
//go:embed migrations/*.sql
var migrations embed.FS
func (m *Module) Migrations() fs.FS { return migrations }
tenant_id uuid NOT NULL + RLS policy (app_tenant_id()) — жишээг devices-ээс хуулOwnScope ашиглах бол created_by uuid багана заавал- Бүх string баганад урттай хязгаар (varchar(n)) — задгай text хориотой
- Төгсгөлд нь
GRANT ... ON <table> TO nexus_app, nexus_admin (функцэд автомат GRANT байхгүй) - Өөр хүснэгт рүү FK (memberships, өөрийн мод) заавал same-tenant trigger-тэй — FK шалгалт RLS-ийг давдаг. Загвар:
apps/organisation/migrations/00002_same_tenant.sql - Апп role-д temp хүснэгт, users.password_hash, auth_* функцууд хаалттай — зориуд
Модуль бүр өөрийн goose хүснэгттэй (goose_<shortid>) тул цөм болон бусад модультай мөргөлдөхгүй.
4. Route-ууд
func (m *Module) RegisterRoutes(r chi.Router, deps nexus.Deps) {
h := &handler{deps: deps}
r.With(nexus.RequirePermission(deps.Perms, "name.read")).Get("/", h.list)
r.With(nexus.RequirePermission(deps.Perms, "name.manage")).Post("/", h.create)
}
Танд өгөгдөх r нь аль хэдийн хамгаалагдсан: /api/apps/<ShortID>/ дор байрладаг, нэвтрээгүй хүн 401, апп суулгаагүй tenant 403 авчихсан байдаг. Handler дотор:
nexus.TenantID(ctx), nexus.UserID(ctx) — хүсэлтийн identitynexus.Scope(ctx) — ScopeOwn бол query-дээ created_by шүүлт нэмdeps.DB — RLS context автоматаар тохирдог холболт; SQL-даа tenant_id = $1 гэж бас бичdeps.Audit.Record(ctx, ...) — чухал үйлдлээ audit гинжид бичnexus.JSON / Decode / Error / DBError — вэб туслахуудnexus.UUIDParam(w, r, "id") / nexus.IsUUID — зам/биеийн id-г DB-д хүргэхээс өмнө (буруу бол 400); string талбарын уртыг valid()-даа шалга- Түдгэлзүүлсэн байгууллага → 403, зөвхөн-унших → бичих 503: платформ RequireTenant-д хийнэ, модуль мэдэх шаардлагагүй; impersonated session-ийн audit-д impersonated_by автоматаар хавсарна
5. Цэс
func (m *Module) Menus() []nexus.MenuDefinition {
return []nexus.MenuDefinition{{
ID: "name.list", Label: "Монгол нэр", Labels: map[string]string{"en": "English"},
Path: "/name", Icon: "device", Order: 10,
}}
}
Path заавал /<ShortID> эсвэл /<ShortID>/… — өөр зам Register panic (portal-ийн middleware нийтийн замаас бусдыг хамгаалдаг, модуль тойрч чадахгүй). Icon нэр: components/icons.tsx.
6. UI хуудас (portal)
UI нь модулийн хавтаст амьдарна: ui/pages/ доторх хуудсууд build үед app/(portal)/<нэр>/ руу хуулагдана, ui/i18n.ts толь цөмийнхтэй нэгдэнэ — цөмийн frontend файлд гар хүрэхгүй. Бэлэн загвар (apps/devices/ui/pages/page.tsx, apps/organisation/ui).
// apps/name/ui/pages/page.tsx → /name (ui/pages/reports/page.tsx → /name/reports)
"use client";
export default function NamePage() {
const { me } = useShell(); // хэрэглэгч + permissions
const { t } = useT(); // хэл (mn/en)
const manage = me.permissions["name.manage"]; // undefined | "all" | "own"
// api.get(`/api/apps/name/`) — cookie автоматаар, 401 бол login руу
}
- Эрхээр UI-гаа нуу:
me.permissions["name.manage"] байхгүй бол товчоо бүү харуул (энэ нь UX — жинхэнэ хамгаалалт серверт) - «Өөрийн» scope-той хэрэглэгчид засах/устгах товчийг
created_by === me.user.id үед л харуулна - Цэсний icon нэрээ
components/icons.tsx-ийн map-д нэм (lucide icon) - Бэлэн загварууд:
card / table / btn / field / badge / modal — globals.css; амжилтад toast(...), текстэд t(...) - Толь:
ui/i18n.ts — { en: { "Төхөөрөмжүүд": "Devices" } } (түлхүүр нь монгол текст)
7. Бүртгэх ба асаах
// backend/apps/apps.go — бинарид орох модулиуд:
func All() []nexus.Module { return []nexus.Module{ devices.New(), name.New() } }
// frontend/modules.json — portal-д орох UI:
{ "short_id": "name", "ui": "../backend/apps/name/ui" }
$ make migrate && make serve # модуль store-д гарч ирнэ
8. Store-д нийтлэх
Манифест кодоос үүснэ: make manifest MOD=name > manifests/name.json — дараа нь nexus-registry репод PR илгээнэ; maintainer index.json-ийг Ed25519-ээр гарын үсэглэнэ. Код registry-д хадгалагдахгүй — go_module зам + git tag хангалттай. Орсны дараа хэн ч nexus add name гэж дистрибуцдаа нэмнэ. Өөрийн registry: репог хуулж, nexus-registry keygen → REGISTRY_URL + REGISTRY_KEYS.
Өөрийн дистрибуц — цөмийг fork хийхгүй
Өөрийн компанид nexus-mini ашиглаж, өөрийн модулиуд, өөрийн store, өөрийн харилцагчидтай (tenant) платформ ажиллуулж болно. Цөмийн репог хуулбарлаж засахгүй — хамаарал болгоно. Ингэж байж цөмийн шинэчлэлтийг merge-гүй, мөргөлдөөнгүй авна.
// nexus CLI — дистрибуц үүсгэх, модуль нэмэх (цөмийг fork хийхгүй):
$ go run github.com/gerege-systems/nexus-mini/backend/cmd/nexus@latest init my-dist
$ cd my-dist && go run github.com/gerege-systems/nexus-mini/backend/cmd/nexus@latest add organisation
$ make migrate && make serve
// backend/main.go (init үүсгэнэ; add маркер хооронд мөр нэмнэ):
func main() { core.Main(modules()...) }
// Цөмийг шинэчлэх — merge байхгүй, зөвхөн хувилбар:
$ go get github.com/gerege-systems/nexus-mini/backend@v1.5.0
$ git fetch upstream --tags && git checkout backend/v1.5.0 -- frontend
core.Main — migrate/serve/manifest коммандууд, env, миграц, анхны админ, permission sync, сервер: бүгд цөмд; та модулиудаа л өгнөnexus upgrade — модулийн шинэ хувилбарт permission нэмэгдсэн/өргөссөн бол зогсоож -approve шаардана; модуль чимээгүй эрх авахгүй- Frontend: цөмийн frontend-ийн хуулбар +
modules.json. Та цөмийн файлд гар хүрдэггүй (UI ui/-д, толь ui/i18n.ts-д) тул цөмийн frontend-ийг tag-аас хуулж дарахад мөргөлдөхгүй - SDK амлалт:
pkg/nexus + core.Main v1.x дотор эвдэхгүй (semver); internal/* чөлөөтэй өөрчлөгдөнө — модуль түүнээс импортолж чадахгүй - Цөмд алдаа олбол өөр дээрээ засахгүй — upstream руу PR. Харилцагч тань таны instance дээр tenant болно; та платформ админ
Гадны систем холбох — OIDC provider, SSO, federation
nexus-mini нь OpenID Connect provider (таны систем энэ платформын бүртгэлээр нэвтэрнэ) ба relying party (энэ платформ Google/өөр issuer-ээр нэвтэрнэ) хоёулаа. Хоёр nexus-mini хоорондоо = federation.
Таны систем nexus-mini-ээр нэвтрэх
- Portal → SSO клиентүүд (core.sso.manage) → Клиент нэмэх: нэр, redirect URI (https/localhost), scope. Confidential бол client_secret нэг л удаа харагдана; SPA/mobile бол Public (PKCE).
- Discovery:
<PORTAL_URL>/api/oauth2/.well-known/openid-configuration — дурын OIDC номын сан (openid-client, oidc-client-ts, Spring, NextAuth…) үүгээр бүх endpoint-ийг олно. - Урсгал: authorization_code + PKCE S256 заавал. Хэрэглэгч portal-д нэвтэрч, клиентийн байгууллагын гишүүн бол consent → code → /token. Зөвшөөрөл санагдана.
- Токен: access opaque (/introspect, /revoke), id_token RS256 (/jwks), offline_access → refresh (rotation, replay бол гэр бүлээр хүчингүй). Claims: sub, name, email, tenant (slug) + tenant_id, roles.
- Сервер-сервер: client_credentials → tenant scope-той access token. Гарах: end_session?id_token_hint=…
// 1. browser → authorize (PKCE):
<issuer>/authorize?response_type=code&client_id=…&redirect_uri=…&scope=openid%20profile%20email&state=…&nonce=…&code_challenge=…&code_challenge_method=S256
// 2. callback-ийн code-оор токен:
$ curl -u "$CLIENT_ID:$CLIENT_SECRET" -X POST <issuer>/token -d grant_type=authorization_code -d code=… -d redirect_uri=… -d code_verifier=…
// 3. шалгах / хүчингүй болгох:
$ curl -u … -X POST <issuer>/introspect -d token=… $ curl -u … -X POST <issuer>/revoke -d token=…
nexus-mini Google / өөр OIDC-ээр нэвтрэх (env)
GOOGLE_CLIENT_ID=… GOOGLE_CLIENT_SECRET=… # redirect: <PORTAL_URL>/api/auth/sso/google/callback
SSO_ISSUER=https://nexus.bold.mn/api/oauth2 SSO_CLIENT_ID=… SSO_CLIENT_SECRET=… SSO_NAME="Bold SSO" # federation
SSO_AUTO_SIGNUP=false # true: танигдаагүй имэйлд данс үүсгэнэ (JIT)
Login хуудсанд товч гарна; PKCE + state + nonce, id_token-ийг issuer-ийн JWKS-ээр шалгана. Дэлгэрэнгүй: docs/04-integrations.md.
Аюулгүй байдлын дүрэм — модуль, цөм хоёуланд
- Эрх зөвхөн серверт: route бүр RequirePermission; UI-гийн нуулт нь UX. Мөрийн түвшин — RLS + query-дээ tenant_id, own scope бол created_by.
- SQL үргэлж параметртэй, төрлийн cast-тай ($1::uuid). Бүх string багана varchar(n); FK бусад хүснэгт рүү → same-tenant trigger.
- Алдааг клиентэд түүхийгээр нь буцаахгүй (DBError/Error): 23505 → 409, бусад → лог + ерөнхий 500. Буруу uuid → 400 (UUIDParam).
- Нууц зүйл (токен, нууц үг, session) лог/audit-д бичихгүй. Client secret argon2-оор hash-лагдсан; OAuth токен sha256 hash-аар хадгалагдана.
- Апп DB role-д: temp хүснэгт үгүй, auth_* функц үгүй, users.password_hash үгүй, tenants төлөв багана үгүй — модуль эдгээрт хүрч чадахгүй, хүрэх гэж бүү оролд.
- Cookie-тэй бичих хүсэлт Origin + Sec-Fetch-Site шалгалттай; гадны домэйноос дуудах endpoint бол токен-аар танигддаг байх (OAuth2 загвар).
- Render дотор window/localStorage/matchMedia уншихгүй (hydration) — useEffect-д. Шалгахдаа dark/light хоёуланг.
Цөмд хувь нэмэр оруулах
- Бүтэц:
backend/internal/core/* (цөмийн дотоод — чөлөөтэй өөрчлөгдөнө), backend/pkg/nexus + backend/core.Main (SDK — v1.x-д эвдэхгүй, эвдэх бол major), backend/pkg/registry (registry гэрээ), frontend/, admin/. - Миграц: backend/db/migrations/000NN_*.sql (goose, Up/Down хоёулаа); definer функц бүр SET search_path = pg_catalog, public, pg_temp + REVOKE FROM PUBLIC + тодорхой GRANT.
- Тест: unit (go test) + env-гэйт integration (NEXUS_TEST_DATABASE_URL*); RLS/RBAC өөрчлөлтөд integration заавал. make check push бүрийн өмнө.
- Баримт: docs/00-decisions.md-д шийдвэрээ, docs/03 ↔ энэ хуудас синк, CLAUDE.md-ийн invariant-ууд.
- PR: жижиг, нэг зорилготой; commit-д «яагаад». Алдаа олбол fork-доо биш upstream-д.
Тест · Deploy
SQL parse/encode бүх логикт unit тест бич. make check нь linux cross-build + vet + test + SDK-ийн хилийн шалгалт (модуль internal/* импортолбол унадаг) — push бүрийн өмнө заавал. Deploy: сервер дээр git pull → deploy/deploy.sh (make migrate ENV_FILE=… + атом бинари/Next солилт + systemd restart); unit/nginx өөрчлөлт гараар. Docker: docker-compose.yml. Production: ENVIRONMENT=production, PORTAL_URL https, nexus_auth role, ADMIN_* env-ээс анхны админ үүссэний дараа устга.