spaps 0.4.2 → 0.5.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/README.md +65 -2
- package/package.json +9 -7
- package/src/index.js +42 -0
- package/src/local-server.js +171 -11
- package/src/middleware/admin.js +238 -0
package/README.md
CHANGED
|
@@ -199,11 +199,74 @@ npm install --save-dev spaps
|
|
|
199
199
|
|
|
200
200
|
- 📖 **Full Documentation**: [sweetpotato.dev](https://sweetpotato.dev)
|
|
201
201
|
- 🔧 **Production Setup**: See deployment guides
|
|
202
|
-
- 💬 **Get Help**: [GitHub Issues](https://github.com/
|
|
202
|
+
- 💬 **Get Help**: [GitHub Issues](https://github.com/build000r/sweet-potato/issues)
|
|
203
203
|
- 🚀 **Examples**: Check `/examples` directory
|
|
204
204
|
|
|
205
|
+
## 🚀 Production Deployment
|
|
206
|
+
|
|
207
|
+
Ready to go live? SPAPS supports seamless migration from local to production:
|
|
208
|
+
|
|
209
|
+
### Local → Production Workflow
|
|
210
|
+
|
|
211
|
+
1. **Export Local Data**:
|
|
212
|
+
```bash
|
|
213
|
+
# Export your products, orders, and customers
|
|
214
|
+
curl http://localhost:3456/api/admin/export > spaps-data.json
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
2. **Set Up Production Server**:
|
|
218
|
+
```bash
|
|
219
|
+
# Deploy to your server (DigitalOcean, AWS, etc.)
|
|
220
|
+
# Example production server: http://104.131.188.214:3000
|
|
221
|
+
git clone https://github.com/build000r/sweet-potato
|
|
222
|
+
cd sweet-potato
|
|
223
|
+
npm install
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
3. **Configure Environment**:
|
|
227
|
+
```bash
|
|
228
|
+
# Set production environment variables
|
|
229
|
+
SUPABASE_URL=https://your-project.supabase.co
|
|
230
|
+
SUPABASE_SERVICE_KEY=eyJhb...your-service-key
|
|
231
|
+
STRIPE_SECRET_KEY=sk_live_... # Your live Stripe key
|
|
232
|
+
JWT_SECRET=your-32-char-secure-secret
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
4. **Sync Products to Production Stripe**:
|
|
236
|
+
```bash
|
|
237
|
+
# Import your local products to production Stripe
|
|
238
|
+
curl -X POST http://104.131.188.214:3000/api/v1/admin/products/sync \
|
|
239
|
+
-H "Content-Type: application/json" \
|
|
240
|
+
-d @spaps-data.json
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
5. **Update Frontend Config**:
|
|
244
|
+
```javascript
|
|
245
|
+
// Change from local to production endpoint
|
|
246
|
+
const SPAPS_URL = 'http://104.131.188.214:3000'; // Your production server
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Production Features
|
|
250
|
+
|
|
251
|
+
The production SPAPS server includes:
|
|
252
|
+
- ✅ **Real Supabase integration** with RLS policies
|
|
253
|
+
- ✅ **Live Stripe webhooks** with signature verification
|
|
254
|
+
- ✅ **Multi-wallet authentication** (Solana, Ethereum, Base, Bitcoin)
|
|
255
|
+
- ✅ **JWT authentication** with refresh tokens
|
|
256
|
+
- ✅ **Rate limiting** and security middleware
|
|
257
|
+
- ✅ **Usage tracking** and analytics
|
|
258
|
+
- ✅ **Multi-tenant support** for multiple client apps
|
|
259
|
+
|
|
260
|
+
### Health Check
|
|
261
|
+
|
|
262
|
+
Check if your production server is running:
|
|
263
|
+
```bash
|
|
264
|
+
curl http://104.131.188.214:3000/health
|
|
265
|
+
# Returns: {"status":"healthy","mode":"production"}
|
|
266
|
+
```
|
|
267
|
+
|
|
205
268
|
---
|
|
206
269
|
|
|
207
|
-
**Current Version**: v0.3
|
|
270
|
+
**Current Version**: v0.4.3
|
|
208
271
|
**License**: MIT
|
|
209
272
|
**Node.js**: >=16.0.0 required
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spaps",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sweet Potato Authentication & Payment Service CLI - Zero-config local development and
|
|
5
|
-
"main": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Sweet Potato Authentication & Payment Service CLI - Zero-config local development with built-in admin middleware and permission utilities",
|
|
5
|
+
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"spaps": "./bin/spaps.js"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
|
-
".": "./
|
|
11
|
-
"./
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./cli": "./bin/spaps.js",
|
|
12
|
+
"./client": "./client.js",
|
|
13
|
+
"./middleware": "./src/middleware/admin.js"
|
|
12
14
|
},
|
|
13
15
|
"scripts": {
|
|
14
16
|
"test": "echo \"No tests yet\""
|
|
@@ -30,10 +32,10 @@
|
|
|
30
32
|
"license": "MIT",
|
|
31
33
|
"repository": {
|
|
32
34
|
"type": "git",
|
|
33
|
-
"url": "https://github.com/
|
|
35
|
+
"url": "https://github.com/build000r/sweet-potato"
|
|
34
36
|
},
|
|
35
37
|
"bugs": {
|
|
36
|
-
"url": "https://github.com/
|
|
38
|
+
"url": "https://github.com/build000r/sweet-potato/issues"
|
|
37
39
|
},
|
|
38
40
|
"homepage": "https://sweetpotato.dev",
|
|
39
41
|
"dependencies": {
|
package/src/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPAPS Package Main Export
|
|
3
|
+
* Provides middleware, utilities, and client functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const adminMiddleware = require('./middleware/admin');
|
|
7
|
+
|
|
8
|
+
// Export admin middleware and utilities
|
|
9
|
+
module.exports = {
|
|
10
|
+
// Admin middleware functions
|
|
11
|
+
requireAdmin: adminMiddleware.requireAdmin,
|
|
12
|
+
requirePermission: adminMiddleware.requirePermission,
|
|
13
|
+
|
|
14
|
+
// Permission checking utilities
|
|
15
|
+
isAdminAccount: adminMiddleware.isAdminAccount,
|
|
16
|
+
getUserRole: adminMiddleware.getUserRole,
|
|
17
|
+
hasPermission: adminMiddleware.hasPermission,
|
|
18
|
+
getRoleAwareErrorMessage: adminMiddleware.getRoleAwareErrorMessage,
|
|
19
|
+
|
|
20
|
+
// Constants
|
|
21
|
+
DEFAULT_ADMIN_ACCOUNTS: adminMiddleware.DEFAULT_ADMIN_ACCOUNTS,
|
|
22
|
+
|
|
23
|
+
// Factory function for custom admin configurations
|
|
24
|
+
createPermissionChecker: adminMiddleware.createPermissionChecker,
|
|
25
|
+
|
|
26
|
+
// Express middleware aliases for convenience
|
|
27
|
+
admin: adminMiddleware.requireAdmin,
|
|
28
|
+
permission: adminMiddleware.requirePermission,
|
|
29
|
+
|
|
30
|
+
// Version and metadata
|
|
31
|
+
version: require('../package.json').version,
|
|
32
|
+
name: 'spaps'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Named exports for ES6 compatibility
|
|
36
|
+
module.exports.requireAdmin = adminMiddleware.requireAdmin;
|
|
37
|
+
module.exports.requirePermission = adminMiddleware.requirePermission;
|
|
38
|
+
module.exports.isAdminAccount = adminMiddleware.isAdminAccount;
|
|
39
|
+
module.exports.getUserRole = adminMiddleware.getUserRole;
|
|
40
|
+
module.exports.hasPermission = adminMiddleware.hasPermission;
|
|
41
|
+
module.exports.getRoleAwareErrorMessage = adminMiddleware.getRoleAwareErrorMessage;
|
|
42
|
+
module.exports.createPermissionChecker = adminMiddleware.createPermissionChecker;
|
package/src/local-server.js
CHANGED
|
@@ -608,10 +608,10 @@ class LocalServer {
|
|
|
608
608
|
}
|
|
609
609
|
});
|
|
610
610
|
|
|
611
|
-
// POST /api/stripe/products - Create new product
|
|
611
|
+
// POST /api/stripe/products - Create new product with optional price
|
|
612
612
|
this.app.post('/api/stripe/products', async (req, res) => {
|
|
613
613
|
try {
|
|
614
|
-
const { name, description, images, metadata = {}, active = true } = req.body;
|
|
614
|
+
const { name, description, images, metadata = {}, active = true, price, currency = 'usd' } = req.body;
|
|
615
615
|
|
|
616
616
|
if (!name) {
|
|
617
617
|
return res.status(400).json({
|
|
@@ -634,29 +634,67 @@ class LocalServer {
|
|
|
634
634
|
}
|
|
635
635
|
});
|
|
636
636
|
|
|
637
|
+
let stripePrice = null;
|
|
638
|
+
|
|
639
|
+
// If price is provided, create a price for the product
|
|
640
|
+
if (price !== undefined && price !== null && price !== '') {
|
|
641
|
+
stripePrice = await stripe.prices.create({
|
|
642
|
+
product: stripeProduct.id,
|
|
643
|
+
unit_amount: parseInt(price), // Price in cents
|
|
644
|
+
currency: currency,
|
|
645
|
+
metadata: {
|
|
646
|
+
spaps_managed: 'true',
|
|
647
|
+
created_by: 'spaps_cli'
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Update product with default price
|
|
652
|
+
await stripe.products.update(stripeProduct.id, {
|
|
653
|
+
default_price: stripePrice.id
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Fetch the updated product with default_price
|
|
658
|
+
const updatedProduct = await stripe.products.retrieve(stripeProduct.id, {
|
|
659
|
+
expand: ['default_price']
|
|
660
|
+
});
|
|
661
|
+
|
|
637
662
|
res.json({
|
|
638
663
|
success: true,
|
|
639
664
|
data: {
|
|
640
665
|
product: {
|
|
641
|
-
id:
|
|
642
|
-
name:
|
|
643
|
-
description:
|
|
644
|
-
images:
|
|
645
|
-
active:
|
|
646
|
-
metadata:
|
|
647
|
-
created:
|
|
666
|
+
id: updatedProduct.id,
|
|
667
|
+
name: updatedProduct.name,
|
|
668
|
+
description: updatedProduct.description,
|
|
669
|
+
images: updatedProduct.images,
|
|
670
|
+
active: updatedProduct.active,
|
|
671
|
+
metadata: updatedProduct.metadata,
|
|
672
|
+
created: updatedProduct.created,
|
|
673
|
+
default_price: updatedProduct.default_price ? {
|
|
674
|
+
id: updatedProduct.default_price.id,
|
|
675
|
+
unit_amount: updatedProduct.default_price.unit_amount,
|
|
676
|
+
currency: updatedProduct.default_price.currency
|
|
677
|
+
} : null
|
|
648
678
|
}
|
|
649
679
|
}
|
|
650
680
|
});
|
|
651
681
|
} else {
|
|
652
682
|
// Create locally
|
|
683
|
+
const productPrice = price ? parseInt(price) : 0;
|
|
653
684
|
const product = this.adminManager.createProduct({
|
|
654
685
|
name,
|
|
655
686
|
description,
|
|
656
|
-
price:
|
|
657
|
-
currency:
|
|
687
|
+
price: productPrice,
|
|
688
|
+
currency: currency
|
|
658
689
|
});
|
|
659
690
|
|
|
691
|
+
// Add mock default_price structure for consistency
|
|
692
|
+
product.default_price = productPrice > 0 ? {
|
|
693
|
+
id: `price_${product.id}`,
|
|
694
|
+
unit_amount: productPrice,
|
|
695
|
+
currency: currency
|
|
696
|
+
} : null;
|
|
697
|
+
|
|
660
698
|
res.json({
|
|
661
699
|
success: true,
|
|
662
700
|
data: { product }
|
|
@@ -730,6 +768,128 @@ class LocalServer {
|
|
|
730
768
|
}
|
|
731
769
|
});
|
|
732
770
|
|
|
771
|
+
// POST /api/stripe/prices - Create a new price for a product
|
|
772
|
+
this.app.post('/api/stripe/prices', async (req, res) => {
|
|
773
|
+
try {
|
|
774
|
+
const { product_id, unit_amount, currency = 'usd', recurring, metadata = {} } = req.body;
|
|
775
|
+
|
|
776
|
+
if (!product_id || !unit_amount) {
|
|
777
|
+
return res.status(400).json({
|
|
778
|
+
success: false,
|
|
779
|
+
error: { message: 'Product ID and unit amount are required' }
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (USE_REAL_STRIPE) {
|
|
784
|
+
const priceData = {
|
|
785
|
+
product: product_id,
|
|
786
|
+
unit_amount: parseInt(unit_amount),
|
|
787
|
+
currency,
|
|
788
|
+
metadata: {
|
|
789
|
+
...metadata,
|
|
790
|
+
spaps_managed: 'true'
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// Add recurring if specified
|
|
795
|
+
if (recurring) {
|
|
796
|
+
priceData.recurring = recurring;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const stripePrice = await stripe.prices.create(priceData);
|
|
800
|
+
|
|
801
|
+
res.json({
|
|
802
|
+
success: true,
|
|
803
|
+
data: {
|
|
804
|
+
price: {
|
|
805
|
+
id: stripePrice.id,
|
|
806
|
+
product: stripePrice.product,
|
|
807
|
+
unit_amount: stripePrice.unit_amount,
|
|
808
|
+
currency: stripePrice.currency,
|
|
809
|
+
recurring: stripePrice.recurring,
|
|
810
|
+
metadata: stripePrice.metadata
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
} else {
|
|
815
|
+
// Mock price creation
|
|
816
|
+
const price = {
|
|
817
|
+
id: `price_${Date.now()}`,
|
|
818
|
+
product: product_id,
|
|
819
|
+
unit_amount: parseInt(unit_amount),
|
|
820
|
+
currency,
|
|
821
|
+
recurring,
|
|
822
|
+
metadata
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
res.json({
|
|
826
|
+
success: true,
|
|
827
|
+
data: { price }
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
} catch (error) {
|
|
831
|
+
console.error('Create price error:', error);
|
|
832
|
+
res.status(500).json({
|
|
833
|
+
success: false,
|
|
834
|
+
error: {
|
|
835
|
+
code: 'CREATE_PRICE_ERROR',
|
|
836
|
+
message: error.message || 'Failed to create price'
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// PUT /api/stripe/products/:productId/default-price - Update product's default price
|
|
843
|
+
this.app.put('/api/stripe/products/:productId/default-price', async (req, res) => {
|
|
844
|
+
try {
|
|
845
|
+
const { productId } = req.params;
|
|
846
|
+
const { price_id } = req.body;
|
|
847
|
+
|
|
848
|
+
if (!price_id) {
|
|
849
|
+
return res.status(400).json({
|
|
850
|
+
success: false,
|
|
851
|
+
error: { message: 'Price ID is required' }
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (USE_REAL_STRIPE) {
|
|
856
|
+
const stripeProduct = await stripe.products.update(productId, {
|
|
857
|
+
default_price: price_id
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
res.json({
|
|
861
|
+
success: true,
|
|
862
|
+
data: {
|
|
863
|
+
product: {
|
|
864
|
+
id: stripeProduct.id,
|
|
865
|
+
name: stripeProduct.name,
|
|
866
|
+
default_price: stripeProduct.default_price
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
} else {
|
|
871
|
+
res.json({
|
|
872
|
+
success: true,
|
|
873
|
+
data: {
|
|
874
|
+
product: {
|
|
875
|
+
id: productId,
|
|
876
|
+
default_price: price_id
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
} catch (error) {
|
|
882
|
+
console.error('Update default price error:', error);
|
|
883
|
+
res.status(500).json({
|
|
884
|
+
success: false,
|
|
885
|
+
error: {
|
|
886
|
+
code: 'UPDATE_DEFAULT_PRICE_ERROR',
|
|
887
|
+
message: error.message || 'Failed to update default price'
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
|
|
733
893
|
// DELETE /api/stripe/products/:productId - Archive product
|
|
734
894
|
this.app.delete('/api/stripe/products/:productId', async (req, res) => {
|
|
735
895
|
try {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin middleware for SPAPS applications
|
|
5
|
+
* Provides built-in admin role checking and permission validation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Default admin configuration
|
|
9
|
+
const DEFAULT_ADMIN_ACCOUNTS = {
|
|
10
|
+
email: 'buildooor@gmail.com',
|
|
11
|
+
wallets: {
|
|
12
|
+
ethereum: '0xa72bb7CeF1e4B2Cc144373d8dE0Add7CCc8DF4Ba',
|
|
13
|
+
solana: 'HVEbdiYU3Rr34NHBSgKs7q8cvdTeZLqNL77Z1FB2vjLy',
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if an identifier (email/wallet) is an admin account
|
|
19
|
+
*/
|
|
20
|
+
function isAdminAccount(identifier, customAdmins = []) {
|
|
21
|
+
if (!identifier) return false;
|
|
22
|
+
|
|
23
|
+
const normalized = identifier.toLowerCase();
|
|
24
|
+
|
|
25
|
+
// Check default admin accounts
|
|
26
|
+
if (normalized === DEFAULT_ADMIN_ACCOUNTS.email.toLowerCase() ||
|
|
27
|
+
normalized === DEFAULT_ADMIN_ACCOUNTS.wallets.ethereum.toLowerCase() ||
|
|
28
|
+
normalized === DEFAULT_ADMIN_ACCOUNTS.wallets.solana.toLowerCase()) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check custom admin accounts
|
|
33
|
+
return customAdmins.some(admin => {
|
|
34
|
+
if (typeof admin === 'string') {
|
|
35
|
+
return admin.toLowerCase() === normalized;
|
|
36
|
+
}
|
|
37
|
+
if (admin.email && admin.email.toLowerCase() === normalized) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (admin.wallet_address && admin.wallet_address.toLowerCase() === normalized) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get user role based on identifier
|
|
49
|
+
*/
|
|
50
|
+
function getUserRole(identifier, customAdmins = []) {
|
|
51
|
+
if (isAdminAccount(identifier, customAdmins)) {
|
|
52
|
+
return 'admin';
|
|
53
|
+
}
|
|
54
|
+
return 'user';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get role-aware error message
|
|
59
|
+
*/
|
|
60
|
+
function getRoleAwareErrorMessage(requiredRole, userRole, action = 'perform this action') {
|
|
61
|
+
const messages = {
|
|
62
|
+
admin: {
|
|
63
|
+
user: `🔒 Admin privileges required to ${action}. Please authenticate with an admin account.`,
|
|
64
|
+
guest: `🔐 Authentication required. Please sign in with an admin account to ${action}.`
|
|
65
|
+
},
|
|
66
|
+
user: {
|
|
67
|
+
guest: `🔐 Authentication required. Please sign in to ${action}.`
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return messages[requiredRole]?.[userRole] || `Access denied. Required role: ${requiredRole}, current role: ${userRole}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Express middleware for admin authentication
|
|
76
|
+
*/
|
|
77
|
+
function requireAdmin(options = {}) {
|
|
78
|
+
const {
|
|
79
|
+
customAdmins = [],
|
|
80
|
+
errorMessage,
|
|
81
|
+
onUnauthorized,
|
|
82
|
+
allowLocalBypass = true
|
|
83
|
+
} = options;
|
|
84
|
+
|
|
85
|
+
return (req, res, next) => {
|
|
86
|
+
// Check if in local development mode
|
|
87
|
+
if (allowLocalBypass && req.isLocalMode) {
|
|
88
|
+
console.log(chalk.yellow('🏠 Local mode: Admin check bypassed'));
|
|
89
|
+
req.isAdmin = true;
|
|
90
|
+
req.userRole = 'admin';
|
|
91
|
+
return next();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract user information from request
|
|
95
|
+
const userEmail = req.user?.email || req.body?.email;
|
|
96
|
+
const walletAddress = req.user?.wallet_address || req.body?.wallet_address;
|
|
97
|
+
const identifier = userEmail || walletAddress;
|
|
98
|
+
|
|
99
|
+
if (!identifier) {
|
|
100
|
+
const message = errorMessage || getRoleAwareErrorMessage('admin', 'guest');
|
|
101
|
+
|
|
102
|
+
if (onUnauthorized) {
|
|
103
|
+
return onUnauthorized(req, res, { reason: 'no_identifier', message });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return res.status(401).json({
|
|
107
|
+
success: false,
|
|
108
|
+
error: {
|
|
109
|
+
code: 'AUTHENTICATION_REQUIRED',
|
|
110
|
+
message
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isAdmin = isAdminAccount(identifier, customAdmins);
|
|
116
|
+
const userRole = getUserRole(identifier, customAdmins);
|
|
117
|
+
|
|
118
|
+
if (!isAdmin) {
|
|
119
|
+
const message = errorMessage || getRoleAwareErrorMessage('admin', userRole);
|
|
120
|
+
|
|
121
|
+
if (onUnauthorized) {
|
|
122
|
+
return onUnauthorized(req, res, {
|
|
123
|
+
reason: 'insufficient_privileges',
|
|
124
|
+
message,
|
|
125
|
+
userRole,
|
|
126
|
+
identifier
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return res.status(403).json({
|
|
131
|
+
success: false,
|
|
132
|
+
error: {
|
|
133
|
+
code: 'INSUFFICIENT_PRIVILEGES',
|
|
134
|
+
message
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add admin information to request
|
|
140
|
+
req.isAdmin = true;
|
|
141
|
+
req.userRole = userRole;
|
|
142
|
+
req.adminAccount = identifier;
|
|
143
|
+
|
|
144
|
+
console.log(chalk.green(`👑 Admin authenticated: ${identifier}`));
|
|
145
|
+
next();
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Permission checking utility
|
|
151
|
+
*/
|
|
152
|
+
function hasPermission(user, requiredPermissions, customAdmins = []) {
|
|
153
|
+
if (!user) return false;
|
|
154
|
+
|
|
155
|
+
const identifier = user.email || user.wallet_address;
|
|
156
|
+
const userRole = getUserRole(identifier, customAdmins);
|
|
157
|
+
|
|
158
|
+
// Admins have all permissions
|
|
159
|
+
if (userRole === 'admin') {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check specific permissions
|
|
164
|
+
if (Array.isArray(requiredPermissions)) {
|
|
165
|
+
return requiredPermissions.every(permission =>
|
|
166
|
+
user.permissions?.includes(permission)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return user.permissions?.includes(requiredPermissions);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Express middleware for permission checking
|
|
175
|
+
*/
|
|
176
|
+
function requirePermission(permissions, options = {}) {
|
|
177
|
+
const {
|
|
178
|
+
customAdmins = [],
|
|
179
|
+
errorMessage,
|
|
180
|
+
onUnauthorized
|
|
181
|
+
} = options;
|
|
182
|
+
|
|
183
|
+
return (req, res, next) => {
|
|
184
|
+
const user = req.user;
|
|
185
|
+
const hasRequiredPermission = hasPermission(user, permissions, customAdmins);
|
|
186
|
+
|
|
187
|
+
if (!hasRequiredPermission) {
|
|
188
|
+
const userRole = user ? getUserRole(user.email || user.wallet_address, customAdmins) : 'guest';
|
|
189
|
+
const message = errorMessage || getRoleAwareErrorMessage('permission', userRole, `access this resource`);
|
|
190
|
+
|
|
191
|
+
if (onUnauthorized) {
|
|
192
|
+
return onUnauthorized(req, res, {
|
|
193
|
+
reason: 'insufficient_permissions',
|
|
194
|
+
message,
|
|
195
|
+
requiredPermissions: permissions,
|
|
196
|
+
userPermissions: user?.permissions || []
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return res.status(403).json({
|
|
201
|
+
success: false,
|
|
202
|
+
error: {
|
|
203
|
+
code: 'INSUFFICIENT_PERMISSIONS',
|
|
204
|
+
message,
|
|
205
|
+
details: {
|
|
206
|
+
required: permissions,
|
|
207
|
+
current: user?.permissions || []
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
next();
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
// Middleware functions
|
|
219
|
+
requireAdmin,
|
|
220
|
+
requirePermission,
|
|
221
|
+
|
|
222
|
+
// Utility functions
|
|
223
|
+
isAdminAccount,
|
|
224
|
+
getUserRole,
|
|
225
|
+
hasPermission,
|
|
226
|
+
getRoleAwareErrorMessage,
|
|
227
|
+
|
|
228
|
+
// Constants
|
|
229
|
+
DEFAULT_ADMIN_ACCOUNTS,
|
|
230
|
+
|
|
231
|
+
// Helper for client-side checking
|
|
232
|
+
createPermissionChecker: (customAdmins = []) => ({
|
|
233
|
+
isAdmin: (identifier) => isAdminAccount(identifier, customAdmins),
|
|
234
|
+
getRole: (identifier) => getUserRole(identifier, customAdmins),
|
|
235
|
+
hasPermission: (user, permissions) => hasPermission(user, permissions, customAdmins),
|
|
236
|
+
getErrorMessage: (requiredRole, userRole, action) => getRoleAwareErrorMessage(requiredRole, userRole, action)
|
|
237
|
+
})
|
|
238
|
+
};
|