@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,78 @@
|
|
|
1
|
+
import { Server } from "@open-core/framework";
|
|
2
|
+
import type { Account } from "../entities/account.entity";
|
|
3
|
+
import type { AccountIdentifiers, BanOptions, IdentifierType } from "../types";
|
|
4
|
+
import { AccountRepository } from "../repositories/account.repository";
|
|
5
|
+
import { RoleService } from "./role.service";
|
|
6
|
+
export declare class AccountService {
|
|
7
|
+
private readonly repo;
|
|
8
|
+
private readonly roleService;
|
|
9
|
+
private readonly config;
|
|
10
|
+
constructor(repo: AccountRepository, roleService: RoleService, config: Server.ConfigService);
|
|
11
|
+
findById(id: number): Promise<Account | null>;
|
|
12
|
+
findByLinkedId(linkedId: string): Promise<Account | null>;
|
|
13
|
+
findByIdentifier(type: IdentifierType, value: string): Promise<Account | null>;
|
|
14
|
+
findOrCreate(identifiers: AccountIdentifiers): Promise<{
|
|
15
|
+
account: Account;
|
|
16
|
+
isNew: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
ban(accountId: number, options: BanOptions): Promise<void>;
|
|
19
|
+
unban(accountId: number): Promise<void>;
|
|
20
|
+
isBanExpired(account: Account): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Add a custom permission to an account (override/additional to role).
|
|
23
|
+
*
|
|
24
|
+
* @param accountId - Account ID
|
|
25
|
+
* @param permission - Permission string to add
|
|
26
|
+
*/
|
|
27
|
+
addCustomPermission(accountId: number, permission: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Remove a custom permission from an account.
|
|
30
|
+
*
|
|
31
|
+
* @param accountId - Account ID
|
|
32
|
+
* @param permission - Permission string to remove
|
|
33
|
+
*/
|
|
34
|
+
removeCustomPermission(accountId: number, permission: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Get effective permissions for an account (role + custom).
|
|
37
|
+
*
|
|
38
|
+
* @param accountId - Account ID
|
|
39
|
+
* @returns Combined permissions array
|
|
40
|
+
*/
|
|
41
|
+
getEffectivePermissions(accountId: number): Promise<string[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Assign a role to an account.
|
|
44
|
+
*
|
|
45
|
+
* @param accountId - Account ID
|
|
46
|
+
* @param roleId - Role ID to assign (null to remove role)
|
|
47
|
+
*/
|
|
48
|
+
assignRole(accountId: number, roleId: number | null): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Combine role permissions with account custom permissions.
|
|
51
|
+
* Custom permissions starting with '-' negate the base permission.
|
|
52
|
+
*
|
|
53
|
+
* @param role - Role with base permissions
|
|
54
|
+
* @param customPerms - Account custom permissions
|
|
55
|
+
* @returns Combined permissions array
|
|
56
|
+
*/
|
|
57
|
+
private combinePermissions;
|
|
58
|
+
touchLastLogin(accountId: number, date?: Date): Promise<void>;
|
|
59
|
+
private lookupExisting;
|
|
60
|
+
private identifierPriority;
|
|
61
|
+
/**
|
|
62
|
+
* Find account by username (for credentials auth)
|
|
63
|
+
* Note: This is a basic implementation. For production, add an index on username.
|
|
64
|
+
*/
|
|
65
|
+
findByUsername(_username: string): Promise<Account | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Update account identifiers (for credentials auth identifier merging)
|
|
68
|
+
*/
|
|
69
|
+
updateIdentifiers(): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Create account with credentials (for CredentialsAuthProvider)
|
|
72
|
+
*/
|
|
73
|
+
createWithCredentials(input: {
|
|
74
|
+
username: string;
|
|
75
|
+
passwordHash: string;
|
|
76
|
+
identifiers: AccountIdentifiers;
|
|
77
|
+
}): Promise<Account>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
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.AccountService = void 0;
|
|
13
|
+
const tsyringe_1 = require("tsyringe");
|
|
14
|
+
const framework_1 = require("@open-core/framework");
|
|
15
|
+
const crypto_1 = require("crypto");
|
|
16
|
+
const account_repository_1 = require("../repositories/account.repository");
|
|
17
|
+
const role_service_1 = require("./role.service");
|
|
18
|
+
let AccountService = class AccountService {
|
|
19
|
+
constructor(repo, roleService, config) {
|
|
20
|
+
this.repo = repo;
|
|
21
|
+
this.roleService = roleService;
|
|
22
|
+
this.config = config;
|
|
23
|
+
}
|
|
24
|
+
async findById(id) {
|
|
25
|
+
return this.repo.findById(id);
|
|
26
|
+
}
|
|
27
|
+
async findByLinkedId(linkedId) {
|
|
28
|
+
return this.repo.findByLinkedId(linkedId);
|
|
29
|
+
}
|
|
30
|
+
async findByIdentifier(type, value) {
|
|
31
|
+
return this.repo.findByIdentifier(type, value);
|
|
32
|
+
}
|
|
33
|
+
async findOrCreate(identifiers) {
|
|
34
|
+
const existing = await this.lookupExisting(identifiers);
|
|
35
|
+
if (existing) {
|
|
36
|
+
return { account: existing, isNew: false };
|
|
37
|
+
}
|
|
38
|
+
// Get default role for new accounts
|
|
39
|
+
const defaultRole = await this.roleService.getDefaultRole();
|
|
40
|
+
// Auto-generate linkedId by default (UUID format for local accounts)
|
|
41
|
+
const created = await this.repo.createAccount({
|
|
42
|
+
linkedId: (0, crypto_1.randomUUID)(),
|
|
43
|
+
externalSource: "local",
|
|
44
|
+
license: identifiers.license ?? null,
|
|
45
|
+
discord: identifiers.discord ?? null,
|
|
46
|
+
steam: identifiers.steam ?? null,
|
|
47
|
+
username: null,
|
|
48
|
+
roleId: defaultRole?.id ?? null,
|
|
49
|
+
});
|
|
50
|
+
return { account: created, isNew: true };
|
|
51
|
+
}
|
|
52
|
+
async ban(accountId, options) {
|
|
53
|
+
const expires = options.durationMs
|
|
54
|
+
? new Date(Date.now() + options.durationMs)
|
|
55
|
+
: null;
|
|
56
|
+
await this.repo.setBan(accountId, true, options.reason ?? "Banned", expires);
|
|
57
|
+
}
|
|
58
|
+
async unban(accountId) {
|
|
59
|
+
await this.repo.setBan(accountId, false, null, null);
|
|
60
|
+
}
|
|
61
|
+
isBanExpired(account) {
|
|
62
|
+
if (!account.banned)
|
|
63
|
+
return false;
|
|
64
|
+
if (!account.banExpires)
|
|
65
|
+
return false;
|
|
66
|
+
return account.banExpires.getTime() <= Date.now();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Add a custom permission to an account (override/additional to role).
|
|
70
|
+
*
|
|
71
|
+
* @param accountId - Account ID
|
|
72
|
+
* @param permission - Permission string to add
|
|
73
|
+
*/
|
|
74
|
+
async addCustomPermission(accountId, permission) {
|
|
75
|
+
const account = await this.repo.findById(accountId);
|
|
76
|
+
if (!account)
|
|
77
|
+
return;
|
|
78
|
+
const permissions = new Set(account.customPermissions ?? []);
|
|
79
|
+
permissions.add(permission);
|
|
80
|
+
await this.repo.updateCustomPermissions(accountId, Array.from(permissions));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Remove a custom permission from an account.
|
|
84
|
+
*
|
|
85
|
+
* @param accountId - Account ID
|
|
86
|
+
* @param permission - Permission string to remove
|
|
87
|
+
*/
|
|
88
|
+
async removeCustomPermission(accountId, permission) {
|
|
89
|
+
const account = await this.repo.findById(accountId);
|
|
90
|
+
if (!account)
|
|
91
|
+
return;
|
|
92
|
+
const filtered = (account.customPermissions ?? []).filter((p) => p !== permission && p !== `-${permission}`);
|
|
93
|
+
await this.repo.updateCustomPermissions(accountId, filtered);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get effective permissions for an account (role + custom).
|
|
97
|
+
*
|
|
98
|
+
* @param accountId - Account ID
|
|
99
|
+
* @returns Combined permissions array
|
|
100
|
+
*/
|
|
101
|
+
async getEffectivePermissions(accountId) {
|
|
102
|
+
const result = await this.repo.findByIdWithRole(accountId);
|
|
103
|
+
if (!result)
|
|
104
|
+
return [];
|
|
105
|
+
return this.combinePermissions(result.role, result.account.customPermissions);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Assign a role to an account.
|
|
109
|
+
*
|
|
110
|
+
* @param accountId - Account ID
|
|
111
|
+
* @param roleId - Role ID to assign (null to remove role)
|
|
112
|
+
*/
|
|
113
|
+
async assignRole(accountId, roleId) {
|
|
114
|
+
await this.repo.updateRole(accountId, roleId);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Combine role permissions with account custom permissions.
|
|
118
|
+
* Custom permissions starting with '-' negate the base permission.
|
|
119
|
+
*
|
|
120
|
+
* @param role - Role with base permissions
|
|
121
|
+
* @param customPerms - Account custom permissions
|
|
122
|
+
* @returns Combined permissions array
|
|
123
|
+
*/
|
|
124
|
+
combinePermissions(role, customPerms) {
|
|
125
|
+
const base = new Set(role?.permissions ?? []);
|
|
126
|
+
for (const perm of customPerms) {
|
|
127
|
+
if (perm.startsWith("-")) {
|
|
128
|
+
// Negation: remove the base permission
|
|
129
|
+
base.delete(perm.slice(1));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Addition: add custom permission
|
|
133
|
+
base.add(perm);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return Array.from(base);
|
|
137
|
+
}
|
|
138
|
+
async touchLastLogin(accountId, date = new Date()) {
|
|
139
|
+
await this.repo.updateLastLogin(accountId, date);
|
|
140
|
+
}
|
|
141
|
+
async lookupExisting(identifiers) {
|
|
142
|
+
const ordered = this.identifierPriority();
|
|
143
|
+
for (const key of ordered) {
|
|
144
|
+
const value = identifiers[key];
|
|
145
|
+
if (!value)
|
|
146
|
+
continue;
|
|
147
|
+
const found = await this.repo.findByIdentifier(key, value);
|
|
148
|
+
if (found)
|
|
149
|
+
return found;
|
|
150
|
+
}
|
|
151
|
+
// Fallback: try any provided identifiers
|
|
152
|
+
for (const [key, value] of Object.entries(identifiers)) {
|
|
153
|
+
if (!value)
|
|
154
|
+
continue;
|
|
155
|
+
const found = await this.repo.findByIdentifier(key, value);
|
|
156
|
+
if (found)
|
|
157
|
+
return found;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
identifierPriority() {
|
|
162
|
+
const fromConfig = this.config.get("identity_primary_identifier", "license");
|
|
163
|
+
const base = ["license", "discord", "steam"];
|
|
164
|
+
return [fromConfig, ...base.filter((id) => id !== fromConfig)];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Find account by username (for credentials auth)
|
|
168
|
+
* Note: This is a basic implementation. For production, add an index on username.
|
|
169
|
+
*/
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
171
|
+
async findByUsername(_username) {
|
|
172
|
+
// TODO: Add findByUsername method to AccountRepository for better performance
|
|
173
|
+
// For now, this is a placeholder that credentials auth will need
|
|
174
|
+
throw new Error("findByUsername not implemented - add to AccountRepository for credentials auth");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Update account identifiers (for credentials auth identifier merging)
|
|
178
|
+
*/
|
|
179
|
+
async updateIdentifiers() {
|
|
180
|
+
// This is a placeholder - in production you'd want a proper update method
|
|
181
|
+
// For now, credentials auth can work without this
|
|
182
|
+
console.warn("updateIdentifiers not fully implemented - identifiers not merged");
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Create account with credentials (for CredentialsAuthProvider)
|
|
186
|
+
*/
|
|
187
|
+
async createWithCredentials(input) {
|
|
188
|
+
const defaultRole = await this.roleService.getDefaultRole();
|
|
189
|
+
// Note: This creates an account without password_hash field
|
|
190
|
+
// You'll need to add password_hash to Account entity and migration 005
|
|
191
|
+
return this.repo.createAccount({
|
|
192
|
+
linkedId: (0, crypto_1.randomUUID)(),
|
|
193
|
+
externalSource: "credentials",
|
|
194
|
+
username: input.username,
|
|
195
|
+
license: input.identifiers.license ?? null,
|
|
196
|
+
discord: input.identifiers.discord ?? null,
|
|
197
|
+
steam: input.identifiers.steam ?? null,
|
|
198
|
+
roleId: defaultRole?.id ?? null,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
exports.AccountService = AccountService;
|
|
203
|
+
exports.AccountService = AccountService = __decorate([
|
|
204
|
+
(0, tsyringe_1.injectable)(),
|
|
205
|
+
__metadata("design:paramtypes", [account_repository_1.AccountRepository,
|
|
206
|
+
role_service_1.RoleService, framework_1.Server.ConfigService])
|
|
207
|
+
], AccountService);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Server } from "@open-core/framework";
|
|
2
|
+
import { MemoryCacheService } from "../cache/memory-cache.service";
|
|
3
|
+
/**
|
|
4
|
+
* API-based authentication provider that delegates auth to external API.
|
|
5
|
+
* Does NOT require local database (uses memory cache only).
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - POSTs credentials + identifiers to external API
|
|
9
|
+
* - Receives linkedId from API (no local generation)
|
|
10
|
+
* - Caches auth results in RAM with configurable TTL
|
|
11
|
+
* - Optionally syncs to local DB if configured
|
|
12
|
+
*
|
|
13
|
+
* Expected API endpoint: POST {apiUrl}/auth
|
|
14
|
+
* Request: { credentials: {...}, identifiers: {license, discord, steam} }
|
|
15
|
+
* Response: { success: boolean, linkedId?: string, error?: string }
|
|
16
|
+
*/
|
|
17
|
+
export declare class ApiAuthProvider implements Server.AuthProviderContract {
|
|
18
|
+
private readonly config;
|
|
19
|
+
private readonly http;
|
|
20
|
+
private readonly cache;
|
|
21
|
+
private apiConfig;
|
|
22
|
+
private cacheTtl;
|
|
23
|
+
constructor(config: Server.ConfigService, http: Server.HttpService, cache: MemoryCacheService);
|
|
24
|
+
authenticate(player: Server.Player, credentials: Record<string, unknown>): Promise<Server.AuthResult>;
|
|
25
|
+
register(player: Server.Player, credentials: Record<string, unknown>): Promise<Server.AuthResult>;
|
|
26
|
+
validateSession(player: Server.Player): Promise<Server.AuthResult>;
|
|
27
|
+
logout(player: Server.Player): Promise<void>;
|
|
28
|
+
private extractFromPlayer;
|
|
29
|
+
private parseHeaders;
|
|
30
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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.ApiAuthProvider = void 0;
|
|
13
|
+
const tsyringe_1 = require("tsyringe");
|
|
14
|
+
const framework_1 = require("@open-core/framework");
|
|
15
|
+
const memory_cache_service_1 = require("../cache/memory-cache.service");
|
|
16
|
+
/**
|
|
17
|
+
* API-based authentication provider that delegates auth to external API.
|
|
18
|
+
* Does NOT require local database (uses memory cache only).
|
|
19
|
+
*
|
|
20
|
+
* Features:
|
|
21
|
+
* - POSTs credentials + identifiers to external API
|
|
22
|
+
* - Receives linkedId from API (no local generation)
|
|
23
|
+
* - Caches auth results in RAM with configurable TTL
|
|
24
|
+
* - Optionally syncs to local DB if configured
|
|
25
|
+
*
|
|
26
|
+
* Expected API endpoint: POST {apiUrl}/auth
|
|
27
|
+
* Request: { credentials: {...}, identifiers: {license, discord, steam} }
|
|
28
|
+
* Response: { success: boolean, linkedId?: string, error?: string }
|
|
29
|
+
*/
|
|
30
|
+
let ApiAuthProvider = class ApiAuthProvider {
|
|
31
|
+
constructor(config, http, cache) {
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.http = http;
|
|
34
|
+
this.cache = cache;
|
|
35
|
+
// Load API configuration from convars
|
|
36
|
+
this.apiConfig = {
|
|
37
|
+
authUrl: this.config.get("identity_api_auth_url", "http://localhost:3000/api/auth"),
|
|
38
|
+
headers: this.parseHeaders(this.config.get("identity_api_headers", "")),
|
|
39
|
+
timeoutMs: this.config.getNumber("identity_api_timeout", 5000),
|
|
40
|
+
};
|
|
41
|
+
this.cacheTtl = this.config.getNumber("identity_cache_ttl", 300000); // 5 min default
|
|
42
|
+
}
|
|
43
|
+
async authenticate(player, credentials) {
|
|
44
|
+
const identifiers = this.extractFromPlayer(player);
|
|
45
|
+
try {
|
|
46
|
+
const response = await this.http.post(this.apiConfig.authUrl, {
|
|
47
|
+
credentials,
|
|
48
|
+
identifiers,
|
|
49
|
+
}, {
|
|
50
|
+
headers: this.apiConfig.headers,
|
|
51
|
+
timeoutMs: this.apiConfig.timeoutMs,
|
|
52
|
+
});
|
|
53
|
+
if (!response.success || !response.linkedId) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: response.error ?? "Authentication failed",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Cache the auth result
|
|
60
|
+
this.cache.set(`auth:${response.linkedId}`, { linkedId: response.linkedId, identifiers }, this.cacheTtl);
|
|
61
|
+
// Link account
|
|
62
|
+
player.linkAccount(response.linkedId);
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
accountID: response.linkedId,
|
|
66
|
+
isNewAccount: response.isNewAccount ?? false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: error instanceof Error ? error.message : "API authentication failed",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async register(player, credentials) {
|
|
77
|
+
// Registration is the same as authentication for API-based auth
|
|
78
|
+
// The external API decides if it's a new account or not
|
|
79
|
+
return this.authenticate(player, credentials);
|
|
80
|
+
}
|
|
81
|
+
async validateSession(player) {
|
|
82
|
+
const linked = player.accountID;
|
|
83
|
+
if (!linked) {
|
|
84
|
+
return { success: false, error: "Not authenticated" };
|
|
85
|
+
}
|
|
86
|
+
// Check cache first
|
|
87
|
+
const cached = this.cache.get(`auth:${linked}`);
|
|
88
|
+
if (cached) {
|
|
89
|
+
return { success: true, accountID: cached.linkedId };
|
|
90
|
+
}
|
|
91
|
+
// If not in cache, we could optionally validate with API
|
|
92
|
+
// For now, we trust the linkedId if it exists
|
|
93
|
+
return { success: true, accountID: String(linked) };
|
|
94
|
+
}
|
|
95
|
+
async logout(player) {
|
|
96
|
+
const linked = player.accountID;
|
|
97
|
+
if (linked) {
|
|
98
|
+
this.cache.delete(`auth:${linked}`);
|
|
99
|
+
}
|
|
100
|
+
player.setMeta("identity:session", null);
|
|
101
|
+
}
|
|
102
|
+
extractFromPlayer(player) {
|
|
103
|
+
const identifiers = player.getIdentifiers();
|
|
104
|
+
const result = {
|
|
105
|
+
license: null,
|
|
106
|
+
discord: null,
|
|
107
|
+
steam: null,
|
|
108
|
+
};
|
|
109
|
+
for (const raw of identifiers) {
|
|
110
|
+
if (raw.startsWith("license:"))
|
|
111
|
+
result.license = raw.slice("license:".length);
|
|
112
|
+
if (raw.startsWith("discord:"))
|
|
113
|
+
result.discord = raw.slice("discord:".length);
|
|
114
|
+
if (raw.startsWith("steam:"))
|
|
115
|
+
result.steam = raw.slice("steam:".length);
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
parseHeaders(headersString) {
|
|
120
|
+
if (!headersString)
|
|
121
|
+
return {};
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(headersString);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
exports.ApiAuthProvider = ApiAuthProvider;
|
|
131
|
+
exports.ApiAuthProvider = ApiAuthProvider = __decorate([
|
|
132
|
+
(0, tsyringe_1.injectable)(),
|
|
133
|
+
__metadata("design:paramtypes", [framework_1.Server.ConfigService, framework_1.Server.HttpService, memory_cache_service_1.MemoryCacheService])
|
|
134
|
+
], ApiAuthProvider);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Server } from "@open-core/framework";
|
|
2
|
+
import { AccountService } from "../account.service";
|
|
3
|
+
/**
|
|
4
|
+
* Credentials-based authentication provider using username/password.
|
|
5
|
+
* Requires password_hash column in accounts table (migration 005).
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Validates username and password
|
|
9
|
+
* - Uses bcrypt for password hashing
|
|
10
|
+
* - Does NOT auto-create accounts (must be registered first)
|
|
11
|
+
* - Can optionally merge FiveM identifiers after authentication
|
|
12
|
+
*/
|
|
13
|
+
export declare class CredentialsAuthProvider implements Server.AuthProviderContract {
|
|
14
|
+
private readonly accounts;
|
|
15
|
+
private readonly config;
|
|
16
|
+
private readonly saltRounds;
|
|
17
|
+
constructor(accounts: AccountService, config: Server.ConfigService);
|
|
18
|
+
/**
|
|
19
|
+
* Get the linked account ID (linkedId or numeric ID as fallback)
|
|
20
|
+
*/
|
|
21
|
+
private getLinkedId;
|
|
22
|
+
authenticate(player: Server.Player, credentials: Record<string, unknown>): Promise<Server.AuthResult>;
|
|
23
|
+
register(player: Server.Player, credentials: Record<string, unknown>): Promise<Server.AuthResult>;
|
|
24
|
+
validateSession(player: Server.Player): Promise<Server.AuthResult>;
|
|
25
|
+
logout(player: Server.Player): Promise<void>;
|
|
26
|
+
private extractFromPlayer;
|
|
27
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
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;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
42
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.CredentialsAuthProvider = void 0;
|
|
46
|
+
const tsyringe_1 = require("tsyringe");
|
|
47
|
+
const framework_1 = require("@open-core/framework");
|
|
48
|
+
const bcrypt = __importStar(require("bcrypt"));
|
|
49
|
+
const account_service_1 = require("../account.service");
|
|
50
|
+
/**
|
|
51
|
+
* Credentials-based authentication provider using username/password.
|
|
52
|
+
* Requires password_hash column in accounts table (migration 005).
|
|
53
|
+
*
|
|
54
|
+
* Features:
|
|
55
|
+
* - Validates username and password
|
|
56
|
+
* - Uses bcrypt for password hashing
|
|
57
|
+
* - Does NOT auto-create accounts (must be registered first)
|
|
58
|
+
* - Can optionally merge FiveM identifiers after authentication
|
|
59
|
+
*/
|
|
60
|
+
let CredentialsAuthProvider = class CredentialsAuthProvider {
|
|
61
|
+
constructor(accounts, config) {
|
|
62
|
+
this.accounts = accounts;
|
|
63
|
+
this.config = config;
|
|
64
|
+
this.saltRounds = 10;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the linked account ID (linkedId or numeric ID as fallback)
|
|
68
|
+
*/
|
|
69
|
+
getLinkedId(account) {
|
|
70
|
+
return account.linkedId ?? String(account.id);
|
|
71
|
+
}
|
|
72
|
+
async authenticate(player, credentials) {
|
|
73
|
+
const username = credentials.username;
|
|
74
|
+
const password = credentials.password;
|
|
75
|
+
if (!username || !password) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: "Username and password are required",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Find account by username
|
|
82
|
+
const account = await this.accounts.findByUsername(username);
|
|
83
|
+
if (!account) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: "Invalid credentials",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Validate password (requires password_hash in Account entity)
|
|
90
|
+
const passwordHash = account
|
|
91
|
+
.passwordHash;
|
|
92
|
+
if (!passwordHash) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: "Account has no password set",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const isValid = await bcrypt.compare(password, passwordHash);
|
|
99
|
+
if (!isValid) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: "Invalid credentials",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Check ban status
|
|
106
|
+
if (this.accounts.isBanExpired(account)) {
|
|
107
|
+
await this.accounts.unban(account.id);
|
|
108
|
+
account.banned = false;
|
|
109
|
+
}
|
|
110
|
+
if (account.banned) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
error: account.banReason ?? "Account banned",
|
|
114
|
+
accountID: this.getLinkedId(account),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Link account and update last login
|
|
118
|
+
const linkedId = this.getLinkedId(account);
|
|
119
|
+
player.linkAccount(linkedId);
|
|
120
|
+
await this.accounts.touchLastLogin(account.id);
|
|
121
|
+
// Optionally merge FiveM identifiers
|
|
122
|
+
const shouldMergeIdentifiers = this.config.getBoolean("identity_merge_identifiers", false);
|
|
123
|
+
if (shouldMergeIdentifiers) {
|
|
124
|
+
// TODO: Implement updateIdentifiers in AccountService
|
|
125
|
+
// const identifiers = this.extractFromPlayer(player);
|
|
126
|
+
// await this.accounts.updateIdentifiers(account.id, identifiers);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
accountID: linkedId,
|
|
131
|
+
isNewAccount: false,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async register(player, credentials) {
|
|
135
|
+
const username = credentials.username;
|
|
136
|
+
const password = credentials.password;
|
|
137
|
+
if (!username || !password) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: "Username and password are required",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Check if username already exists
|
|
144
|
+
const existing = await this.accounts.findByUsername(username);
|
|
145
|
+
if (existing) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: "Username already taken",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Hash password
|
|
152
|
+
const passwordHash = await bcrypt.hash(password, this.saltRounds);
|
|
153
|
+
// Create account with password
|
|
154
|
+
const account = await this.accounts.createWithCredentials({
|
|
155
|
+
username,
|
|
156
|
+
passwordHash,
|
|
157
|
+
identifiers: this.extractFromPlayer(player),
|
|
158
|
+
});
|
|
159
|
+
const linkedId = this.getLinkedId(account);
|
|
160
|
+
player.linkAccount(linkedId);
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
accountID: linkedId,
|
|
164
|
+
isNewAccount: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async validateSession(player) {
|
|
168
|
+
const linked = player.accountID;
|
|
169
|
+
if (!linked) {
|
|
170
|
+
return { success: false, error: "Not authenticated" };
|
|
171
|
+
}
|
|
172
|
+
const account = await this.accounts.findByLinkedId(String(linked));
|
|
173
|
+
if (!account) {
|
|
174
|
+
return { success: false, error: "Account not found", accountID: linked };
|
|
175
|
+
}
|
|
176
|
+
if (this.accounts.isBanExpired(account)) {
|
|
177
|
+
await this.accounts.unban(account.id);
|
|
178
|
+
account.banned = false;
|
|
179
|
+
}
|
|
180
|
+
if (account.banned) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: account.banReason ?? "Account banned",
|
|
184
|
+
accountID: this.getLinkedId(account),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { success: true, accountID: this.getLinkedId(account) };
|
|
188
|
+
}
|
|
189
|
+
async logout(player) {
|
|
190
|
+
player.setMeta("identity:session", null);
|
|
191
|
+
}
|
|
192
|
+
extractFromPlayer(player) {
|
|
193
|
+
const identifiers = player.getIdentifiers();
|
|
194
|
+
const result = {
|
|
195
|
+
license: null,
|
|
196
|
+
discord: null,
|
|
197
|
+
steam: null,
|
|
198
|
+
};
|
|
199
|
+
for (const raw of identifiers) {
|
|
200
|
+
if (raw.startsWith("license:"))
|
|
201
|
+
result.license = raw.slice("license:".length);
|
|
202
|
+
if (raw.startsWith("discord:"))
|
|
203
|
+
result.discord = raw.slice("discord:".length);
|
|
204
|
+
if (raw.startsWith("steam:"))
|
|
205
|
+
result.steam = raw.slice("steam:".length);
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
exports.CredentialsAuthProvider = CredentialsAuthProvider;
|
|
211
|
+
exports.CredentialsAuthProvider = CredentialsAuthProvider = __decorate([
|
|
212
|
+
(0, tsyringe_1.injectable)(),
|
|
213
|
+
__metadata("design:paramtypes", [account_service_1.AccountService, framework_1.Server.ConfigService])
|
|
214
|
+
], CredentialsAuthProvider);
|