Task management app with personal to-dos, team workspaces, team chat, and reminders.
Two parts: native Android app (app/) + Node.js/Express API (backend/).
Releases are published to GitHub Releases as signed APK/AAB (see docs/CI_CD.md).
| Part | Stack |
|---|---|
| Android | Kotlin 2.2.0, AGP 8.13.2, Gradle 8.13, Java 17, compileSdk/targetSdk 36, minSdk 24 |
| Android UI | Single MainActivity, Fragments, Navigation component (2 graphs), ViewBinding/DataBinding |
| Android data | Room (morp.db, v8, fallbackToDestructiveMigration), Retrofit 2.11 + Gson, socket.io-client 2.1.1, WorkManager, EncryptedSharedPreferences, Biometric, Glide, MPAndroidChart, Cloudinary Android 3.0.2 |
| Backend | Node.js (CI pins 20), Express 4, MySQL 8 (mysql2), Redis 5 (node-redis), Socket.IO 4, JWT (jsonwebtoken), bcryptjs, nodemailer (Gmail SMTP), Cloudinary, sharp, multer |
| CI/CD | GitHub Actions (ci.yml, release.yml), Dependabot, SemVer tags vX.Y.Z |
app/src/main/java/com/example/morp_prj/
MyApplication.kt # app entry: auth interceptor, session-expiry broadcast, channels, worker schedule
ui/ # MainActivity + ~30 fragments, adapters, bottom sheets (see docs/ARCHITECTURE.md)
ui/dashboard/ ui/teamChat/ # dashboard + team chat (note: teamChat/ files declare package ui.chat)
data/api/ # RetrofitClient + 6 ApiService interfaces
data/db/ # Room: AppDatabase + 5 DAOs + entities
data/model/ # ~32 DTO/entity classes
data/repository/ data/ # Auth/Task/Team/Notification/Encryption repositories, sync + realtime
data/remote/ # SocketManager
notifications/ # TaskDueSoonWorker, WorkScheduler, channels
security/ # AuthInterceptor, SecureTokenStorage, KeyManager, CryptoManager, SecurityChecker
utils/ # PreferenceManager, DateUtils, CloudinaryHelper
app/src/main/res/navigation/ # main_nav.xml (auth + bottom tabs), team_nav_graph.xml (team workspace)
backend/
server.js # entry: middleware, 6 route mounts, Redis + Socket.IO + due poller
routes/ controllers/ # auth, tasks, team, team-tasks, notifications, encryption
services/ # tokenService, redisOTPService, passwordResetService, realtime, taskDueScheduler, notificationService
models/ # thin MySQL wrappers (User, Team)
middleware/ # authenticateToken, validation, rateLimiter (imported, not applied), upload (legacy)
config/ # database.js (mysql2 pool), redis.js
database/ # schema.sql + incremental alters (Task table is created inline by controllers)
utils/ # helpers.js, emailService.js
test/helpers.test.js # node:test coverage for utils/helpers
.github/workflows/ # ci.yml (lint+unit+build, emulator UI test, backend check), release.yml (manual SemVer release)
scripts/generate-demo-keystore.sh # MOCK demo keystore generator (see docs/CI_CD.md before real release)
- Android Studio (or JDK 17 + Android SDK, compileSdk 36) for
app/. - Node.js 20 + MySQL 8 + Redis for
backend/. - No Android SDK is installed on every machine — local Gradle builds require one (
ANDROID_HOMEorlocal.propertieswithsdk.dir).
cd backend
npm ci
cp .env.example .env # then fill in DB_*, REDIS_*, JWT_*, OTP_*, SMTP_* (see .env.example)
# create MySQL database matching DB_NAME, apply backend/database/*.sql as needed
npm run dev # or: npm start (PORT defaults to 3001 if unset)
npm test # runs test/helpers.test.js- Open the repo root in Android Studio; let it sync Gradle 8.13.
- Point the app at the backend (Retrofit base URL) and set Cloudinary values in
local.properties(defaults are hard-coded demo values inapp/build.gradle.kts— replace before production). - Run the
appconfiguration on a device/emulator (API 24+).
| Suite | Command | Notes |
|---|---|---|
| Backend unit | cd backend && npm test |
node:test, no extra deps |
| Android unit | ./gradlew testDebugUnitTest |
includes TaskUiMapperTest |
| Android lint | ./gradlew lintDebug |
report: app/build/reports/lint-results-debug.html |
| Android instrumented | ./gradlew connectedDebugAndroidTest |
needs emulator; CI uses API 30 x86_64 |
- Every push/PR to
main/developrunsci.yml:android-check(lint → unit test → debug APK),android-ui-test(emulator, API 30),backend-check(syntax → test → audit warning-only). - Release is manual: Actions → Release → Run workflow on
main, pickpatch/minor/major. Full procedure, version rules (versionCode = MAJOR*10000 + MINOR*100 + PATCH, MINOR/PATCH < 100), secrets table, and branch-protection steps:docs/CI_CD.md(Vietnamese).
Base: http://host:PORT (PORT env, default 3001). Auth: Bearer access JWT (30 min default) + rotating refresh JWT.
| Area | Endpoints |
|---|---|
| Auth (public) | POST /api/auth/register(-init) verify-otp resend-otp otp-status login refresh logout forgot-password verify-reset-otp resend-reset-otp reset-password |
| Auth (Bearer) | GET /api/auth/profile user/:userId sessions, POST sessions/revoke, PUT profile, POST change-password cloudinary-signature |
| Tasks | POST /api/tasks/sync ({tasks[], deletedServerIds[]} → {idMap[]}), GET /api/tasks/ |
| Team | `POST /api/team/create |
| Team tasks | POST /api/team-tasks/, GET /api/team-tasks/team/:teamId .../assigned, PATCH .../:taskId/status, PUT/DELETE .../:taskId |
| Notifications | GET /api/notifications, `POST .../mark-read |
| Encryption | `POST /api/encryption/initialize |
| Realtime (Socket.IO) | JWT handshake; rooms user:<id>, team:<teamId>; joinTeam/leaveTeam/sendTeamMessage → receiveTeamMessage, notification* events |
backend/package.jsonscriptsetup-dbpoints tosetup-database.js, which does not exist in the repo.app/src/.../ui/teamChat/*.ktfiles declarepackage ...ui.chat(folder/package mismatch); nav graphs reference both spellings.- Cloudinary cloud name / API key / upload preset are hard-coded demo defaults in
app/build.gradle.kts. ci.ymlbackend audit step usescontinue-on-error: true(warning only, demo mode).middleware/rateLimiter.jsis imported by routes but not applied;middleware/upload.jsis legacy (profile uses Cloudinary URLs).