@open-core/identity 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 +7 -0
- package/README.md +682 -0
- package/dist/entities/account.entity.d.ts +34 -0
- package/dist/entities/account.entity.js +2 -0
- package/dist/entities/role.entity.d.ts +35 -0
- package/dist/entities/role.entity.js +2 -0
- package/dist/events/identity.events.d.ts +24 -0
- package/dist/events/identity.events.js +2 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +100 -0
- package/dist/repositories/account.repository.d.ts +60 -0
- package/dist/repositories/account.repository.js +185 -0
- package/dist/repositories/role.repository.d.ts +50 -0
- package/dist/repositories/role.repository.js +79 -0
- package/dist/services/account.service.d.ts +78 -0
- package/dist/services/account.service.js +207 -0
- package/dist/services/auth/api-auth.provider.d.ts +30 -0
- package/dist/services/auth/api-auth.provider.js +134 -0
- package/dist/services/auth/credentials-auth.provider.d.ts +27 -0
- package/dist/services/auth/credentials-auth.provider.js +214 -0
- package/dist/services/auth/local-auth.provider.d.ts +28 -0
- package/dist/services/auth/local-auth.provider.js +135 -0
- package/dist/services/cache/memory-cache.service.d.ts +47 -0
- package/dist/services/cache/memory-cache.service.js +108 -0
- package/dist/services/identity-auth.provider.d.ts +18 -0
- package/dist/services/identity-auth.provider.js +125 -0
- package/dist/services/identity-principal.provider.d.ts +29 -0
- package/dist/services/identity-principal.provider.js +104 -0
- package/dist/services/principal/api-principal.provider.d.ts +27 -0
- package/dist/services/principal/api-principal.provider.js +141 -0
- package/dist/services/principal/local-principal.provider.d.ts +39 -0
- package/dist/services/principal/local-principal.provider.js +114 -0
- package/dist/services/role.service.d.ts +73 -0
- package/dist/services/role.service.js +145 -0
- package/dist/setup.d.ts +58 -0
- package/dist/setup.js +93 -0
- package/dist/types/auth.types.d.ts +48 -0
- package/dist/types/auth.types.js +2 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.js +2 -0
- package/migrations/001_accounts_table.sql +16 -0
- package/migrations/002_roles_table.sql +21 -0
- package/migrations/003_alter_accounts_add_role.sql +24 -0
- package/migrations/004_rename_uuid_to_linked_id.sql +12 -0
- package/migrations/005_add_password_hash.sql +7 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Opencore Framework
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
# @open-core/identity
|
|
2
|
+
|
|
3
|
+
Flexible identity and permission system for OpenCore. Provides multiple authentication strategies, role management, and permission-based authorization through the framework's Principal system.
|
|
4
|
+
|
|
5
|
+
## What It Solves
|
|
6
|
+
|
|
7
|
+
- **Flexible Authentication**: Choose between local (auto-create), credentials (username/password), or external API authentication
|
|
8
|
+
- **Account Management**: Create and lookup accounts via multiple identifiers (license/discord/steam)
|
|
9
|
+
- **Role-Based Permissions**: Assign roles with hierarchical ranks and base permissions
|
|
10
|
+
- **Custom Permissions**: Per-account permission overrides (additions or negations)
|
|
11
|
+
- **Authorization**: Expose `Principal` with combined permissions for `@Guard` decorators
|
|
12
|
+
- **Bans**: Temporary or permanent account bans with expiration tracking
|
|
13
|
+
- **Flexible Storage**: Use local database OR external API with RAM caching
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @open-core/identity
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Authentication Strategies
|
|
22
|
+
|
|
23
|
+
### 1. Local Authentication (Default)
|
|
24
|
+
|
|
25
|
+
Auto-creates accounts based on FiveM identifiers. Best for traditional FiveM servers.
|
|
26
|
+
|
|
27
|
+
**Features:**
|
|
28
|
+
- Auto-creates accounts on first connection
|
|
29
|
+
- Uses FiveM identifiers (license/discord/steam)
|
|
30
|
+
- Stores everything in local database
|
|
31
|
+
- Generates UUID-based linkedId
|
|
32
|
+
|
|
33
|
+
**Setup:**
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { container } from "tsyringe";
|
|
37
|
+
import { Identity } from "@open-core/identity";
|
|
38
|
+
|
|
39
|
+
Identity.setup(container, {
|
|
40
|
+
authProvider: "local",
|
|
41
|
+
principalProvider: "local",
|
|
42
|
+
useDatabase: true, // default
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Credentials Authentication
|
|
47
|
+
|
|
48
|
+
Username/password authentication against local database.
|
|
49
|
+
|
|
50
|
+
**Features:**
|
|
51
|
+
- Validates username + password (bcrypt)
|
|
52
|
+
- Requires explicit registration (no auto-create)
|
|
53
|
+
- Stores accounts in local database
|
|
54
|
+
- Requires `password_hash` column (migration 005)
|
|
55
|
+
|
|
56
|
+
**Setup:**
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
Identity.setup(container, {
|
|
60
|
+
authProvider: "credentials",
|
|
61
|
+
principalProvider: "local",
|
|
62
|
+
useDatabase: true,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Note:** Requires implementing `findByUsername` in AccountRepository for production use.
|
|
67
|
+
|
|
68
|
+
### 3. API Authentication
|
|
69
|
+
|
|
70
|
+
Delegates authentication to external API. No local database required.
|
|
71
|
+
|
|
72
|
+
**Features:**
|
|
73
|
+
- POSTs credentials to external API
|
|
74
|
+
- Receives linkedId from API
|
|
75
|
+
- Caches auth results in RAM (configurable TTL)
|
|
76
|
+
- Optional: sync to local DB for offline fallback
|
|
77
|
+
|
|
78
|
+
**Setup:**
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
Identity.setup(container, {
|
|
82
|
+
authProvider: "api",
|
|
83
|
+
principalProvider: "api",
|
|
84
|
+
useDatabase: false, // no DB needed
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Convars:**
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
set identity_api_auth_url "https://your-api.com/auth"
|
|
92
|
+
set identity_api_principal_url "https://your-api.com/principals"
|
|
93
|
+
set identity_api_headers '{"Authorization": "Bearer YOUR_TOKEN"}'
|
|
94
|
+
set identity_api_timeout 5000
|
|
95
|
+
set identity_cache_ttl 300000
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API Contracts
|
|
99
|
+
|
|
100
|
+
### POST /auth
|
|
101
|
+
|
|
102
|
+
**Request:**
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"credentials": {
|
|
107
|
+
"username": "player123",
|
|
108
|
+
"password": "secret"
|
|
109
|
+
},
|
|
110
|
+
"identifiers": {
|
|
111
|
+
"license": "license:abc123",
|
|
112
|
+
"discord": "123456789",
|
|
113
|
+
"steam": "steam:110000123456789"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Response:**
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"success": true,
|
|
123
|
+
"linkedId": "user_abc123",
|
|
124
|
+
"isNewAccount": false
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### GET /principals/:linkedId
|
|
129
|
+
|
|
130
|
+
**Response:**
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"name": "Administrator",
|
|
135
|
+
"rank": 100,
|
|
136
|
+
"permissions": ["admin.*", "player.kick", "player.ban"],
|
|
137
|
+
"meta": {
|
|
138
|
+
"roleId": 1,
|
|
139
|
+
"roleName": "admin"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Data Model
|
|
145
|
+
|
|
146
|
+
### Entities
|
|
147
|
+
|
|
148
|
+
- **Account**: User identity with identifiers, role assignment, custom permissions, and ban status
|
|
149
|
+
- **Role**: Named role (e.g., "admin", "moderator") with display name, rank weight, and base permissions
|
|
150
|
+
|
|
151
|
+
### linkedId vs UUID
|
|
152
|
+
|
|
153
|
+
The `linkedId` field replaces the old `uuid` field:
|
|
154
|
+
|
|
155
|
+
- **Local accounts**: Auto-generated UUID (e.g., `"550e8400-e29b-41d4-a716-446655440000"`)
|
|
156
|
+
- **API accounts**: ID from external system (e.g., `"user_123"`, `"discord:456789"`)
|
|
157
|
+
- **Nullable**: Can be `null` if not needed
|
|
158
|
+
- **Used in**: `player.accountID`, `Principal.id`
|
|
159
|
+
|
|
160
|
+
### Relationships
|
|
161
|
+
|
|
162
|
+
- An Account has **one** Role (FK `role_id`, nullable)
|
|
163
|
+
- Role provides **base permissions** and **rank**
|
|
164
|
+
- Account can have **custom permissions** (additions with `+perm` or negations with `-perm`)
|
|
165
|
+
- **Effective permissions** = Role permissions + Custom permissions (with negations applied)
|
|
166
|
+
|
|
167
|
+
## Migrations
|
|
168
|
+
|
|
169
|
+
Run SQL migrations in order:
|
|
170
|
+
|
|
171
|
+
1. `migrations/001_accounts_table.sql` - Create accounts table
|
|
172
|
+
2. `migrations/002_roles_table.sql` - Create roles table and insert default "user" role
|
|
173
|
+
3. `migrations/003_alter_accounts_add_role.sql` - Add `role_id` and `custom_permissions` to accounts
|
|
174
|
+
4. `migrations/004_rename_uuid_to_linked_id.sql` - Rename `uuid` to `linked_id`, add `external_source`
|
|
175
|
+
5. `migrations/005_add_password_hash.sql` - Add `password_hash` (optional, only for credentials auth)
|
|
176
|
+
|
|
177
|
+
**Important**: Execute migrations in order. Migrations 4 and 5 are for upgrading existing installations.
|
|
178
|
+
|
|
179
|
+
## Convars
|
|
180
|
+
|
|
181
|
+
### General
|
|
182
|
+
|
|
183
|
+
- `identity_primary_identifier` (default: `"license"`): Priority identifier for account lookup/creation
|
|
184
|
+
- `identity_auto_create` (default: `true`): Auto-create account on authentication if not found (local auth only)
|
|
185
|
+
- `identity_default_role` (default: `"user"`): Name of the default role assigned to new accounts
|
|
186
|
+
|
|
187
|
+
### API Authentication
|
|
188
|
+
|
|
189
|
+
- `identity_api_auth_url`: URL for authentication API endpoint
|
|
190
|
+
- `identity_api_principal_url`: URL for principal/permissions API endpoint
|
|
191
|
+
- `identity_api_headers`: JSON string of custom headers (e.g., `'{"Authorization": "Bearer TOKEN"}'`)
|
|
192
|
+
- `identity_api_timeout` (default: `5000`): Request timeout in milliseconds
|
|
193
|
+
- `identity_cache_ttl` (default: `300000`): Cache TTL in milliseconds (5 minutes)
|
|
194
|
+
- `identity_api_allow_fallback` (default: `false`): Allow empty permissions if API fails
|
|
195
|
+
|
|
196
|
+
### Credentials Authentication
|
|
197
|
+
|
|
198
|
+
- `identity_merge_identifiers` (default: `false`): Merge FiveM identifiers after credentials auth
|
|
199
|
+
|
|
200
|
+
## Quick Start
|
|
201
|
+
|
|
202
|
+
### Example: Local Auth (Auto-Create)
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// server/bootstrap.ts
|
|
206
|
+
import { container } from "tsyringe";
|
|
207
|
+
import { Identity } from "@open-core/identity";
|
|
208
|
+
|
|
209
|
+
// Setup Identity with local auth
|
|
210
|
+
Identity.setup(container, {
|
|
211
|
+
authProvider: "local",
|
|
212
|
+
principalProvider: "local",
|
|
213
|
+
useDatabase: true,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// server/controllers/auth.controller.ts
|
|
219
|
+
import { Server, Utils } from "@open-core/framework";
|
|
220
|
+
import { Identity } from "@open-core/identity";
|
|
221
|
+
|
|
222
|
+
@Server.Controller()
|
|
223
|
+
export class AuthController {
|
|
224
|
+
constructor(
|
|
225
|
+
private readonly auth: Identity.LocalAuthProvider,
|
|
226
|
+
private readonly players: Server.PlayerService,
|
|
227
|
+
) {}
|
|
228
|
+
|
|
229
|
+
@Server.OnCoreEvent("core:playerSessionCreated")
|
|
230
|
+
async handlePlayerSession({
|
|
231
|
+
clientId,
|
|
232
|
+
}: Server.PlayerSessionCreatedPayload): Promise<void> {
|
|
233
|
+
const player = this.players.getByClient(clientId);
|
|
234
|
+
if (!player) {
|
|
235
|
+
throw new Utils.AppError(
|
|
236
|
+
"PLAYER_NOT_FOUND",
|
|
237
|
+
`Player ${clientId} not found during session creation`,
|
|
238
|
+
"server",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Authenticate automatically using FiveM identifiers
|
|
243
|
+
const result = await this.auth.authenticate(player, {});
|
|
244
|
+
|
|
245
|
+
if (!result.success) {
|
|
246
|
+
player.kick(result.error ?? "Authentication failed");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Player is now authenticated and linked
|
|
251
|
+
player.emit("auth:success", {
|
|
252
|
+
linkedId: result.accountID,
|
|
253
|
+
isNewAccount: result.isNewAccount,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Example: Credentials Auth (Username/Password)
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
// server/bootstrap.ts
|
|
263
|
+
import { container } from "tsyringe";
|
|
264
|
+
import { Identity } from "@open-core/identity";
|
|
265
|
+
|
|
266
|
+
Identity.setup(container, {
|
|
267
|
+
authProvider: "credentials",
|
|
268
|
+
principalProvider: "local",
|
|
269
|
+
useDatabase: true,
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
// server/controllers/auth.controller.ts
|
|
275
|
+
import { Server, Utils } from "@open-core/framework";
|
|
276
|
+
import { Identity } from "@open-core/identity";
|
|
277
|
+
|
|
278
|
+
@Server.Controller()
|
|
279
|
+
export class AuthController {
|
|
280
|
+
constructor(
|
|
281
|
+
private readonly auth: Identity.CredentialsAuthProvider,
|
|
282
|
+
private readonly players: Server.PlayerService,
|
|
283
|
+
) {}
|
|
284
|
+
|
|
285
|
+
@Server.OnCoreEvent("core:playerSessionCreated")
|
|
286
|
+
async handlePlayerSession({
|
|
287
|
+
clientId,
|
|
288
|
+
}: Server.PlayerSessionCreatedPayload): Promise<void> {
|
|
289
|
+
const player = this.players.getByClient(clientId);
|
|
290
|
+
if (!player) return;
|
|
291
|
+
|
|
292
|
+
// Show login UI to player
|
|
293
|
+
player.emit("auth:showLogin");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@Server.OnNet("auth:loginAttempt")
|
|
297
|
+
async handleLoginAttempt(
|
|
298
|
+
player: Server.Player,
|
|
299
|
+
username: string,
|
|
300
|
+
password: string,
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
const result = await this.auth.authenticate(player, {
|
|
303
|
+
username,
|
|
304
|
+
password,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (!result.success) {
|
|
308
|
+
player.emit("auth:loginFailed", result.error);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Authentication successful
|
|
313
|
+
player.emit("auth:loginSuccess", {
|
|
314
|
+
linkedId: result.accountID,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Spawn player
|
|
318
|
+
player.emit("player:spawn", {
|
|
319
|
+
position: { x: -1032.0, y: -2732.0, z: 13.8 },
|
|
320
|
+
model: "mp_m_freemode_01",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@Server.OnNet("auth:register")
|
|
325
|
+
async handleRegister(
|
|
326
|
+
player: Server.Player,
|
|
327
|
+
username: string,
|
|
328
|
+
password: string,
|
|
329
|
+
): Promise<void> {
|
|
330
|
+
const result = await this.auth.register(player, {
|
|
331
|
+
username,
|
|
332
|
+
password,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (!result.success) {
|
|
336
|
+
player.emit("auth:registerFailed", result.error);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
player.emit("auth:registerSuccess", {
|
|
341
|
+
linkedId: result.accountID,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Example: API Auth (External System)
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
// server/bootstrap.ts
|
|
351
|
+
import { container } from "tsyringe";
|
|
352
|
+
import { Identity } from "@open-core/identity";
|
|
353
|
+
|
|
354
|
+
Identity.setup(container, {
|
|
355
|
+
authProvider: "api",
|
|
356
|
+
principalProvider: "api",
|
|
357
|
+
useDatabase: false, // No local DB needed
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
// server/controllers/auth.controller.ts
|
|
363
|
+
import { Server, Utils } from "@open-core/framework";
|
|
364
|
+
import { Identity } from "@open-core/identity";
|
|
365
|
+
|
|
366
|
+
@Server.Controller()
|
|
367
|
+
export class AuthController {
|
|
368
|
+
constructor(
|
|
369
|
+
private readonly auth: Identity.ApiAuthProvider,
|
|
370
|
+
private readonly players: Server.PlayerService,
|
|
371
|
+
) {}
|
|
372
|
+
|
|
373
|
+
@Server.OnCoreEvent("core:playerSessionCreated")
|
|
374
|
+
async handlePlayerSession({
|
|
375
|
+
clientId,
|
|
376
|
+
}: Server.PlayerSessionCreatedPayload): Promise<void> {
|
|
377
|
+
const player = this.players.getByClient(clientId);
|
|
378
|
+
if (!player) return;
|
|
379
|
+
|
|
380
|
+
// Show login UI - API will validate
|
|
381
|
+
player.emit("auth:showLogin");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@Server.OnNet("auth:loginAttempt")
|
|
385
|
+
async handleLoginAttempt(
|
|
386
|
+
player: Server.Player,
|
|
387
|
+
token: string,
|
|
388
|
+
): Promise<void> {
|
|
389
|
+
// API provider will POST to external API
|
|
390
|
+
const result = await this.auth.authenticate(player, { token });
|
|
391
|
+
|
|
392
|
+
if (!result.success) {
|
|
393
|
+
player.emit("auth:loginFailed", result.error);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// linkedId comes from external API
|
|
398
|
+
player.emit("auth:loginSuccess", {
|
|
399
|
+
linkedId: result.accountID,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
player.emit("player:spawn", {
|
|
403
|
+
position: { x: -1032.0, y: -2732.0, z: 13.8 },
|
|
404
|
+
model: "mp_m_freemode_01",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Example: Protect Handlers with Permissions
|
|
411
|
+
|
|
412
|
+
The framework **secures all handlers by default**. Use `@Guard` for permission/rank checks:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import { Server } from "@open-core/framework";
|
|
416
|
+
import { Identity } from "@open-core/identity";
|
|
417
|
+
|
|
418
|
+
@Server.Controller()
|
|
419
|
+
export class AdminController {
|
|
420
|
+
constructor(private readonly accounts: Identity.AccountService) {}
|
|
421
|
+
|
|
422
|
+
@Server.OnNet("admin:banPlayer")
|
|
423
|
+
@Server.Guard({ permission: "admin.ban" })
|
|
424
|
+
async banPlayer(player: Server.Player, targetId: number): Promise<void> {
|
|
425
|
+
// player.accountID is guaranteed to exist (authenticated)
|
|
426
|
+
const target = await this.accounts.findById(targetId);
|
|
427
|
+
if (!target) {
|
|
428
|
+
player.emit("error", "Target not found");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await this.accounts.ban(targetId, {
|
|
433
|
+
reason: "Banned by admin",
|
|
434
|
+
durationMs: 86400000, // 24 hours
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
player.emit("success", `Player ${targetId} banned successfully`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
@Server.OnNet("admin:givePermission")
|
|
441
|
+
@Server.Guard({ rank: 100 }) // Only rank 100+ (super admin)
|
|
442
|
+
async givePermission(
|
|
443
|
+
player: Server.Player,
|
|
444
|
+
targetId: number,
|
|
445
|
+
permission: string,
|
|
446
|
+
): Promise<void> {
|
|
447
|
+
await this.accounts.addCustomPermission(targetId, permission);
|
|
448
|
+
player.emit("success", `Permission ${permission} granted`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
@Server.OnNet("chat:moderate")
|
|
452
|
+
@Server.Guard({ rank: 50 }) // Moderator rank or higher
|
|
453
|
+
async moderateChat(player: Server.Player, message: string): Promise<void> {
|
|
454
|
+
// Moderation logic here
|
|
455
|
+
player.emit("chat:moderated", message);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Note**: Use `@Server.Public()` to allow unauthenticated access to specific handlers.
|
|
461
|
+
|
|
462
|
+
## Services
|
|
463
|
+
|
|
464
|
+
### RoleService
|
|
465
|
+
|
|
466
|
+
Manage roles and their permissions:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
const roleService = container.resolve(Identity.RoleService);
|
|
470
|
+
|
|
471
|
+
// Create a role
|
|
472
|
+
await roleService.create({
|
|
473
|
+
name: "moderator",
|
|
474
|
+
displayName: "Moderator",
|
|
475
|
+
rank: 50,
|
|
476
|
+
permissions: ["chat.moderate", "player.kick"],
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Add permission to role
|
|
480
|
+
await roleService.addPermission(roleId, "player.mute");
|
|
481
|
+
|
|
482
|
+
// Get default role
|
|
483
|
+
const defaultRole = await roleService.getDefaultRole();
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### AccountService
|
|
487
|
+
|
|
488
|
+
Manage accounts and custom permissions:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
const accountService = container.resolve(Identity.AccountService);
|
|
492
|
+
|
|
493
|
+
// Find account by linkedId
|
|
494
|
+
const account = await accountService.findByLinkedId(linkedId);
|
|
495
|
+
|
|
496
|
+
// Or find by ID
|
|
497
|
+
const accountById = await accountService.findById(accountId);
|
|
498
|
+
|
|
499
|
+
// Assign role
|
|
500
|
+
await accountService.assignRole(accountId, roleId);
|
|
501
|
+
|
|
502
|
+
// Add custom permission (override)
|
|
503
|
+
await accountService.addCustomPermission(accountId, "special.feature");
|
|
504
|
+
|
|
505
|
+
// Negate a role permission
|
|
506
|
+
await accountService.addCustomPermission(accountId, "-chat.moderate");
|
|
507
|
+
|
|
508
|
+
// Get effective permissions (role + custom)
|
|
509
|
+
const permissions = await accountService.getEffectivePermissions(accountId);
|
|
510
|
+
|
|
511
|
+
// Ban account
|
|
512
|
+
await accountService.ban(accountId, {
|
|
513
|
+
reason: "Violation of rules",
|
|
514
|
+
durationMs: 86400000, // 24 hours
|
|
515
|
+
});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### MemoryCacheService
|
|
519
|
+
|
|
520
|
+
Cache service used by API providers:
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
const cache = container.resolve(Identity.MemoryCacheService);
|
|
524
|
+
|
|
525
|
+
// Set with TTL
|
|
526
|
+
cache.set("key", { data: "value" }, 60000); // 1 minute
|
|
527
|
+
|
|
528
|
+
// Get
|
|
529
|
+
const value = cache.get<{ data: string }>("key");
|
|
530
|
+
|
|
531
|
+
// Delete
|
|
532
|
+
cache.delete("key");
|
|
533
|
+
|
|
534
|
+
// Clear all
|
|
535
|
+
cache.clear();
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Permission System
|
|
539
|
+
|
|
540
|
+
### How Effective Permissions Work
|
|
541
|
+
|
|
542
|
+
1. **Base**: Start with role's base permissions
|
|
543
|
+
2. **Additions**: Custom permissions without `-` prefix are added
|
|
544
|
+
3. **Negations**: Custom permissions with `-` prefix remove base permissions
|
|
545
|
+
|
|
546
|
+
Example:
|
|
547
|
+
|
|
548
|
+
- Role "moderator" has: `["chat.moderate", "player.kick"]`
|
|
549
|
+
- Account custom permissions: `["admin.view", "-player.kick"]`
|
|
550
|
+
- **Effective**: `["chat.moderate", "admin.view"]` (kick negated)
|
|
551
|
+
|
|
552
|
+
### Principal Structure
|
|
553
|
+
|
|
554
|
+
When a player is authenticated, the Principal Provider returns a `Principal`:
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
{
|
|
558
|
+
id: account.linkedId ?? String(account.id), // linkedId or numeric ID
|
|
559
|
+
name: role.displayName, // e.g., "Administrator"
|
|
560
|
+
rank: role.rank, // e.g., 100
|
|
561
|
+
permissions: effectivePerms, // combined array
|
|
562
|
+
meta: {
|
|
563
|
+
accountId: account.id,
|
|
564
|
+
roleId: role.id,
|
|
565
|
+
roleName: role.name
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## Error Handling
|
|
571
|
+
|
|
572
|
+
The module throws `AppError` for security violations:
|
|
573
|
+
|
|
574
|
+
- **No linked account**: `AppError('UNAUTHORIZED', 'Player is not authenticated')`
|
|
575
|
+
- **Account not found**: `AppError('UNAUTHORIZED', 'Linked account not found')`
|
|
576
|
+
- **Account banned**: `AppError('PERMISSION_DENIED', 'Account is banned', { banReason, banExpires })`
|
|
577
|
+
|
|
578
|
+
Handle these in your error handlers or let the framework's default handler catch them.
|
|
579
|
+
|
|
580
|
+
## Migration Guide
|
|
581
|
+
|
|
582
|
+
### From uuid to linkedId
|
|
583
|
+
|
|
584
|
+
If you're upgrading from an earlier version:
|
|
585
|
+
|
|
586
|
+
1. Run migration 004: `migrations/004_rename_uuid_to_linked_id.sql`
|
|
587
|
+
2. Update code: `findByUuid()` → `findByLinkedId()`
|
|
588
|
+
3. Update code: `account.uuid` → `account.linkedId`
|
|
589
|
+
4. Everything else works the same
|
|
590
|
+
|
|
591
|
+
### Adding API Authentication
|
|
592
|
+
|
|
593
|
+
1. Set up your external API with the contracts above
|
|
594
|
+
2. Configure convars
|
|
595
|
+
3. Update setup:
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
Identity.setup(container, {
|
|
599
|
+
authProvider: "api",
|
|
600
|
+
principalProvider: "api",
|
|
601
|
+
useDatabase: false,
|
|
602
|
+
});
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
## Advanced Usage
|
|
606
|
+
|
|
607
|
+
### Hybrid Setup (API Auth + Local Permissions)
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
Identity.setup(container, {
|
|
611
|
+
authProvider: "api",
|
|
612
|
+
principalProvider: "local", // Use local DB for permissions
|
|
613
|
+
useDatabase: true, // Keep DB for roles/permissions
|
|
614
|
+
});
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Custom Provider
|
|
618
|
+
|
|
619
|
+
You can create your own auth/principal provider:
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
@injectable()
|
|
623
|
+
class CustomAuthProvider implements Server.AuthProviderContract {
|
|
624
|
+
async authenticate(player, credentials) {
|
|
625
|
+
// Your custom logic
|
|
626
|
+
}
|
|
627
|
+
// ... implement other methods
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Register manually
|
|
631
|
+
container.registerSingleton("AuthProviderContract", CustomAuthProvider);
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## Namespace Exports
|
|
635
|
+
|
|
636
|
+
All exports are available under the `Identity` namespace:
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
import { Identity } from "@open-core/identity";
|
|
640
|
+
|
|
641
|
+
// Types
|
|
642
|
+
Identity.Account;
|
|
643
|
+
Identity.Role;
|
|
644
|
+
Identity.SetupOptions;
|
|
645
|
+
|
|
646
|
+
// Services
|
|
647
|
+
Identity.AccountService;
|
|
648
|
+
Identity.RoleService;
|
|
649
|
+
Identity.MemoryCacheService;
|
|
650
|
+
|
|
651
|
+
// Auth Providers
|
|
652
|
+
Identity.LocalAuthProvider;
|
|
653
|
+
Identity.CredentialsAuthProvider;
|
|
654
|
+
Identity.ApiAuthProvider;
|
|
655
|
+
|
|
656
|
+
// Principal Providers
|
|
657
|
+
Identity.LocalPrincipalProvider;
|
|
658
|
+
Identity.ApiPrincipalProvider;
|
|
659
|
+
|
|
660
|
+
// Setup
|
|
661
|
+
Identity.setup(container, options);
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
## Scripts
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
# Build
|
|
668
|
+
pnpm build
|
|
669
|
+
|
|
670
|
+
# Lint
|
|
671
|
+
pnpm lint
|
|
672
|
+
|
|
673
|
+
# Lint and fix
|
|
674
|
+
pnpm lint:fix
|
|
675
|
+
|
|
676
|
+
# Clean build artifacts
|
|
677
|
+
pnpm clean
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
## License
|
|
681
|
+
|
|
682
|
+
MIT
|