@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.
Files changed (46) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +682 -0
  3. package/dist/entities/account.entity.d.ts +34 -0
  4. package/dist/entities/account.entity.js +2 -0
  5. package/dist/entities/role.entity.d.ts +35 -0
  6. package/dist/entities/role.entity.js +2 -0
  7. package/dist/events/identity.events.d.ts +24 -0
  8. package/dist/events/identity.events.js +2 -0
  9. package/dist/index.d.ts +70 -0
  10. package/dist/index.js +100 -0
  11. package/dist/repositories/account.repository.d.ts +60 -0
  12. package/dist/repositories/account.repository.js +185 -0
  13. package/dist/repositories/role.repository.d.ts +50 -0
  14. package/dist/repositories/role.repository.js +79 -0
  15. package/dist/services/account.service.d.ts +78 -0
  16. package/dist/services/account.service.js +207 -0
  17. package/dist/services/auth/api-auth.provider.d.ts +30 -0
  18. package/dist/services/auth/api-auth.provider.js +134 -0
  19. package/dist/services/auth/credentials-auth.provider.d.ts +27 -0
  20. package/dist/services/auth/credentials-auth.provider.js +214 -0
  21. package/dist/services/auth/local-auth.provider.d.ts +28 -0
  22. package/dist/services/auth/local-auth.provider.js +135 -0
  23. package/dist/services/cache/memory-cache.service.d.ts +47 -0
  24. package/dist/services/cache/memory-cache.service.js +108 -0
  25. package/dist/services/identity-auth.provider.d.ts +18 -0
  26. package/dist/services/identity-auth.provider.js +125 -0
  27. package/dist/services/identity-principal.provider.d.ts +29 -0
  28. package/dist/services/identity-principal.provider.js +104 -0
  29. package/dist/services/principal/api-principal.provider.d.ts +27 -0
  30. package/dist/services/principal/api-principal.provider.js +141 -0
  31. package/dist/services/principal/local-principal.provider.d.ts +39 -0
  32. package/dist/services/principal/local-principal.provider.js +114 -0
  33. package/dist/services/role.service.d.ts +73 -0
  34. package/dist/services/role.service.js +145 -0
  35. package/dist/setup.d.ts +58 -0
  36. package/dist/setup.js +93 -0
  37. package/dist/types/auth.types.d.ts +48 -0
  38. package/dist/types/auth.types.js +2 -0
  39. package/dist/types/index.d.ts +36 -0
  40. package/dist/types/index.js +2 -0
  41. package/migrations/001_accounts_table.sql +16 -0
  42. package/migrations/002_roles_table.sql +21 -0
  43. package/migrations/003_alter_accounts_add_role.sql +24 -0
  44. package/migrations/004_rename_uuid_to_linked_id.sql +12 -0
  45. package/migrations/005_add_password_hash.sql +7 -0
  46. 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);