Major updates, authentication, multi-users

This commit is contained in:
blaisadmin
2026-04-07 23:20:00 -04:00
parent 2aff57ee7f
commit 6de323d487
6 changed files with 3035 additions and 231 deletions
+50 -4
View File
@@ -4,19 +4,28 @@ FlockPal is a Dockerized TypeScript app for tracking flock health with a clean,
## Current scope
- Passwordless authentication only
- Magic-link email sign-in
- OAuth-ready login flow for Google, Microsoft, and Apple
- Multi-workspace model with `standard` household and `rescue` modes
- Shared workspace member management for both households and rescues
- Separate per-workspace billing plan foundation with `rescue_free`, `household_basic`, and `household_plus`
- Bird profiles with name, tag ID, and species
- Bird DOB and gotcha day fields
- Daily weight recordings
- 30-day weight graph
- Vet visit history with notes
- Postgres-backed storage
- React frontend and Express backend
- Security-minded defaults like Helmet, CORS allow-listing, rate limiting, and input validation
## Planned next steps
- Vet visit history
- Medication and care reminders
- Accounts, authorization, and role-based rescue access
- Billing and plan management for paid organizations with a free rescue tier
- Invitation acceptance and onboarding polish for workspace members
- Stripe or equivalent billing integration for paid household tiers
- Scheduled reminder delivery for birthdays, gotcha days, and care events
- Audit logging for workspace access changes and bird transfers
## Run locally
@@ -30,6 +39,43 @@ docker compose up --build
3. Open `http://localhost:3000`.
4. The API health check is available at `http://localhost:5000/api/health`.
## Auth and workspace notes
- One user can belong to multiple workspaces.
- A rescue member can also keep their own household flock in a separate workspace.
- Billing should attach to the household workspace, not the user account.
- Rescue workspaces stay on the free plan.
- Shared access is controlled by workspace roles like `owner`, `manager`, `staff`, and `viewer`.
- FlockPal no longer stores local passwords.
- Authentication now happens through magic links or external identity providers.
## OAuth environment
Set these in Docker or your `.env` file if you want provider login enabled:
- `FRONTEND_URL`
- `BACKEND_URL`
- `GOOGLE_CLIENT_ID`
- `GOOGLE_CLIENT_SECRET`
- `MICROSOFT_CLIENT_ID`
- `MICROSOFT_CLIENT_SECRET`
- `APPLE_CLIENT_ID`
- `APPLE_CLIENT_SECRET`
## Magic-link email environment
Set these if you want magic links delivered by email instead of logged as a preview URL during local development:
- `SMTP_HOST`
- `SMTP_PORT`
- `SMTP_SECURE`
- `SMTP_USER`
- `SMTP_PASS`
- `SMTP_FROM_EMAIL`
- `SMTP_FROM_NAME`
## Notes for monetization and security
This starter keeps the data model and deployment simple, but it is intentionally shaped so we can add authentication, organization scoping, audit trails, reminders, and Stripe-style billing later without redesigning the whole app.
This starter now includes the account and workspace foundation for monetization, but it still needs production-grade session hardening, invitation verification, billing integration, audit logging, and background reminder delivery before launch.
For account design, `standard` vs `rescue` is best treated as a workspace type, not as a user role. If paid plans are added later, a separate `admin account mode` is usually less flexible than workspace roles such as `owner`, `manager`, `staff`, and `viewer`. That lets the same underlying account system work for both households and rescues without splitting product logic into unrelated account classes.
+20 -2
View File
@@ -8,12 +8,14 @@
"name": "flockpal-backend",
"version": "0.1.0",
"dependencies": {
"@types/nodemailer": "^8.0.0",
"cors": "2.8.5",
"dotenv": "16.4.5",
"express": "4.21.2",
"express-rate-limit": "7.5.0",
"helmet": "8.1.0",
"morgan": "1.10.0",
"nodemailer": "^8.0.5",
"pg": "8.13.1",
"zod": "3.24.1"
},
@@ -513,12 +515,20 @@
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/nodemailer": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz",
"integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/pg": {
"version": "8.11.10",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
@@ -1239,6 +1249,15 @@
"node": ">= 0.6"
}
},
"node_modules/nodemailer": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
"integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -1808,7 +1827,6 @@
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},
"node_modules/unpipe": {
+2
View File
@@ -9,12 +9,14 @@
"start": "node dist/app.js"
},
"dependencies": {
"@types/nodemailer": "^8.0.0",
"cors": "2.8.5",
"dotenv": "16.4.5",
"express": "4.21.2",
"express-rate-limit": "7.5.0",
"helmet": "8.1.0",
"morgan": "1.10.0",
"nodemailer": "^8.0.5",
"pg": "8.13.1",
"zod": "3.24.1"
},
+1433 -140
View File
File diff suppressed because it is too large Load Diff
+1306 -78
View File
File diff suppressed because it is too large Load Diff
+224 -7
View File
@@ -102,6 +102,12 @@ textarea {
align-items: start;
}
.auth-shell {
max-width: 1180px;
margin: 0 auto;
padding: 2rem;
}
.content-shell {
display: grid;
gap: 1.5rem;
@@ -114,6 +120,120 @@ textarea {
gap: 1.25rem;
}
.auth-panel {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 1.5rem;
align-items: start;
}
.auth-hero-card {
min-height: 280px;
align-items: end;
}
.auth-copy,
.auth-card {
display: grid;
gap: 1rem;
}
.auth-card {
padding: 1.25rem;
border-radius: 24px;
background: linear-gradient(180deg, rgba(255, 252, 247, 0.9), rgba(240, 248, 244, 0.82));
border: 1px solid rgba(39, 105, 179, 0.12);
}
.auth-tabs {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.auth-provider-stack {
display: grid;
gap: 0.75rem;
}
.provider-button {
text-decoration: none;
display: grid;
grid-template-columns: 46px 1fr;
align-items: center;
gap: 0.9rem;
border: 1px solid rgba(39, 105, 179, 0.12);
border-radius: 18px;
padding: 0.85rem 0.95rem;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 12px 24px rgba(39, 105, 179, 0.08);
color: var(--ink);
}
.provider-button-mark {
width: 46px;
height: 46px;
border-radius: 14px;
display: grid;
place-items: center;
background: #ffffff;
border: 1px solid rgba(31, 42, 42, 0.08);
color: #1f2328;
}
.provider-button-copy {
display: grid;
gap: 0.15rem;
}
.provider-button-copy small {
color: var(--muted);
}
.provider-icon-svg {
width: 22px;
height: 22px;
display: block;
}
.provider-icon-apple {
width: 20px;
height: 20px;
}
.provider-google {
background: #ffffff;
border-color: rgba(66, 133, 244, 0.18);
}
.provider-microsoft {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(246, 250, 255, 0.92));
border-color: rgba(0, 164, 239, 0.18);
}
.provider-apple {
background: linear-gradient(180deg, #1f2328, #111418);
border-color: rgba(17, 20, 24, 0.92);
color: #f7f7f5;
}
.provider-apple .provider-button-mark {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.12);
color: #f7f7f5;
}
.provider-apple .provider-button-copy small {
color: rgba(247, 247, 245, 0.76);
}
.provider-button.disabled {
opacity: 0.5;
}
.provider-button:not(.disabled):hover {
transform: translateY(-1px);
box-shadow: 0 16px 28px rgba(39, 105, 179, 0.12);
}
.stack-grid {
display: grid;
gap: 1.5rem;
@@ -202,8 +322,9 @@ textarea {
}
.hero-stats {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: minmax(220px, 280px);
align-self: end;
justify-content: end;
}
.hero-stats article,
@@ -228,8 +349,7 @@ textarea {
}
.hero-stats article::before,
.bird-card::before,
.chart-card::before {
.bird-card::before {
content: "";
display: block;
height: 5px;
@@ -286,6 +406,42 @@ textarea {
flex-wrap: wrap;
}
.workspace-switcher {
display: grid;
gap: 0.9rem;
}
.workspace-switcher-header {
display: grid;
gap: 0.2rem;
}
.workspace-switcher-header small,
.workspace-switcher-item small {
color: var(--muted);
}
.workspace-switcher-list {
display: grid;
gap: 0.75rem;
}
.workspace-switcher-item {
width: 100%;
text-align: left;
border: 1px solid rgba(39, 105, 179, 0.12);
border-radius: 18px;
padding: 0.85rem 0.95rem;
background: rgba(255, 255, 255, 0.58);
display: grid;
gap: 0.2rem;
}
.workspace-switcher-item.active {
background: linear-gradient(135deg, rgba(203, 58, 53, 0.16), rgba(39, 105, 179, 0.18));
border-color: rgba(39, 105, 179, 0.24);
}
.bird-list {
display: grid;
gap: 0.9rem;
@@ -298,6 +454,16 @@ textarea {
align-items: center;
}
.bird-card-copy {
display: grid;
gap: 0.25rem;
}
.bird-card-copy span {
display: block;
font-weight: 600;
}
.bird-avatar,
.profile-photo {
width: 56px;
@@ -360,10 +526,6 @@ textarea {
overflow: hidden;
}
.chart-card::before {
margin-bottom: 1rem;
}
.overview-chart-card::before {
display: none;
}
@@ -516,6 +678,23 @@ label {
font-weight: 600;
}
.toggle-card {
display: grid;
gap: 0.45rem;
padding: 1rem;
border-radius: 20px;
background: linear-gradient(180deg, rgba(255, 252, 247, 0.9), rgba(240, 248, 244, 0.82));
border: 1px solid rgba(39, 105, 179, 0.1);
}
.toggle-card input[type="checkbox"] {
width: 20px;
height: 20px;
margin-top: 0.15rem;
padding: 0;
accent-color: var(--accent-green);
}
.primary-button {
border: 0;
border-radius: 18px;
@@ -616,11 +795,48 @@ label {
justify-content: center;
}
.crop-preview-frame {
width: 112px;
height: 112px;
position: relative;
overflow: hidden;
border-radius: 28px;
border: 1px solid rgba(39, 105, 179, 0.16);
background: rgba(255, 255, 255, 0.86);
cursor: grab;
touch-action: none;
user-select: none;
}
.crop-preview-image {
position: absolute;
object-fit: cover;
pointer-events: none;
}
.crop-preview-overlay {
position: absolute;
inset: 0;
border: 2px solid rgba(255, 255, 255, 0.78);
border-radius: 28px;
box-shadow: inset 0 0 0 999px rgba(31, 42, 42, 0.08);
pointer-events: none;
}
.crop-preview-frame.dragging {
cursor: grabbing;
}
.photo-copy {
display: grid;
gap: 0.75rem;
}
.crop-control-stack {
display: grid;
gap: 0.85rem;
}
.file-picker input[type="file"] {
margin-top: 0.75rem;
padding: 0.75rem;
@@ -628,6 +844,7 @@ label {
@media (max-width: 980px) {
.app-shell,
.auth-panel,
.hero-card,
.dashboard-grid,
.forms-grid,