@open-core/identity 1.0.0 → 1.2.1
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 +54 -658
- package/dist/contracts.d.ts +93 -0
- package/dist/contracts.js +21 -0
- package/dist/entities/account.entity.js +1 -2
- package/dist/entities/role.entity.js +1 -2
- package/dist/events/identity.events.js +1 -2
- package/dist/index.d.ts +86 -67
- package/dist/index.js +110 -99
- package/dist/providers/auth/api-auth.provider.d.ts +52 -0
- package/dist/providers/auth/api-auth.provider.js +82 -0
- package/dist/providers/auth/credentials-auth.provider.d.ts +63 -0
- package/dist/providers/auth/credentials-auth.provider.js +149 -0
- package/dist/providers/auth/local-auth.provider.d.ts +82 -0
- package/dist/providers/auth/local-auth.provider.js +151 -0
- package/dist/providers/identity-auth.provider.d.ts +0 -0
- package/dist/providers/identity-auth.provider.js +1 -0
- package/dist/providers/principal/api-principal.provider.d.ts +50 -0
- package/dist/providers/principal/api-principal.provider.js +84 -0
- package/dist/providers/principal/local-principal.provider.d.ts +77 -0
- package/dist/providers/principal/local-principal.provider.js +164 -0
- package/dist/repositories/account.repository.d.ts +4 -4
- package/dist/repositories/account.repository.js +2 -6
- package/dist/repositories/role.repository.d.ts +4 -4
- package/dist/repositories/role.repository.js +2 -6
- package/dist/services/account.service.d.ts +52 -57
- package/dist/services/account.service.js +80 -166
- package/dist/services/auth/api-auth.provider.js +7 -10
- package/dist/services/auth/credentials-auth.provider.js +8 -44
- package/dist/services/auth/local-auth.provider.js +7 -10
- package/dist/services/cache/memory-cache.service.js +4 -7
- package/dist/services/identity-auth.provider.js +7 -10
- package/dist/services/identity-principal.provider.js +12 -15
- package/dist/services/principal/api-principal.provider.js +9 -12
- package/dist/services/principal/local-principal.provider.js +12 -15
- package/dist/services/role.service.d.ts +33 -54
- package/dist/services/role.service.js +51 -109
- package/dist/setup.js +25 -28
- package/dist/tokens.d.ts +7 -0
- package/dist/tokens.js +7 -0
- package/dist/types/auth.types.js +1 -2
- package/dist/types/index.js +1 -2
- package/dist/types.d.ts +170 -0
- package/dist/types.js +1 -0
- package/package.json +13 -8
- package/migrations/001_accounts_table.sql +0 -16
- package/migrations/002_roles_table.sql +0 -21
- package/migrations/003_alter_accounts_add_role.sql +0 -24
- package/migrations/004_rename_uuid_to_linked_id.sql +0 -12
- package/migrations/005_add_password_hash.sql +0 -7
package/README.md
CHANGED
|
@@ -1,681 +1,77 @@
|
|
|
1
1
|
# @open-core/identity
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Enterprise-grade identity, authentication, and authorization plugin for the OpenCore Framework.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Documentation Index
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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
|
|
7
|
+
- [Architecture & Dependency Injection](./docs/architecture.md) - Learn about constructor injection and DI.
|
|
8
|
+
- [Authentication Modes](./docs/auth-modes.md) - Details on `local`, `credentials`, and `api` auth.
|
|
9
|
+
- [Principal Modes](./docs/principal-modes.md) - Details on `roles`, `db`, and `api` authorization.
|
|
10
|
+
- [Implementing Contracts](./docs/contracts.md) - How to build your own `IdentityStore` or `RoleStore`.
|
|
14
11
|
|
|
15
|
-
##
|
|
12
|
+
## Features
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
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)
|
|
14
|
+
- **Multi-Strategy Authentication**: Support for `local`, `credentials`, and `api` strategies.
|
|
15
|
+
- **Hierarchical RBAC**: Rank-based authorization and permission merging.
|
|
16
|
+
- **Constructor Injection**: Services are automatically available in your classes via DI.
|
|
17
|
+
- **Stateless Architecture**: Decoupled persistence via implementable contracts.
|
|
348
18
|
|
|
349
|
-
|
|
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
|
-
});
|
|
19
|
+
## Quick Start (Constructor Injection)
|
|
401
20
|
|
|
402
|
-
|
|
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:
|
|
21
|
+
The recommended way to use the identity system is through **Constructor Injection**. The framework handles the lifecycle for you.
|
|
413
22
|
|
|
414
23
|
```ts
|
|
415
24
|
import { Server } from "@open-core/framework";
|
|
416
|
-
import {
|
|
25
|
+
import { AccountService } from "@open-core/identity";
|
|
26
|
+
import { injectable } from "tsyringe";
|
|
417
27
|
|
|
28
|
+
@injectable()
|
|
418
29
|
@Server.Controller()
|
|
419
|
-
export class
|
|
420
|
-
|
|
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:
|
|
30
|
+
export class MyController {
|
|
31
|
+
// AccountService is automatically injected
|
|
32
|
+
constructor(private readonly accounts: AccountService) {}
|
|
521
33
|
|
|
522
|
-
|
|
523
|
-
|
|
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
|
|
34
|
+
@Server.OnNet("admin:ban")
|
|
35
|
+
async handleBan(player: Server.Player, targetId: string) {
|
|
36
|
+
await this.accounts.ban(targetId, { reason: "Policy violation" });
|
|
626
37
|
}
|
|
627
|
-
// ... implement other methods
|
|
628
38
|
}
|
|
629
|
-
|
|
630
|
-
// Register manually
|
|
631
|
-
container.registerSingleton("AuthProviderContract", CustomAuthProvider);
|
|
632
39
|
```
|
|
633
40
|
|
|
634
|
-
##
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
Identity.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
Identity.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
|
41
|
+
## Installation & Setup
|
|
42
|
+
|
|
43
|
+
1. **Implement your Store** (See [Contracts](./docs/contracts.md)):
|
|
44
|
+
```ts
|
|
45
|
+
import { Identity, IdentityStore } from "@open-core/identity";
|
|
46
|
+
|
|
47
|
+
class MyStore extends IdentityStore { /* ... */ }
|
|
48
|
+
|
|
49
|
+
// Register it before installation
|
|
50
|
+
Identity.setIdentityStore(MyStore);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
2. **Install the Plugin**:
|
|
54
|
+
```ts
|
|
55
|
+
Identity.install({
|
|
56
|
+
auth: { mode: 'local', autoCreate: true },
|
|
57
|
+
principal: {
|
|
58
|
+
mode: 'roles',
|
|
59
|
+
roles: {
|
|
60
|
+
admin: { name: 'admin', rank: 100, permissions: ['*'] },
|
|
61
|
+
user: { name: 'user', rank: 0, permissions: ['chat.use'] }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
```
|
|
672
66
|
|
|
673
|
-
|
|
674
|
-
pnpm lint:fix
|
|
67
|
+
## Exports
|
|
675
68
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
69
|
+
The library only exports high-level components to keep your IDE suggestions clean:
|
|
70
|
+
- `Identity`: The main namespace for installation and registration.
|
|
71
|
+
- `AccountService`, `RoleService`: Public services for business logic.
|
|
72
|
+
- `IdentityStore`, `RoleStore`: Abstract contracts for persistence.
|
|
73
|
+
- `IDENTITY_OPTIONS`: Token for advanced DI usage.
|
|
74
|
+
- All relevant types and interfaces.
|
|
679
75
|
|
|
680
76
|
## License
|
|
681
77
|
|