@vocoweb/tenant 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/index.d.mts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +192 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +179 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react.d.mts +26 -0
- package/dist/react.d.ts +26 -0
- package/dist/react.js +221 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +198 -0
- package/dist/react.mjs.map +1 -0
- package/dist/types-BX4GZa4r.d.mts +63 -0
- package/dist/types-BX4GZa4r.d.ts +63 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 VocoWeb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# @vocoweb/tenant
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@vocoweb/tenant)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Production-ready multi-tenancy and RBAC for B2B SaaS applications.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
`@vocoweb/tenant` is the "Tenant Guard" module that solves the nightmare of building Teams, Invites, Roles, Permissions, and Data Isolation for B2B SaaS. It provides automatic tenant_id injection, pre-built components, and complete organization management.
|
|
11
|
+
|
|
12
|
+
**Key Features:**
|
|
13
|
+
- 🏢 **Organization Management** - Create and manage teams/workspaces
|
|
14
|
+
- 👥 **Role-Based Access Control** - Admin, Member, Viewer roles with custom permissions
|
|
15
|
+
- 📧 **Invite System** - Email-based team invitations
|
|
16
|
+
- 🔒 **Data Isolation** - Automatic tenant_id injection in all queries
|
|
17
|
+
- ⚛️ **Pre-built Components** - Team switcher, invite UI, settings
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @vocoweb/tenant
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### Server-Side (API Routes)
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { tenant } from '@vocoweb/tenant';
|
|
31
|
+
|
|
32
|
+
// Create organization
|
|
33
|
+
export async function POST(request: Request) {
|
|
34
|
+
const { name } = await request.json();
|
|
35
|
+
const user = await auth.requireUser(request);
|
|
36
|
+
|
|
37
|
+
const org = await tenant.createOrganization({
|
|
38
|
+
name,
|
|
39
|
+
ownerId: user.id,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return Response.json({ organization: org });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Invite member
|
|
46
|
+
export async function POST(request: Request) {
|
|
47
|
+
const { email, role, organizationId } = await request.json();
|
|
48
|
+
|
|
49
|
+
await tenant.inviteMember({
|
|
50
|
+
organizationId,
|
|
51
|
+
email,
|
|
52
|
+
role: 'member', // or 'admin', 'viewer'
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return Response.json({ success: true });
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Client-Side (React Components)
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { VocoTeamSwitcher, VocoInviteMember } from '@vocoweb/tenant/react';
|
|
63
|
+
|
|
64
|
+
// Navbar with team switcher
|
|
65
|
+
export default function Navbar() {
|
|
66
|
+
return (
|
|
67
|
+
<nav>
|
|
68
|
+
<VocoTeamSwitcher />
|
|
69
|
+
</nav>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Settings page with invite UI
|
|
74
|
+
export default function TeamSettings() {
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<h1>Team Settings</h1>
|
|
78
|
+
<VocoInviteMember role="admin" />
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Middleware (Automatic Tenant Injection)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// middleware.ts
|
|
88
|
+
import { createTenantMiddleware } from '@vocoweb/tenant';
|
|
89
|
+
|
|
90
|
+
export const middleware = createTenantMiddleware({
|
|
91
|
+
// All database queries automatically include tenant_id
|
|
92
|
+
enforceIsolation: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const config = {
|
|
96
|
+
matcher: ['/api/:path*', '/dashboard/:path*'],
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## API Reference
|
|
101
|
+
|
|
102
|
+
### Server Methods
|
|
103
|
+
|
|
104
|
+
- `createOrganization(options)` - Create new organization
|
|
105
|
+
- `inviteMember(options)` - Send organization invite
|
|
106
|
+
- `acceptInvite(token)` - Accept organization invite
|
|
107
|
+
- `setMemberRole(options)` - Update member role
|
|
108
|
+
- `removeMember(options)` - Remove member from organization
|
|
109
|
+
- `getOrganizationMembers(orgId)` - List all members
|
|
110
|
+
- `getUserOrganizations(userId)` - Get user's organizations
|
|
111
|
+
|
|
112
|
+
### Client Methods
|
|
113
|
+
|
|
114
|
+
- `switchOrganization(orgId)` - Switch active organization
|
|
115
|
+
- `getCurrentOrganization()` - Get current active organization
|
|
116
|
+
|
|
117
|
+
### React Components
|
|
118
|
+
|
|
119
|
+
- `<VocoTeamSwitcher />` - Notion-style workspace switcher
|
|
120
|
+
- `<VocoInviteMember role="admin" />` - Invite member UI
|
|
121
|
+
|
|
122
|
+
## Environment Variables
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Supabase (Required)
|
|
126
|
+
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
|
|
127
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
128
|
+
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
|
|
129
|
+
|
|
130
|
+
# App Configuration
|
|
131
|
+
NEXT_PUBLIC_APP_URL=https://yourapp.com
|
|
132
|
+
SUPPORT_EMAIL=support@yourapp.com
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Database Schema
|
|
136
|
+
|
|
137
|
+
The package expects the following tables:
|
|
138
|
+
|
|
139
|
+
```sql
|
|
140
|
+
-- Organizations
|
|
141
|
+
CREATE TABLE organizations (
|
|
142
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
143
|
+
name TEXT NOT NULL,
|
|
144
|
+
owner_id UUID NOT NULL REFERENCES auth.users(id),
|
|
145
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
-- Organization Members
|
|
149
|
+
CREATE TABLE organization_members (
|
|
150
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
151
|
+
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
152
|
+
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
153
|
+
role TEXT NOT NULL CHECK (role IN ('admin', 'member', 'viewer')),
|
|
154
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
155
|
+
UNIQUE(organization_id, user_id)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
-- Invites
|
|
159
|
+
CREATE TABLE organization_invites (
|
|
160
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
161
|
+
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
162
|
+
email TEXT NOT NULL,
|
|
163
|
+
role TEXT NOT NULL,
|
|
164
|
+
token TEXT UNIQUE NOT NULL,
|
|
165
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
166
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
167
|
+
);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT © VocoWeb
|
|
173
|
+
|
|
174
|
+
## Support
|
|
175
|
+
|
|
176
|
+
- Email: legal@vocoweb.in
|
|
177
|
+
- Documentation: [GitHub Wiki](https://github.com/vocoweb/vocoweb-tenant/wiki)
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
**Built with care by [VocoWeb](https://vocoweb.in)**
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { A as AcceptInviteOptions, O as OrganizationMember, C as CreateOrganizationOptions, a as Organization, I as InviteMemberOptions, b as OrganizationInvite, R as RemoveMemberOptions, S as SetMemberRoleOptions } from './types-BX4GZa4r.mjs';
|
|
2
|
+
export { T as TenantContext, c as TenantMiddlewareOptions, U as UserRole } from './types-BX4GZa4r.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client-side functions for @vocoweb/tenant
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Switch active organization
|
|
9
|
+
*/
|
|
10
|
+
declare function switchOrganization(organizationId: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Get current active organization
|
|
13
|
+
*/
|
|
14
|
+
declare function getCurrentOrganization(): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Listen to organization changes
|
|
17
|
+
*/
|
|
18
|
+
declare function onOrganizationChange(callback: (organizationId: string | null) => void): () => void;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Server-side functions for @vocoweb/tenant
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a new organization
|
|
26
|
+
*/
|
|
27
|
+
declare function createOrganization(options: CreateOrganizationOptions): Promise<Organization>;
|
|
28
|
+
/**
|
|
29
|
+
* Invite a member to an organization
|
|
30
|
+
*/
|
|
31
|
+
declare function inviteMember(options: InviteMemberOptions): Promise<OrganizationInvite>;
|
|
32
|
+
/**
|
|
33
|
+
* Accept an organization invite
|
|
34
|
+
*/
|
|
35
|
+
declare function acceptInvite(options: AcceptInviteOptions): Promise<OrganizationMember>;
|
|
36
|
+
/**
|
|
37
|
+
* Set member role
|
|
38
|
+
*/
|
|
39
|
+
declare function setMemberRole(options: SetMemberRoleOptions): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Remove member from organization
|
|
42
|
+
*/
|
|
43
|
+
declare function removeMember(options: RemoveMemberOptions): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Get organization members
|
|
46
|
+
*/
|
|
47
|
+
declare function getOrganizationMembers(organizationId: string): Promise<OrganizationMember[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Get user's organizations
|
|
50
|
+
*/
|
|
51
|
+
declare function getUserOrganizations(userId: string): Promise<Organization[]>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @vocoweb/tenant
|
|
55
|
+
* Production-ready multi-tenancy and RBAC for B2B SaaS
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Main tenant API
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* import { tenant } from '@vocoweb/tenant';
|
|
63
|
+
*
|
|
64
|
+
* // Server-side
|
|
65
|
+
* const org = await tenant.createOrganization({ name: 'Acme Inc', ownerId: userId });
|
|
66
|
+
* await tenant.inviteMember({ organizationId, email, role: 'member' });
|
|
67
|
+
*
|
|
68
|
+
* // Client-side
|
|
69
|
+
* await tenant.switchOrganization(orgId);
|
|
70
|
+
*/
|
|
71
|
+
declare const tenant: {
|
|
72
|
+
switchOrganization: typeof switchOrganization;
|
|
73
|
+
getCurrentOrganization: typeof getCurrentOrganization;
|
|
74
|
+
onOrganizationChange: typeof onOrganizationChange;
|
|
75
|
+
createOrganization: typeof createOrganization;
|
|
76
|
+
inviteMember: typeof inviteMember;
|
|
77
|
+
acceptInvite: typeof acceptInvite;
|
|
78
|
+
setMemberRole: typeof setMemberRole;
|
|
79
|
+
removeMember: typeof removeMember;
|
|
80
|
+
getOrganizationMembers: typeof getOrganizationMembers;
|
|
81
|
+
getUserOrganizations: typeof getUserOrganizations;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
declare const VERSION = "1.0.0";
|
|
85
|
+
|
|
86
|
+
export { AcceptInviteOptions, CreateOrganizationOptions, InviteMemberOptions, Organization, OrganizationInvite, OrganizationMember, RemoveMemberOptions, SetMemberRoleOptions, VERSION, acceptInvite, createOrganization, getCurrentOrganization, getOrganizationMembers, getUserOrganizations, inviteMember, onOrganizationChange, removeMember, setMemberRole, switchOrganization, tenant };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { A as AcceptInviteOptions, O as OrganizationMember, C as CreateOrganizationOptions, a as Organization, I as InviteMemberOptions, b as OrganizationInvite, R as RemoveMemberOptions, S as SetMemberRoleOptions } from './types-BX4GZa4r.js';
|
|
2
|
+
export { T as TenantContext, c as TenantMiddlewareOptions, U as UserRole } from './types-BX4GZa4r.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client-side functions for @vocoweb/tenant
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Switch active organization
|
|
9
|
+
*/
|
|
10
|
+
declare function switchOrganization(organizationId: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Get current active organization
|
|
13
|
+
*/
|
|
14
|
+
declare function getCurrentOrganization(): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Listen to organization changes
|
|
17
|
+
*/
|
|
18
|
+
declare function onOrganizationChange(callback: (organizationId: string | null) => void): () => void;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Server-side functions for @vocoweb/tenant
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a new organization
|
|
26
|
+
*/
|
|
27
|
+
declare function createOrganization(options: CreateOrganizationOptions): Promise<Organization>;
|
|
28
|
+
/**
|
|
29
|
+
* Invite a member to an organization
|
|
30
|
+
*/
|
|
31
|
+
declare function inviteMember(options: InviteMemberOptions): Promise<OrganizationInvite>;
|
|
32
|
+
/**
|
|
33
|
+
* Accept an organization invite
|
|
34
|
+
*/
|
|
35
|
+
declare function acceptInvite(options: AcceptInviteOptions): Promise<OrganizationMember>;
|
|
36
|
+
/**
|
|
37
|
+
* Set member role
|
|
38
|
+
*/
|
|
39
|
+
declare function setMemberRole(options: SetMemberRoleOptions): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Remove member from organization
|
|
42
|
+
*/
|
|
43
|
+
declare function removeMember(options: RemoveMemberOptions): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Get organization members
|
|
46
|
+
*/
|
|
47
|
+
declare function getOrganizationMembers(organizationId: string): Promise<OrganizationMember[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Get user's organizations
|
|
50
|
+
*/
|
|
51
|
+
declare function getUserOrganizations(userId: string): Promise<Organization[]>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @vocoweb/tenant
|
|
55
|
+
* Production-ready multi-tenancy and RBAC for B2B SaaS
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Main tenant API
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* import { tenant } from '@vocoweb/tenant';
|
|
63
|
+
*
|
|
64
|
+
* // Server-side
|
|
65
|
+
* const org = await tenant.createOrganization({ name: 'Acme Inc', ownerId: userId });
|
|
66
|
+
* await tenant.inviteMember({ organizationId, email, role: 'member' });
|
|
67
|
+
*
|
|
68
|
+
* // Client-side
|
|
69
|
+
* await tenant.switchOrganization(orgId);
|
|
70
|
+
*/
|
|
71
|
+
declare const tenant: {
|
|
72
|
+
switchOrganization: typeof switchOrganization;
|
|
73
|
+
getCurrentOrganization: typeof getCurrentOrganization;
|
|
74
|
+
onOrganizationChange: typeof onOrganizationChange;
|
|
75
|
+
createOrganization: typeof createOrganization;
|
|
76
|
+
inviteMember: typeof inviteMember;
|
|
77
|
+
acceptInvite: typeof acceptInvite;
|
|
78
|
+
setMemberRole: typeof setMemberRole;
|
|
79
|
+
removeMember: typeof removeMember;
|
|
80
|
+
getOrganizationMembers: typeof getOrganizationMembers;
|
|
81
|
+
getUserOrganizations: typeof getUserOrganizations;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
declare const VERSION = "1.0.0";
|
|
85
|
+
|
|
86
|
+
export { AcceptInviteOptions, CreateOrganizationOptions, InviteMemberOptions, Organization, OrganizationInvite, OrganizationMember, RemoveMemberOptions, SetMemberRoleOptions, VERSION, acceptInvite, createOrganization, getCurrentOrganization, getOrganizationMembers, getUserOrganizations, inviteMember, onOrganizationChange, removeMember, setMemberRole, switchOrganization, tenant };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var supabaseJs = require('@supabase/supabase-js');
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var CURRENT_ORG_KEY = "voco_current_organization";
|
|
7
|
+
async function switchOrganization(organizationId) {
|
|
8
|
+
if (typeof window === "undefined") {
|
|
9
|
+
throw new Error("switchOrganization can only be called client-side");
|
|
10
|
+
}
|
|
11
|
+
localStorage.setItem(CURRENT_ORG_KEY, organizationId);
|
|
12
|
+
window.dispatchEvent(
|
|
13
|
+
new StorageEvent("storage", {
|
|
14
|
+
key: CURRENT_ORG_KEY,
|
|
15
|
+
newValue: organizationId
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function getCurrentOrganization() {
|
|
20
|
+
if (typeof window === "undefined") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return localStorage.getItem(CURRENT_ORG_KEY);
|
|
24
|
+
}
|
|
25
|
+
function onOrganizationChange(callback) {
|
|
26
|
+
if (typeof window === "undefined") {
|
|
27
|
+
return () => {
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const handler = (e) => {
|
|
31
|
+
if (e.key === CURRENT_ORG_KEY) {
|
|
32
|
+
callback(e.newValue);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
window.addEventListener("storage", handler);
|
|
36
|
+
return () => {
|
|
37
|
+
window.removeEventListener("storage", handler);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var getSupabaseClient = () => {
|
|
41
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
42
|
+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
43
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
44
|
+
throw new Error("Supabase credentials not configured");
|
|
45
|
+
}
|
|
46
|
+
return supabaseJs.createClient(supabaseUrl, supabaseKey);
|
|
47
|
+
};
|
|
48
|
+
async function createOrganization(options) {
|
|
49
|
+
const supabase = getSupabaseClient();
|
|
50
|
+
const { data, error } = await supabase.from("organizations").insert({
|
|
51
|
+
name: options.name,
|
|
52
|
+
owner_id: options.ownerId
|
|
53
|
+
}).select().single();
|
|
54
|
+
if (error) {
|
|
55
|
+
throw new Error(`Failed to create organization: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
await supabase.from("organization_members").insert({
|
|
58
|
+
organization_id: data.id,
|
|
59
|
+
user_id: options.ownerId,
|
|
60
|
+
role: "owner"
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
id: data.id,
|
|
64
|
+
name: data.name,
|
|
65
|
+
ownerId: data.owner_id,
|
|
66
|
+
createdAt: new Date(data.created_at)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function inviteMember(options) {
|
|
70
|
+
const supabase = getSupabaseClient();
|
|
71
|
+
const token = crypto.randomUUID();
|
|
72
|
+
const expiresAt = /* @__PURE__ */ new Date();
|
|
73
|
+
expiresAt.setDate(expiresAt.getDate() + 7);
|
|
74
|
+
const { data, error } = await supabase.from("organization_invites").insert({
|
|
75
|
+
organization_id: options.organizationId,
|
|
76
|
+
email: options.email,
|
|
77
|
+
role: options.role,
|
|
78
|
+
token,
|
|
79
|
+
expires_at: expiresAt.toISOString()
|
|
80
|
+
}).select().single();
|
|
81
|
+
if (error) {
|
|
82
|
+
throw new Error(`Failed to create invite: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
id: data.id,
|
|
86
|
+
organizationId: data.organization_id,
|
|
87
|
+
email: data.email,
|
|
88
|
+
role: data.role,
|
|
89
|
+
token: data.token,
|
|
90
|
+
expiresAt: new Date(data.expires_at),
|
|
91
|
+
createdAt: new Date(data.created_at)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function acceptInvite(options) {
|
|
95
|
+
const supabase = getSupabaseClient();
|
|
96
|
+
const { data: invite, error: inviteError } = await supabase.from("organization_invites").select("*").eq("token", options.token).single();
|
|
97
|
+
if (inviteError || !invite) {
|
|
98
|
+
throw new Error("Invalid or expired invite");
|
|
99
|
+
}
|
|
100
|
+
if (new Date(invite.expires_at) < /* @__PURE__ */ new Date()) {
|
|
101
|
+
throw new Error("Invite has expired");
|
|
102
|
+
}
|
|
103
|
+
const { data, error } = await supabase.from("organization_members").insert({
|
|
104
|
+
organization_id: invite.organization_id,
|
|
105
|
+
user_id: options.userId,
|
|
106
|
+
role: invite.role
|
|
107
|
+
}).select().single();
|
|
108
|
+
if (error) {
|
|
109
|
+
throw new Error(`Failed to add member: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
await supabase.from("organization_invites").delete().eq("id", invite.id);
|
|
112
|
+
return {
|
|
113
|
+
id: data.id,
|
|
114
|
+
organizationId: data.organization_id,
|
|
115
|
+
userId: data.user_id,
|
|
116
|
+
role: data.role,
|
|
117
|
+
createdAt: new Date(data.created_at)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function setMemberRole(options) {
|
|
121
|
+
const supabase = getSupabaseClient();
|
|
122
|
+
const { error } = await supabase.from("organization_members").update({ role: options.role }).eq("organization_id", options.organizationId).eq("user_id", options.userId);
|
|
123
|
+
if (error) {
|
|
124
|
+
throw new Error(`Failed to update role: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function removeMember(options) {
|
|
128
|
+
const supabase = getSupabaseClient();
|
|
129
|
+
const { error } = await supabase.from("organization_members").delete().eq("organization_id", options.organizationId).eq("user_id", options.userId);
|
|
130
|
+
if (error) {
|
|
131
|
+
throw new Error(`Failed to remove member: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function getOrganizationMembers(organizationId) {
|
|
135
|
+
const supabase = getSupabaseClient();
|
|
136
|
+
const { data, error } = await supabase.from("organization_members").select("*").eq("organization_id", organizationId);
|
|
137
|
+
if (error) {
|
|
138
|
+
throw new Error(`Failed to get members: ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
return data.map((row) => ({
|
|
141
|
+
id: row.id,
|
|
142
|
+
organizationId: row.organization_id,
|
|
143
|
+
userId: row.user_id,
|
|
144
|
+
role: row.role,
|
|
145
|
+
createdAt: new Date(row.created_at)
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
async function getUserOrganizations(userId) {
|
|
149
|
+
const supabase = getSupabaseClient();
|
|
150
|
+
const { data, error } = await supabase.from("organization_members").select("organizations(*)").eq("user_id", userId);
|
|
151
|
+
if (error) {
|
|
152
|
+
throw new Error(`Failed to get organizations: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
return data.map((row) => ({
|
|
155
|
+
id: row.organizations.id,
|
|
156
|
+
name: row.organizations.name,
|
|
157
|
+
ownerId: row.organizations.owner_id,
|
|
158
|
+
createdAt: new Date(row.organizations.created_at)
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/index.ts
|
|
163
|
+
var tenant = {
|
|
164
|
+
// Client methods
|
|
165
|
+
switchOrganization,
|
|
166
|
+
getCurrentOrganization,
|
|
167
|
+
onOrganizationChange,
|
|
168
|
+
// Server methods
|
|
169
|
+
createOrganization,
|
|
170
|
+
inviteMember,
|
|
171
|
+
acceptInvite,
|
|
172
|
+
setMemberRole,
|
|
173
|
+
removeMember,
|
|
174
|
+
getOrganizationMembers,
|
|
175
|
+
getUserOrganizations
|
|
176
|
+
};
|
|
177
|
+
var VERSION = "1.0.0";
|
|
178
|
+
|
|
179
|
+
exports.VERSION = VERSION;
|
|
180
|
+
exports.acceptInvite = acceptInvite;
|
|
181
|
+
exports.createOrganization = createOrganization;
|
|
182
|
+
exports.getCurrentOrganization = getCurrentOrganization;
|
|
183
|
+
exports.getOrganizationMembers = getOrganizationMembers;
|
|
184
|
+
exports.getUserOrganizations = getUserOrganizations;
|
|
185
|
+
exports.inviteMember = inviteMember;
|
|
186
|
+
exports.onOrganizationChange = onOrganizationChange;
|
|
187
|
+
exports.removeMember = removeMember;
|
|
188
|
+
exports.setMemberRole = setMemberRole;
|
|
189
|
+
exports.switchOrganization = switchOrganization;
|
|
190
|
+
exports.tenant = tenant;
|
|
191
|
+
//# sourceMappingURL=index.js.map
|
|
192
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/server.ts","../src/index.ts"],"names":["createClient"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Client-side functions for @vocoweb/tenant\n */\n\n\n\nconst CURRENT_ORG_KEY = 'voco_current_organization';\n\n/**\n * Switch active organization\n */\nexport async function switchOrganization(organizationId: string): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('switchOrganization can only be called client-side');\n }\n\n localStorage.setItem(CURRENT_ORG_KEY, organizationId);\n\n // Trigger storage event for cross-tab synchronization\n window.dispatchEvent(\n new StorageEvent('storage', {\n key: CURRENT_ORG_KEY,\n newValue: organizationId,\n })\n );\n}\n\n/**\n * Get current active organization\n */\nexport function getCurrentOrganization(): string | null {\n if (typeof window === 'undefined') {\n return null;\n }\n\n return localStorage.getItem(CURRENT_ORG_KEY);\n}\n\n/**\n * Listen to organization changes\n */\nexport function onOrganizationChange(\n callback: (organizationId: string | null) => void\n): () => void {\n if (typeof window === 'undefined') {\n return () => { };\n }\n\n const handler = (e: StorageEvent) => {\n if (e.key === CURRENT_ORG_KEY) {\n callback(e.newValue);\n }\n };\n\n window.addEventListener('storage', handler);\n\n return () => {\n window.removeEventListener('storage', handler);\n };\n}\n","/**\n * Server-side functions for @vocoweb/tenant\n */\n\nimport { createClient } from '@supabase/supabase-js';\nimport type {\n Organization,\n OrganizationMember,\n OrganizationInvite,\n CreateOrganizationOptions,\n InviteMemberOptions,\n AcceptInviteOptions,\n SetMemberRoleOptions,\n RemoveMemberOptions,\n} from './types';\n\nconst getSupabaseClient = () => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;\n\n if (!supabaseUrl || !supabaseKey) {\n throw new Error('Supabase credentials not configured');\n }\n\n return createClient(supabaseUrl, supabaseKey);\n};\n\n/**\n * Create a new organization\n */\nexport async function createOrganization(\n options: CreateOrganizationOptions\n): Promise<Organization> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('organizations')\n .insert({\n name: options.name,\n owner_id: options.ownerId,\n })\n .select()\n .single();\n\n if (error) {\n throw new Error(`Failed to create organization: ${error.message}`);\n }\n\n // Add owner as admin member\n await supabase.from('organization_members').insert({\n organization_id: data.id,\n user_id: options.ownerId,\n role: 'owner',\n });\n\n return {\n id: data.id,\n name: data.name,\n ownerId: data.owner_id,\n createdAt: new Date(data.created_at),\n };\n}\n\n/**\n * Invite a member to an organization\n */\nexport async function inviteMember(\n options: InviteMemberOptions\n): Promise<OrganizationInvite> {\n const supabase = getSupabaseClient();\n\n // Generate unique token\n const token = crypto.randomUUID();\n const expiresAt = new Date();\n expiresAt.setDate(expiresAt.getDate() + 7); // 7 days expiry\n\n const { data, error } = await supabase\n .from('organization_invites')\n .insert({\n organization_id: options.organizationId,\n email: options.email,\n role: options.role,\n token,\n expires_at: expiresAt.toISOString(),\n })\n .select()\n .single();\n\n if (error) {\n throw new Error(`Failed to create invite: ${error.message}`);\n }\n\n // TODO: Send email notification\n // await notify.sendEmail({\n // to: options.email,\n // template: 'organization-invite',\n // data: { organizationId: options.organizationId, token }\n // });\n\n return {\n id: data.id,\n organizationId: data.organization_id,\n email: data.email,\n role: data.role,\n token: data.token,\n expiresAt: new Date(data.expires_at),\n createdAt: new Date(data.created_at),\n };\n}\n\n/**\n * Accept an organization invite\n */\nexport async function acceptInvite(\n options: AcceptInviteOptions\n): Promise<OrganizationMember> {\n const supabase = getSupabaseClient();\n\n // Get invite\n const { data: invite, error: inviteError } = await supabase\n .from('organization_invites')\n .select('*')\n .eq('token', options.token)\n .single();\n\n if (inviteError || !invite) {\n throw new Error('Invalid or expired invite');\n }\n\n // Check expiry\n if (new Date(invite.expires_at) < new Date()) {\n throw new Error('Invite has expired');\n }\n\n // Add member\n const { data, error } = await supabase\n .from('organization_members')\n .insert({\n organization_id: invite.organization_id,\n user_id: options.userId,\n role: invite.role,\n })\n .select()\n .single();\n\n if (error) {\n throw new Error(`Failed to add member: ${error.message}`);\n }\n\n // Delete invite\n await supabase.from('organization_invites').delete().eq('id', invite.id);\n\n return {\n id: data.id,\n organizationId: data.organization_id,\n userId: data.user_id,\n role: data.role,\n createdAt: new Date(data.created_at),\n };\n}\n\n/**\n * Set member role\n */\nexport async function setMemberRole(\n options: SetMemberRoleOptions\n): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('organization_members')\n .update({ role: options.role })\n .eq('organization_id', options.organizationId)\n .eq('user_id', options.userId);\n\n if (error) {\n throw new Error(`Failed to update role: ${error.message}`);\n }\n}\n\n/**\n * Remove member from organization\n */\nexport async function removeMember(\n options: RemoveMemberOptions\n): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('organization_members')\n .delete()\n .eq('organization_id', options.organizationId)\n .eq('user_id', options.userId);\n\n if (error) {\n throw new Error(`Failed to remove member: ${error.message}`);\n }\n}\n\n/**\n * Get organization members\n */\nexport async function getOrganizationMembers(\n organizationId: string\n): Promise<OrganizationMember[]> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('organization_members')\n .select('*')\n .eq('organization_id', organizationId);\n\n if (error) {\n throw new Error(`Failed to get members: ${error.message}`);\n }\n\n return data.map((row) => ({\n id: row.id,\n organizationId: row.organization_id,\n userId: row.user_id,\n role: row.role,\n createdAt: new Date(row.created_at),\n }));\n}\n\n/**\n * Get user's organizations\n */\nexport async function getUserOrganizations(\n userId: string\n): Promise<Organization[]> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('organization_members')\n .select('organizations(*)')\n .eq('user_id', userId);\n\n if (error) {\n throw new Error(`Failed to get organizations: ${error.message}`);\n }\n\n return data.map((row: any) => ({\n id: row.organizations.id,\n name: row.organizations.name,\n ownerId: row.organizations.owner_id,\n createdAt: new Date(row.organizations.created_at),\n }));\n}\n","/**\n * @vocoweb/tenant\n * Production-ready multi-tenancy and RBAC for B2B SaaS\n */\n\n// Types\nexport * from './types';\n\n// Client-side\nexport * from './client';\n\n// Server-side\nexport * from './server';\n\n// Import for unified API\nimport * as clientTenant from './client';\nimport * as serverTenant from './server';\n\nimport type {\n Organization,\n OrganizationMember,\n OrganizationInvite,\n CreateOrganizationOptions,\n InviteMemberOptions,\n AcceptInviteOptions,\n SetMemberRoleOptions,\n RemoveMemberOptions,\n UserRole,\n} from './types';\n\n/**\n * Main tenant API\n *\n * @example\n * import { tenant } from '@vocoweb/tenant';\n *\n * // Server-side\n * const org = await tenant.createOrganization({ name: 'Acme Inc', ownerId: userId });\n * await tenant.inviteMember({ organizationId, email, role: 'member' });\n *\n * // Client-side\n * await tenant.switchOrganization(orgId);\n */\nexport const tenant = {\n // Client methods\n switchOrganization: clientTenant.switchOrganization,\n getCurrentOrganization: clientTenant.getCurrentOrganization,\n onOrganizationChange: clientTenant.onOrganizationChange,\n\n // Server methods\n createOrganization: serverTenant.createOrganization,\n inviteMember: serverTenant.inviteMember,\n acceptInvite: serverTenant.acceptInvite,\n setMemberRole: serverTenant.setMemberRole,\n removeMember: serverTenant.removeMember,\n getOrganizationMembers: serverTenant.getOrganizationMembers,\n getUserOrganizations: serverTenant.getUserOrganizations,\n};\n\n// Re-export types\nexport type {\n Organization,\n OrganizationMember,\n OrganizationInvite,\n CreateOrganizationOptions,\n InviteMemberOptions,\n AcceptInviteOptions,\n SetMemberRoleOptions,\n RemoveMemberOptions,\n UserRole,\n};\n\n// Version\nexport const VERSION = '1.0.0';\n"]}
|