@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Server } from "@open-core/framework";
|
|
2
|
+
import { AccountService } from "../account.service";
|
|
3
|
+
/**
|
|
4
|
+
* Local authentication provider that auto-creates accounts based on FiveM identifiers.
|
|
5
|
+
* This is the default/traditional authentication method for FiveM servers.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Auto-creates accounts on first connection
|
|
9
|
+
* - Uses FiveM identifiers (license/discord/steam)
|
|
10
|
+
* - Stores accounts in local database
|
|
11
|
+
* - Generates UUID-based linkedId
|
|
12
|
+
*/
|
|
13
|
+
export declare class LocalAuthProvider implements Server.AuthProviderContract {
|
|
14
|
+
private readonly accounts;
|
|
15
|
+
private readonly config;
|
|
16
|
+
constructor(accounts: AccountService, config: Server.ConfigService);
|
|
17
|
+
/**
|
|
18
|
+
* Get the linked account ID (linkedId or numeric ID as fallback)
|
|
19
|
+
*/
|
|
20
|
+
private getLinkedId;
|
|
21
|
+
authenticate(player: Server.Player, credentials: Record<string, unknown>): Promise<Server.AuthResult>;
|
|
22
|
+
register(player: Server.Player, credentials: Record<string, unknown>): Promise<Server.AuthResult>;
|
|
23
|
+
validateSession(player: Server.Player): Promise<Server.AuthResult>;
|
|
24
|
+
logout(player: Server.Player): Promise<void>;
|
|
25
|
+
private mergeIdentifiers;
|
|
26
|
+
private extractIdentifiers;
|
|
27
|
+
private extractFromPlayer;
|
|
28
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.LocalAuthProvider = void 0;
|
|
13
|
+
const tsyringe_1 = require("tsyringe");
|
|
14
|
+
const framework_1 = require("@open-core/framework");
|
|
15
|
+
const account_service_1 = require("../account.service");
|
|
16
|
+
/**
|
|
17
|
+
* Local authentication provider that auto-creates accounts based on FiveM identifiers.
|
|
18
|
+
* This is the default/traditional authentication method for FiveM servers.
|
|
19
|
+
*
|
|
20
|
+
* Features:
|
|
21
|
+
* - Auto-creates accounts on first connection
|
|
22
|
+
* - Uses FiveM identifiers (license/discord/steam)
|
|
23
|
+
* - Stores accounts in local database
|
|
24
|
+
* - Generates UUID-based linkedId
|
|
25
|
+
*/
|
|
26
|
+
let LocalAuthProvider = class LocalAuthProvider {
|
|
27
|
+
constructor(accounts, config) {
|
|
28
|
+
this.accounts = accounts;
|
|
29
|
+
this.config = config;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get the linked account ID (linkedId or numeric ID as fallback)
|
|
33
|
+
*/
|
|
34
|
+
getLinkedId(account) {
|
|
35
|
+
return account.linkedId ?? String(account.id);
|
|
36
|
+
}
|
|
37
|
+
async authenticate(player, credentials) {
|
|
38
|
+
const identifiers = this.mergeIdentifiers(credentials, player);
|
|
39
|
+
const { account, isNew } = await this.accounts.findOrCreate(identifiers);
|
|
40
|
+
if (this.accounts.isBanExpired(account)) {
|
|
41
|
+
await this.accounts.unban(account.id);
|
|
42
|
+
account.banned = false;
|
|
43
|
+
account.banExpires = null;
|
|
44
|
+
account.banReason = null;
|
|
45
|
+
}
|
|
46
|
+
if (account.banned) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: account.banReason ?? "Account banned",
|
|
50
|
+
accountID: this.getLinkedId(account),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const linkedId = this.getLinkedId(account);
|
|
54
|
+
player.linkAccount(linkedId);
|
|
55
|
+
await this.accounts.touchLastLogin(account.id);
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
accountID: linkedId,
|
|
59
|
+
isNewAccount: isNew,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async register(player, credentials) {
|
|
63
|
+
// Registration flow is equivalent to authenticate with auto-create.
|
|
64
|
+
return this.authenticate(player, credentials);
|
|
65
|
+
}
|
|
66
|
+
async validateSession(player) {
|
|
67
|
+
const linked = player.accountID;
|
|
68
|
+
if (!linked) {
|
|
69
|
+
// Attempt implicit authentication using identifiers if auto-create is enabled.
|
|
70
|
+
const autoCreate = this.config.getBoolean("identity_auto_create", true);
|
|
71
|
+
if (!autoCreate) {
|
|
72
|
+
return { success: false, error: "Not authenticated" };
|
|
73
|
+
}
|
|
74
|
+
return this.authenticate(player, {});
|
|
75
|
+
}
|
|
76
|
+
const account = await this.accounts.findByLinkedId(String(linked));
|
|
77
|
+
if (!account) {
|
|
78
|
+
return { success: false, error: "Account not found", accountID: linked };
|
|
79
|
+
}
|
|
80
|
+
if (this.accounts.isBanExpired(account)) {
|
|
81
|
+
await this.accounts.unban(account.id);
|
|
82
|
+
account.banned = false;
|
|
83
|
+
}
|
|
84
|
+
if (account.banned) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: account.banReason ?? "Account banned",
|
|
88
|
+
accountID: this.getLinkedId(account),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return { success: true, accountID: this.getLinkedId(account) };
|
|
92
|
+
}
|
|
93
|
+
async logout(player) {
|
|
94
|
+
// The framework session holds the account binding; clearing meta is enough for now.
|
|
95
|
+
player.setMeta("identity:session", null);
|
|
96
|
+
}
|
|
97
|
+
mergeIdentifiers(credentials, player) {
|
|
98
|
+
const fromCredentials = this.extractIdentifiers(credentials);
|
|
99
|
+
const fromPlayer = this.extractFromPlayer(player);
|
|
100
|
+
return {
|
|
101
|
+
license: fromCredentials.license ?? fromPlayer.license ?? null,
|
|
102
|
+
discord: fromCredentials.discord ?? fromPlayer.discord ?? null,
|
|
103
|
+
steam: fromCredentials.steam ?? fromPlayer.steam ?? null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
extractIdentifiers(input) {
|
|
107
|
+
return {
|
|
108
|
+
license: typeof input.license === "string" ? input.license : null,
|
|
109
|
+
discord: typeof input.discord === "string" ? input.discord : null,
|
|
110
|
+
steam: typeof input.steam === "string" ? input.steam : null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
extractFromPlayer(player) {
|
|
114
|
+
const identifiers = player.getIdentifiers();
|
|
115
|
+
const result = {
|
|
116
|
+
license: null,
|
|
117
|
+
discord: null,
|
|
118
|
+
steam: null,
|
|
119
|
+
};
|
|
120
|
+
for (const raw of identifiers) {
|
|
121
|
+
if (raw.startsWith("license:"))
|
|
122
|
+
result.license = raw.slice("license:".length);
|
|
123
|
+
if (raw.startsWith("discord:"))
|
|
124
|
+
result.discord = raw.slice("discord:".length);
|
|
125
|
+
if (raw.startsWith("steam:"))
|
|
126
|
+
result.steam = raw.slice("steam:".length);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
exports.LocalAuthProvider = LocalAuthProvider;
|
|
132
|
+
exports.LocalAuthProvider = LocalAuthProvider = __decorate([
|
|
133
|
+
(0, tsyringe_1.injectable)(),
|
|
134
|
+
__metadata("design:paramtypes", [account_service_1.AccountService, framework_1.Server.ConfigService])
|
|
135
|
+
], LocalAuthProvider);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory cache with TTL support.
|
|
3
|
+
* Used to cache API responses and reduce external calls.
|
|
4
|
+
*/
|
|
5
|
+
export declare class MemoryCacheService {
|
|
6
|
+
private cache;
|
|
7
|
+
private cleanupInterval;
|
|
8
|
+
private readonly defaultTtlMs;
|
|
9
|
+
private readonly cleanupIntervalMs;
|
|
10
|
+
constructor();
|
|
11
|
+
/**
|
|
12
|
+
* Get a value from cache if it exists and hasn't expired
|
|
13
|
+
*/
|
|
14
|
+
get<T>(key: string): T | null;
|
|
15
|
+
/**
|
|
16
|
+
* Set a value in cache with TTL
|
|
17
|
+
*/
|
|
18
|
+
set<T>(key: string, value: T, ttlMs?: number): void;
|
|
19
|
+
/**
|
|
20
|
+
* Delete a specific key from cache
|
|
21
|
+
*/
|
|
22
|
+
delete(key: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Clear all cache entries
|
|
25
|
+
*/
|
|
26
|
+
clear(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Get current cache size
|
|
29
|
+
*/
|
|
30
|
+
size(): number;
|
|
31
|
+
/**
|
|
32
|
+
* Check if a key exists in cache (and is not expired)
|
|
33
|
+
*/
|
|
34
|
+
has(key: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Start automatic cleanup of expired entries
|
|
37
|
+
*/
|
|
38
|
+
private startCleanup;
|
|
39
|
+
/**
|
|
40
|
+
* Clean up all expired entries
|
|
41
|
+
*/
|
|
42
|
+
private cleanExpired;
|
|
43
|
+
/**
|
|
44
|
+
* Stop the cleanup interval (useful for testing or shutdown)
|
|
45
|
+
*/
|
|
46
|
+
stopCleanup(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.MemoryCacheService = void 0;
|
|
13
|
+
const tsyringe_1 = require("tsyringe");
|
|
14
|
+
/**
|
|
15
|
+
* Simple in-memory cache with TTL support.
|
|
16
|
+
* Used to cache API responses and reduce external calls.
|
|
17
|
+
*/
|
|
18
|
+
let MemoryCacheService = class MemoryCacheService {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.cache = new Map();
|
|
21
|
+
this.cleanupInterval = null;
|
|
22
|
+
this.defaultTtlMs = 300000; // 5 minutes
|
|
23
|
+
this.cleanupIntervalMs = 60000; // 1 minute
|
|
24
|
+
this.startCleanup();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get a value from cache if it exists and hasn't expired
|
|
28
|
+
*/
|
|
29
|
+
get(key) {
|
|
30
|
+
const entry = this.cache.get(key);
|
|
31
|
+
if (!entry) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (Date.now() > entry.expiresAt) {
|
|
35
|
+
this.cache.delete(key);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return entry.value;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Set a value in cache with TTL
|
|
42
|
+
*/
|
|
43
|
+
set(key, value, ttlMs) {
|
|
44
|
+
const expiresAt = Date.now() + (ttlMs ?? this.defaultTtlMs);
|
|
45
|
+
this.cache.set(key, { value, expiresAt });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Delete a specific key from cache
|
|
49
|
+
*/
|
|
50
|
+
delete(key) {
|
|
51
|
+
return this.cache.delete(key);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Clear all cache entries
|
|
55
|
+
*/
|
|
56
|
+
clear() {
|
|
57
|
+
this.cache.clear();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get current cache size
|
|
61
|
+
*/
|
|
62
|
+
size() {
|
|
63
|
+
return this.cache.size;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if a key exists in cache (and is not expired)
|
|
67
|
+
*/
|
|
68
|
+
has(key) {
|
|
69
|
+
return this.get(key) !== null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Start automatic cleanup of expired entries
|
|
73
|
+
*/
|
|
74
|
+
startCleanup() {
|
|
75
|
+
this.cleanupInterval = setInterval(() => {
|
|
76
|
+
this.cleanExpired();
|
|
77
|
+
}, this.cleanupIntervalMs);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Clean up all expired entries
|
|
81
|
+
*/
|
|
82
|
+
cleanExpired() {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const keysToDelete = [];
|
|
85
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
86
|
+
if (now > entry.expiresAt) {
|
|
87
|
+
keysToDelete.push(key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const key of keysToDelete) {
|
|
91
|
+
this.cache.delete(key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Stop the cleanup interval (useful for testing or shutdown)
|
|
96
|
+
*/
|
|
97
|
+
stopCleanup() {
|
|
98
|
+
if (this.cleanupInterval) {
|
|
99
|
+
clearInterval(this.cleanupInterval);
|
|
100
|
+
this.cleanupInterval = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
exports.MemoryCacheService = MemoryCacheService;
|
|
105
|
+
exports.MemoryCacheService = MemoryCacheService = __decorate([
|
|
106
|
+
(0, tsyringe_1.injectable)(),
|
|
107
|
+
__metadata("design:paramtypes", [])
|
|
108
|
+
], MemoryCacheService);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Server } from "@open-core/framework";
|
|
2
|
+
import { AccountService } from "./account.service";
|
|
3
|
+
export declare class IdentityAuthProvider implements Server.AuthProviderContract {
|
|
4
|
+
private readonly accounts;
|
|
5
|
+
private readonly config;
|
|
6
|
+
constructor(accounts: AccountService, config: Server.ConfigService);
|
|
7
|
+
/**
|
|
8
|
+
* Get the linked account ID (linkedId or numeric ID as fallback)
|
|
9
|
+
*/
|
|
10
|
+
private getLinkedId;
|
|
11
|
+
authenticate(player: Server.Player, credentials: Record<string, unknown>): Promise<Server.AuthResult>;
|
|
12
|
+
register(player: Server.Player, credentials: Record<string, unknown>): Promise<Server.AuthResult>;
|
|
13
|
+
validateSession(player: Server.Player): Promise<Server.AuthResult>;
|
|
14
|
+
logout(player: Server.Player): Promise<void>;
|
|
15
|
+
private mergeIdentifiers;
|
|
16
|
+
private extractIdentifiers;
|
|
17
|
+
private extractFromPlayer;
|
|
18
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.IdentityAuthProvider = void 0;
|
|
13
|
+
const tsyringe_1 = require("tsyringe");
|
|
14
|
+
const framework_1 = require("@open-core/framework");
|
|
15
|
+
const account_service_1 = require("./account.service");
|
|
16
|
+
let IdentityAuthProvider = class IdentityAuthProvider {
|
|
17
|
+
constructor(accounts, config) {
|
|
18
|
+
this.accounts = accounts;
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the linked account ID (linkedId or numeric ID as fallback)
|
|
23
|
+
*/
|
|
24
|
+
getLinkedId(account) {
|
|
25
|
+
return account.linkedId ?? String(account.id);
|
|
26
|
+
}
|
|
27
|
+
async authenticate(player, credentials) {
|
|
28
|
+
const identifiers = this.mergeIdentifiers(credentials, player);
|
|
29
|
+
const { account, isNew } = await this.accounts.findOrCreate(identifiers);
|
|
30
|
+
if (this.accounts.isBanExpired(account)) {
|
|
31
|
+
await this.accounts.unban(account.id);
|
|
32
|
+
account.banned = false;
|
|
33
|
+
account.banExpires = null;
|
|
34
|
+
account.banReason = null;
|
|
35
|
+
}
|
|
36
|
+
if (account.banned) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: account.banReason ?? "Account banned",
|
|
40
|
+
accountID: this.getLinkedId(account),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const linkedId = this.getLinkedId(account);
|
|
44
|
+
player.linkAccount(linkedId);
|
|
45
|
+
await this.accounts.touchLastLogin(account.id);
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
accountID: linkedId,
|
|
49
|
+
isNewAccount: isNew,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async register(player, credentials) {
|
|
53
|
+
// Registration flow is equivalent to authenticate with auto-create.
|
|
54
|
+
return this.authenticate(player, credentials);
|
|
55
|
+
}
|
|
56
|
+
async validateSession(player) {
|
|
57
|
+
const linked = player.accountID;
|
|
58
|
+
if (!linked) {
|
|
59
|
+
// Attempt implicit authentication using identifiers if auto-create is enabled.
|
|
60
|
+
const autoCreate = this.config.getBoolean("identity_auto_create", true);
|
|
61
|
+
if (!autoCreate) {
|
|
62
|
+
return { success: false, error: "Not authenticated" };
|
|
63
|
+
}
|
|
64
|
+
return this.authenticate(player, {});
|
|
65
|
+
}
|
|
66
|
+
const account = await this.accounts.findByLinkedId(String(linked));
|
|
67
|
+
if (!account) {
|
|
68
|
+
return { success: false, error: "Account not found", accountID: linked };
|
|
69
|
+
}
|
|
70
|
+
if (this.accounts.isBanExpired(account)) {
|
|
71
|
+
await this.accounts.unban(account.id);
|
|
72
|
+
account.banned = false;
|
|
73
|
+
}
|
|
74
|
+
if (account.banned) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: account.banReason ?? "Account banned",
|
|
78
|
+
accountID: this.getLinkedId(account),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { success: true, accountID: this.getLinkedId(account) };
|
|
82
|
+
}
|
|
83
|
+
async logout(player) {
|
|
84
|
+
// The framework session holds the account binding; clearing meta is enough for now.
|
|
85
|
+
player.setMeta("identity:session", null);
|
|
86
|
+
}
|
|
87
|
+
mergeIdentifiers(credentials, player) {
|
|
88
|
+
const fromCredentials = this.extractIdentifiers(credentials);
|
|
89
|
+
const fromPlayer = this.extractFromPlayer(player);
|
|
90
|
+
return {
|
|
91
|
+
license: fromCredentials.license ?? fromPlayer.license ?? null,
|
|
92
|
+
discord: fromCredentials.discord ?? fromPlayer.discord ?? null,
|
|
93
|
+
steam: fromCredentials.steam ?? fromPlayer.steam ?? null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
extractIdentifiers(input) {
|
|
97
|
+
return {
|
|
98
|
+
license: typeof input.license === "string" ? input.license : null,
|
|
99
|
+
discord: typeof input.discord === "string" ? input.discord : null,
|
|
100
|
+
steam: typeof input.steam === "string" ? input.steam : null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
extractFromPlayer(player) {
|
|
104
|
+
const identifiers = player.getIdentifiers();
|
|
105
|
+
const result = {
|
|
106
|
+
license: null,
|
|
107
|
+
discord: null,
|
|
108
|
+
steam: null,
|
|
109
|
+
};
|
|
110
|
+
for (const raw of identifiers) {
|
|
111
|
+
if (raw.startsWith("license:"))
|
|
112
|
+
result.license = raw.slice("license:".length);
|
|
113
|
+
if (raw.startsWith("discord:"))
|
|
114
|
+
result.discord = raw.slice("discord:".length);
|
|
115
|
+
if (raw.startsWith("steam:"))
|
|
116
|
+
result.steam = raw.slice("steam:".length);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
exports.IdentityAuthProvider = IdentityAuthProvider;
|
|
122
|
+
exports.IdentityAuthProvider = IdentityAuthProvider = __decorate([
|
|
123
|
+
(0, tsyringe_1.injectable)(),
|
|
124
|
+
__metadata("design:paramtypes", [account_service_1.AccountService, framework_1.Server.ConfigService])
|
|
125
|
+
], IdentityAuthProvider);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Server } from "@open-core/framework";
|
|
2
|
+
import { AccountService } from "./account.service";
|
|
3
|
+
import { AccountRepository } from "../repositories/account.repository";
|
|
4
|
+
export declare class IdentityPrincipalProvider implements Server.PrincipalProviderContract {
|
|
5
|
+
private readonly accounts;
|
|
6
|
+
private readonly repo;
|
|
7
|
+
constructor(accounts: AccountService, repo: AccountRepository);
|
|
8
|
+
getPrincipal(player: Server.Player): Promise<Server.Principal | null>;
|
|
9
|
+
refreshPrincipal(player: Server.Player): Promise<void>;
|
|
10
|
+
getPrincipalByLinkedID(linkedID: string): Promise<Server.Principal | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Builds a Principal from account and role.
|
|
13
|
+
* Combines role permissions with account custom permissions.
|
|
14
|
+
*
|
|
15
|
+
* @param account - Account entity
|
|
16
|
+
* @param role - Role entity (or null if no role assigned)
|
|
17
|
+
* @returns Principal with combined permissions
|
|
18
|
+
*/
|
|
19
|
+
private toPrincipal;
|
|
20
|
+
/**
|
|
21
|
+
* Combine role permissions with account custom permissions.
|
|
22
|
+
* Custom permissions starting with '-' negate the base permission.
|
|
23
|
+
*
|
|
24
|
+
* @param role - Role with base permissions
|
|
25
|
+
* @param customPerms - Account custom permissions
|
|
26
|
+
* @returns Combined permissions array
|
|
27
|
+
*/
|
|
28
|
+
private combinePermissions;
|
|
29
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.IdentityPrincipalProvider = void 0;
|
|
13
|
+
const tsyringe_1 = require("tsyringe");
|
|
14
|
+
const framework_1 = require("@open-core/framework");
|
|
15
|
+
const account_service_1 = require("./account.service");
|
|
16
|
+
const account_repository_1 = require("../repositories/account.repository");
|
|
17
|
+
let IdentityPrincipalProvider = class IdentityPrincipalProvider {
|
|
18
|
+
constructor(accounts, repo) {
|
|
19
|
+
this.accounts = accounts;
|
|
20
|
+
this.repo = repo;
|
|
21
|
+
}
|
|
22
|
+
async getPrincipal(player) {
|
|
23
|
+
const linked = player.accountID;
|
|
24
|
+
if (!linked) {
|
|
25
|
+
throw new framework_1.Utils.AppError("UNAUTHORIZED", "Player is not authenticated (no linked account)", "server");
|
|
26
|
+
}
|
|
27
|
+
const result = await this.repo.findByLinkedIdWithRole(String(linked));
|
|
28
|
+
if (!result) {
|
|
29
|
+
throw new framework_1.Utils.AppError("UNAUTHORIZED", "Linked account not found", "server");
|
|
30
|
+
}
|
|
31
|
+
const { account, role } = result;
|
|
32
|
+
if (this.accounts.isBanExpired(account)) {
|
|
33
|
+
await this.accounts.unban(account.id);
|
|
34
|
+
account.banned = false;
|
|
35
|
+
}
|
|
36
|
+
if (account.banned) {
|
|
37
|
+
throw new framework_1.Utils.AppError("PERMISSION_DENIED", "Account is banned", "server", {
|
|
38
|
+
banReason: account.banReason,
|
|
39
|
+
banExpires: account.banExpires,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return this.toPrincipal(account, role);
|
|
43
|
+
}
|
|
44
|
+
async refreshPrincipal(player) {
|
|
45
|
+
const principal = await this.getPrincipal(player);
|
|
46
|
+
player.setMeta("identity:principal", principal);
|
|
47
|
+
}
|
|
48
|
+
async getPrincipalByLinkedID(linkedID) {
|
|
49
|
+
const result = await this.repo.findByLinkedIdWithRole(linkedID);
|
|
50
|
+
if (!result || result.account.banned)
|
|
51
|
+
return null;
|
|
52
|
+
return this.toPrincipal(result.account, result.role);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds a Principal from account and role.
|
|
56
|
+
* Combines role permissions with account custom permissions.
|
|
57
|
+
*
|
|
58
|
+
* @param account - Account entity
|
|
59
|
+
* @param role - Role entity (or null if no role assigned)
|
|
60
|
+
* @returns Principal with combined permissions
|
|
61
|
+
*/
|
|
62
|
+
toPrincipal(account, role) {
|
|
63
|
+
const effectivePermissions = this.combinePermissions(role, account.customPermissions);
|
|
64
|
+
return {
|
|
65
|
+
id: account.linkedId ?? String(account.id),
|
|
66
|
+
name: role?.displayName ?? undefined,
|
|
67
|
+
rank: role?.rank ?? undefined,
|
|
68
|
+
permissions: effectivePermissions,
|
|
69
|
+
meta: {
|
|
70
|
+
accountId: account.id,
|
|
71
|
+
roleId: role?.id,
|
|
72
|
+
roleName: role?.name,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Combine role permissions with account custom permissions.
|
|
78
|
+
* Custom permissions starting with '-' negate the base permission.
|
|
79
|
+
*
|
|
80
|
+
* @param role - Role with base permissions
|
|
81
|
+
* @param customPerms - Account custom permissions
|
|
82
|
+
* @returns Combined permissions array
|
|
83
|
+
*/
|
|
84
|
+
combinePermissions(role, customPerms) {
|
|
85
|
+
const base = new Set(role?.permissions ?? []);
|
|
86
|
+
for (const perm of customPerms) {
|
|
87
|
+
if (perm.startsWith("-")) {
|
|
88
|
+
// Negation: remove the base permission
|
|
89
|
+
base.delete(perm.slice(1));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Addition: add custom permission
|
|
93
|
+
base.add(perm);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return Array.from(base);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
exports.IdentityPrincipalProvider = IdentityPrincipalProvider;
|
|
100
|
+
exports.IdentityPrincipalProvider = IdentityPrincipalProvider = __decorate([
|
|
101
|
+
(0, tsyringe_1.injectable)(),
|
|
102
|
+
__metadata("design:paramtypes", [account_service_1.AccountService,
|
|
103
|
+
account_repository_1.AccountRepository])
|
|
104
|
+
], IdentityPrincipalProvider);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Server } from "@open-core/framework";
|
|
2
|
+
import { MemoryCacheService } from "../cache/memory-cache.service";
|
|
3
|
+
/**
|
|
4
|
+
* API-based principal provider that fetches permissions from external API.
|
|
5
|
+
* Does NOT require local database (uses memory cache only).
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - GETs principal data from external API by linkedId
|
|
9
|
+
* - Caches results in RAM with configurable TTL
|
|
10
|
+
* - Falls back to empty permissions if API fails
|
|
11
|
+
* - Optionally syncs to local DB if configured
|
|
12
|
+
*
|
|
13
|
+
* Expected API endpoint: GET {principalUrl}/principals/{linkedId}
|
|
14
|
+
* Response: { name?: string, rank?: number, permissions: string[], meta?: {...} }
|
|
15
|
+
*/
|
|
16
|
+
export declare class ApiPrincipalProvider implements Server.PrincipalProviderContract {
|
|
17
|
+
private readonly config;
|
|
18
|
+
private readonly http;
|
|
19
|
+
private readonly cache;
|
|
20
|
+
private apiConfig;
|
|
21
|
+
private cacheTtl;
|
|
22
|
+
constructor(config: Server.ConfigService, http: Server.HttpService, cache: MemoryCacheService);
|
|
23
|
+
getPrincipal(player: Server.Player): Promise<Server.Principal | null>;
|
|
24
|
+
refreshPrincipal(player: Server.Player): Promise<void>;
|
|
25
|
+
getPrincipalByLinkedID(linkedID: string): Promise<Server.Principal | null>;
|
|
26
|
+
private parseHeaders;
|
|
27
|
+
}
|