Major updates, authentication, multi-users
This commit is contained in:
@@ -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.
|
||||
|
||||
Generated
+20
-2
@@ -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": {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+1306
-78
File diff suppressed because it is too large
Load Diff
+224
-7
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user