Introduces /community-guidelines and /policies/{privacy,terms,[slug]} pages,
swaps the signup/invite checkbox from agreedToTerms to agreedToGuidelines,
adds Member.agreement.acceptedAt, and stamps the field when a Helcim
customer is created.
117 lines
3 KiB
JavaScript
117 lines
3 KiB
JavaScript
// server/models/member.js
|
|
import mongoose from "mongoose";
|
|
import { resolve } from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
|
|
// Import configs using dynamic imports to avoid build issues
|
|
const getValidCircleValues = () => ["community", "founder", "practitioner"];
|
|
const getValidContributionValues = () => ["0", "5", "15", "30", "50"];
|
|
|
|
const memberSchema = new mongoose.Schema({
|
|
email: { type: String, required: true, unique: true },
|
|
emailHistory: [
|
|
{
|
|
email: { type: String, required: true },
|
|
changedAt: { type: Date, default: Date.now },
|
|
},
|
|
],
|
|
name: { type: String, required: true },
|
|
circle: {
|
|
type: String,
|
|
enum: getValidCircleValues(),
|
|
required: true,
|
|
},
|
|
contributionTier: {
|
|
type: String,
|
|
enum: getValidContributionValues(),
|
|
required: true,
|
|
},
|
|
role: {
|
|
type: String,
|
|
enum: ["member", "admin"],
|
|
default: "member",
|
|
},
|
|
status: {
|
|
type: String,
|
|
enum: ["pending_payment", "active", "suspended", "cancelled", "guest"],
|
|
default: "pending_payment",
|
|
},
|
|
helcimCustomerId: String,
|
|
helcimSubscriptionId: String,
|
|
paymentMethod: {
|
|
type: String,
|
|
enum: ["card", "bank", "none"],
|
|
default: "none",
|
|
},
|
|
subscriptionStartDate: Date,
|
|
subscriptionEndDate: Date,
|
|
nextBillingDate: Date,
|
|
slackInvited: { type: Boolean, default: false },
|
|
slackInviteStatus: {
|
|
type: String,
|
|
enum: ["pending", "sent", "failed", "accepted", "joined"],
|
|
default: "pending",
|
|
},
|
|
slackUserId: String,
|
|
|
|
// Profile fields
|
|
pronouns: String,
|
|
timeZone: String,
|
|
avatar: String,
|
|
studio: String,
|
|
bio: String,
|
|
location: String,
|
|
socialLinks: {
|
|
mastodon: String,
|
|
linkedin: String,
|
|
website: String,
|
|
other: String,
|
|
},
|
|
showInDirectory: { type: Boolean, default: true },
|
|
|
|
craftTags: [String],
|
|
board: {
|
|
slackHandle: String,
|
|
},
|
|
|
|
notifications: {
|
|
events: { type: Boolean, default: true },
|
|
},
|
|
|
|
inviteEmailSent: { type: Boolean, default: false },
|
|
inviteEmailSentAt: Date,
|
|
|
|
agreement: {
|
|
acceptedAt: Date,
|
|
},
|
|
|
|
// Magic link single-use enforcement
|
|
magicLinkJti: String,
|
|
magicLinkJtiUsed: { type: Boolean, default: false },
|
|
|
|
// Session revocation via token versioning
|
|
tokenVersion: { type: Number, default: 0 },
|
|
|
|
memberNumber: { type: Number, unique: true, sparse: true },
|
|
|
|
onboarding: {
|
|
completedAt: { type: Date, default: null },
|
|
eventPageVisited: { type: Boolean, default: false },
|
|
boardPageVisited: { type: Boolean, default: false },
|
|
wikiClicked: { type: Boolean, default: false },
|
|
skipped: {
|
|
profileTags: { type: Boolean, default: false },
|
|
visitEvent: { type: Boolean, default: false },
|
|
board: { type: Boolean, default: false },
|
|
wiki: { type: Boolean, default: false },
|
|
},
|
|
},
|
|
|
|
createdAt: { type: Date, default: Date.now },
|
|
lastLogin: Date,
|
|
});
|
|
|
|
// Check if model already exists to prevent re-compilation in development
|
|
export default mongoose.models.Member || mongoose.model("Member", memberSchema);
|