a20edf5a324d0592da8a72309330dab073a587ed80c02538e0a1b1c971800d12| Command | Method | Description |
|---|---|---|
tier | GET | Show your current tier |
1import { getUserMeta, setUserMeta } from "../../seed/tree/userMetadata.js";
2
3let User;
4
5export function setModels(models) {
6 User = models.User;
7}
8
9// Feature access map. Extensions can register their own features via registerFeature().
10const FEATURE_ACCESS = {
11 "auto-place": ["standard", "premium"],
12 "file-upload": ["standard", "premium"],
13};
14
15/**
16 * Register a feature with its required tiers.
17 * Called by extensions during init to declare what tiers unlock their features.
18 */
19export function registerFeature(feature, tiers) {
20 FEATURE_ACCESS[feature] = tiers;
21}
22
23/**
24 * Get a user's current tier. Returns "basic" if not set.
25 */
26export async function getUserTier(userId) {
27 const user = await User.findById(userId).select("metadata").lean();
28 if (!user) return "basic";
29 const tiers = getUserMeta(user, "tiers");
30 return tiers.plan || "basic";
31}
32
33/**
34 * Check if a user's tier grants access to a feature.
35 * Returns true if the feature isn't registered (permissive default).
36 */
37export async function hasAccess(userId, feature) {
38 const allowedTiers = FEATURE_ACCESS[feature];
39 if (!allowedTiers) return true; // unknown feature = no restriction
40
41 const tier = await getUserTier(userId);
42 return allowedTiers.includes(tier);
43}
44
45/**
46 * Set a user's tier. Called by billing or admin.
47 */
48export async function setUserTier(userId, tier) {
49 const { batchSetUserMeta } = await import("../../seed/tree/userMetadata.js");
50 await batchSetUserMeta(userId, "tiers", { plan: tier });
51 return tier;
52}
531import log from "../../seed/log.js";
2import { setModels, getUserTier, hasAccess, setUserTier, registerFeature } from "./core.js";
3import { getExtension } from "../loader.js";
4import { getUserMeta } from "../../seed/tree/userMetadata.js";
5
6export async function init(core) {
7 setModels(core.models);
8
9 log.info("UserTiers", "Tier management loaded");
10
11 const router = (await import("./routes.js")).default(core);
12
13 // Register plan badge on user profile (replaces the default "User/Admin" badge)
14 try {
15 const treeos = getExtension("treeos-base");
16 treeos?.exports?.registerSlot?.("user-profile-badge", "user-tiers", ({ userId, queryString, user }) => {
17 const plan = (getUserMeta(user, "tiers").plan || "basic").toLowerCase();
18 const label = plan.charAt(0).toUpperCase() + plan.slice(1);
19 return `<a href="/api/v1/user/${userId}/energy${queryString}">
20 <span class="plan-badge plan-${plan}">${label} Plan</span>
21 </a>`;
22 }, { priority: 10 });
23 } catch {}
24
25 return {
26 router,
27 exports: { getUserTier, hasAccess, setUserTier, registerFeature },
28 };
29}
301export default {
2 name: "user-tiers",
3 version: "1.0.3",
4 builtFor: "TreeOS",
5 description:
6 "The access control layer between users and features. Every user has a tier stored " +
7 "in metadata.tiers.plan (defaults to 'basic' if unset). Extensions declare which " +
8 "tiers unlock their features by calling registerFeature(featureName, allowedTiers) " +
9 "during init. The hasAccess(userId, feature) export checks the user's current tier " +
10 "against the registered tier list for that feature. Unknown features return true " +
11 "(permissive default: if nobody registered the feature, nobody restricted it).\n\n" +
12 "Built-in feature gates: auto-place requires standard or premium, file-upload " +
13 "requires standard or premium. Other extensions add their own gates at boot. The " +
14 "billing extension calls setUserTier when a subscription changes. Admin users can " +
15 "set any user's tier via PUT /user/:userId/tier. The CLI exposes 'tier' to check " +
16 "your current plan. No models of its own. Tier data lives in user metadata. The " +
17 "exports (getUserTier, hasAccess, setUserTier, registerFeature) are the contract " +
18 "other extensions depend on. Energy reads the tier for daily limits. Billing " +
19 "writes the tier on payment. Everything else checks access through hasAccess.",
20
21 needs: {
22 models: ["User"],
23 services: ["protocol"],
24 },
25
26 optional: {
27 extensions: ["treeos-base"],
28 },
29
30 provides: {
31 routes: "./routes.js",
32 tools: false,
33 modes: false,
34 jobs: false,
35 orchestrator: false,
36 energyActions: {},
37 sessionTypes: {},
38 cli: [
39 { command: "tier", scope: ["home"], description: "Show your current tier", method: "GET", endpoint: "/user/:userId/tier" },
40 ],
41 },
42};
431import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { getUserTier, setUserTier } from "./core.js";
4
5export default function (core) {
6 const router = express.Router();
7 const { sendOk, sendError, ERR } = core.protocol;
8 const User = core.models.User;
9
10 // GET /user/:userId/tier
11 router.get("/user/:userId/tier", authenticate, async (req, res) => {
12 try {
13 const tier = await getUserTier(req.params.userId);
14 sendOk(res, { userId: req.params.userId, tier });
15 } catch (err) {
16 sendError(res, 500, ERR.INTERNAL, err.message);
17 }
18 });
19
20 // PUT /user/:userId/tier - admin only
21 router.put("/user/:userId/tier", authenticate, async (req, res) => {
22 try {
23 const admin = await User.findById(req.userId).select("isAdmin").lean();
24 if (!admin?.isAdmin) {
25 return sendError(res, 403, ERR.FORBIDDEN, "Admin access required");
26 }
27
28 const { tier } = req.body;
29 if (!tier || typeof tier !== "string") {
30 return sendError(res, 400, ERR.INVALID_INPUT, "Tier is required");
31 }
32
33 const result = await setUserTier(req.params.userId, tier);
34 sendOk(res, { userId: req.params.userId, tier: result });
35 } catch (err) {
36 sendError(res, 500, ERR.INTERNAL, err.message);
37 }
38 });
39
40 return router;
41}
42
treeos ext star user-tiers
Post comments from the CLI: treeos ext comment user-tiers "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...