@objectstack/plugin-auth 2.0.2 → 2.0.3
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/.turbo/turbo-build.log +10 -10
- package/ARCHITECTURE.md +176 -0
- package/CHANGELOG.md +10 -0
- package/IMPLEMENTATION_SUMMARY.md +69 -27
- package/README.md +153 -25
- package/dist/index.d.mts +7485 -5
- package/dist/index.d.ts +7485 -5
- package/dist/index.js +558 -76
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +551 -75
- package/dist/index.mjs.map +1 -1
- package/examples/basic-usage.ts +36 -24
- package/package.json +4 -11
- package/src/auth-manager.ts +164 -0
- package/src/auth-plugin.test.ts +14 -4
- package/src/auth-plugin.ts +71 -99
- package/src/index.ts +5 -1
- package/src/objectql-adapter.ts +181 -0
- package/src/objects/auth-account.object.ts +121 -0
- package/src/objects/auth-session.object.ts +89 -0
- package/src/objects/auth-user.object.ts +97 -0
- package/src/objects/auth-verification.object.ts +78 -0
- package/src/objects/index.ts +13 -0
package/src/auth-plugin.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
|
|
4
4
|
import { AuthConfig } from '@objectstack/spec/system';
|
|
5
|
+
import { AuthManager } from './auth-manager.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Auth Plugin Options
|
|
@@ -37,9 +38,9 @@ export interface AuthPluginOptions extends Partial<AuthConfig> {
|
|
|
37
38
|
* - `auth` service (auth manager instance)
|
|
38
39
|
* - HTTP routes for authentication endpoints
|
|
39
40
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* Integrates with better-auth library to provide comprehensive
|
|
42
|
+
* authentication capabilities including email/password, OAuth, 2FA,
|
|
43
|
+
* magic links, passkeys, and organization support.
|
|
43
44
|
*/
|
|
44
45
|
export class AuthPlugin implements Plugin {
|
|
45
46
|
name = 'com.objectstack.auth';
|
|
@@ -66,8 +67,17 @@ export class AuthPlugin implements Plugin {
|
|
|
66
67
|
throw new Error('AuthPlugin: secret is required');
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
//
|
|
70
|
-
|
|
70
|
+
// Get data engine service for database operations
|
|
71
|
+
const dataEngine = ctx.getService<any>('data');
|
|
72
|
+
if (!dataEngine) {
|
|
73
|
+
ctx.logger.warn('No data engine service found - auth will use in-memory storage');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Initialize auth manager with data engine
|
|
77
|
+
this.authManager = new AuthManager({
|
|
78
|
+
...this.options,
|
|
79
|
+
dataEngine,
|
|
80
|
+
});
|
|
71
81
|
|
|
72
82
|
// Register auth service
|
|
73
83
|
ctx.registerService('auth', this.authManager);
|
|
@@ -105,118 +115,80 @@ export class AuthPlugin implements Plugin {
|
|
|
105
115
|
|
|
106
116
|
/**
|
|
107
117
|
* Register authentication routes with HTTP server
|
|
118
|
+
*
|
|
119
|
+
* Uses better-auth's universal handler for all authentication requests.
|
|
120
|
+
* This forwards all requests under basePath to better-auth, which handles:
|
|
121
|
+
* - Email/password authentication
|
|
122
|
+
* - OAuth providers (Google, GitHub, etc.)
|
|
123
|
+
* - Session management
|
|
124
|
+
* - Password reset
|
|
125
|
+
* - Email verification
|
|
126
|
+
* - 2FA, passkeys, magic links (if enabled)
|
|
108
127
|
*/
|
|
109
128
|
private registerAuthRoutes(httpServer: IHttpServer, ctx: PluginContext): void {
|
|
110
129
|
if (!this.authManager) return;
|
|
111
130
|
|
|
112
131
|
const basePath = this.options.basePath || '/api/v1/auth';
|
|
113
132
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
res.status(401).json({
|
|
124
|
-
success: false,
|
|
125
|
-
error: err.message,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
});
|
|
133
|
+
// Get raw Hono app to use native wildcard routing
|
|
134
|
+
// Type assertion is safe here because we explicitly require Hono server as a dependency
|
|
135
|
+
if (!('getRawApp' in httpServer) || typeof (httpServer as any).getRawApp !== 'function') {
|
|
136
|
+
ctx.logger.error('HTTP server does not support getRawApp() - wildcard routing requires Hono server');
|
|
137
|
+
throw new Error(
|
|
138
|
+
'AuthPlugin requires HonoServerPlugin for wildcard routing support. ' +
|
|
139
|
+
'Please ensure HonoServerPlugin is loaded before AuthPlugin.'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
129
142
|
|
|
130
|
-
|
|
131
|
-
httpServer.post(`${basePath}/register`, async (req, res) => {
|
|
132
|
-
try {
|
|
133
|
-
const body = req.body;
|
|
134
|
-
const result = await this.authManager!.register(body);
|
|
135
|
-
res.status(201).json(result);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
138
|
-
ctx.logger.error('Registration error:', err);
|
|
139
|
-
res.status(400).json({
|
|
140
|
-
success: false,
|
|
141
|
-
error: err.message,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
});
|
|
143
|
+
const rawApp = (httpServer as any).getRawApp();
|
|
145
144
|
|
|
146
|
-
//
|
|
147
|
-
|
|
145
|
+
// Register wildcard route to forward all auth requests to better-auth
|
|
146
|
+
// Better-auth expects requests at its baseURL, so we need to preserve the full path
|
|
147
|
+
rawApp.all(`${basePath}/*`, async (c: any) => {
|
|
148
148
|
try {
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
149
|
+
// Get the Web standard Request from Hono context
|
|
150
|
+
const request = c.req.raw as Request;
|
|
151
|
+
|
|
152
|
+
// Create a new Request with the path rewritten to match better-auth's expectations
|
|
153
|
+
// Better-auth expects paths like /sign-in/email, /sign-up/email, etc.
|
|
154
|
+
// We need to strip our basePath prefix
|
|
155
|
+
const url = new URL(request.url);
|
|
156
|
+
const authPath = url.pathname.replace(basePath, '');
|
|
157
|
+
const rewrittenUrl = new URL(authPath || '/', url.origin);
|
|
158
|
+
rewrittenUrl.search = url.search; // Preserve query params
|
|
159
|
+
|
|
160
|
+
const rewrittenRequest = new Request(rewrittenUrl, {
|
|
161
|
+
method: request.method,
|
|
162
|
+
headers: request.headers,
|
|
163
|
+
body: request.body,
|
|
164
|
+
duplex: 'half' as any, // Required for Request with body
|
|
159
165
|
});
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const token = typeof authHeader === 'string' ? authHeader.replace('Bearer ', '') : undefined;
|
|
168
|
-
const session = await this.authManager!.getSession(token);
|
|
169
|
-
res.status(200).json({ success: true, data: session });
|
|
167
|
+
// Forward to better-auth handler
|
|
168
|
+
const response = await this.authManager!.handleRequest(rewrittenRequest);
|
|
169
|
+
|
|
170
|
+
return response;
|
|
170
171
|
} catch (error) {
|
|
171
172
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
ctx.logger.error('Auth request error:', err);
|
|
174
|
+
|
|
175
|
+
// Return error response
|
|
176
|
+
return new Response(
|
|
177
|
+
JSON.stringify({
|
|
178
|
+
success: false,
|
|
179
|
+
error: err.message,
|
|
180
|
+
}),
|
|
181
|
+
{
|
|
182
|
+
status: 500,
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
}
|
|
185
|
+
);
|
|
176
186
|
}
|
|
177
187
|
});
|
|
178
188
|
|
|
179
|
-
ctx.logger.
|
|
180
|
-
basePath,
|
|
181
|
-
routes: [
|
|
182
|
-
`POST ${basePath}/login`,
|
|
183
|
-
`POST ${basePath}/register`,
|
|
184
|
-
`POST ${basePath}/logout`,
|
|
185
|
-
`GET ${basePath}/session`,
|
|
186
|
-
],
|
|
187
|
-
});
|
|
189
|
+
ctx.logger.info(`Auth routes registered: All requests under ${basePath}/* forwarded to better-auth`);
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
/**
|
|
192
|
-
* Auth Manager
|
|
193
|
-
*
|
|
194
|
-
* @planned This is a stub implementation. Real authentication logic
|
|
195
|
-
* will be implemented using better-auth or similar library in future versions.
|
|
196
|
-
*/
|
|
197
|
-
class AuthManager {
|
|
198
|
-
constructor(_config: AuthPluginOptions) {
|
|
199
|
-
// Store config for future use
|
|
200
|
-
}
|
|
201
193
|
|
|
202
|
-
async login(_credentials: any): Promise<any> {
|
|
203
|
-
// @planned Implement actual login logic with better-auth
|
|
204
|
-
throw new Error('Login not yet implemented');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async register(_userData: any): Promise<any> {
|
|
208
|
-
// @planned Implement actual registration logic with better-auth
|
|
209
|
-
throw new Error('Registration not yet implemented');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async logout(_token?: string): Promise<void> {
|
|
213
|
-
// @planned Implement actual logout logic
|
|
214
|
-
throw new Error('Logout not yet implemented');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async getSession(_token?: string): Promise<any> {
|
|
218
|
-
// @planned Implement actual session retrieval
|
|
219
|
-
throw new Error('Session retrieval not yet implemented');
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
194
|
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Authentication & Identity Plugin for ObjectStack
|
|
7
7
|
* Powered by better-auth for robust, secure authentication
|
|
8
|
+
* Uses ObjectQL for data persistence (no third-party ORM required)
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
export * from './auth-plugin';
|
|
11
|
+
export * from './auth-plugin.js';
|
|
12
|
+
export * from './auth-manager.js';
|
|
13
|
+
export * from './objectql-adapter.js';
|
|
14
|
+
export * from './objects/index.js';
|
|
11
15
|
export type { AuthConfig, AuthProviderConfig, AuthPluginConfig } from '@objectstack/spec/system';
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type { IDataEngine } from '@objectstack/core';
|
|
4
|
+
import type { CleanedWhere } from 'better-auth/adapters';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ObjectQL Adapter for better-auth
|
|
8
|
+
*
|
|
9
|
+
* Bridges better-auth's database adapter interface with ObjectQL's IDataEngine.
|
|
10
|
+
* This allows better-auth to use ObjectQL for data persistence instead of
|
|
11
|
+
* third-party ORMs like drizzle-orm.
|
|
12
|
+
*
|
|
13
|
+
* Uses better-auth's native naming conventions (camelCase) for seamless migration.
|
|
14
|
+
*
|
|
15
|
+
* @param dataEngine - ObjectQL data engine instance
|
|
16
|
+
* @returns better-auth CustomAdapter
|
|
17
|
+
*/
|
|
18
|
+
export function createObjectQLAdapter(dataEngine: IDataEngine) {
|
|
19
|
+
/**
|
|
20
|
+
* Convert better-auth where clause to ObjectQL query format
|
|
21
|
+
*/
|
|
22
|
+
function convertWhere(where: CleanedWhere[]): Record<string, any> {
|
|
23
|
+
const filter: Record<string, any> = {};
|
|
24
|
+
|
|
25
|
+
for (const condition of where) {
|
|
26
|
+
// Use field names as-is (no conversion needed)
|
|
27
|
+
const fieldName = condition.field;
|
|
28
|
+
|
|
29
|
+
if (condition.operator === 'eq') {
|
|
30
|
+
filter[fieldName] = condition.value;
|
|
31
|
+
} else if (condition.operator === 'ne') {
|
|
32
|
+
filter[fieldName] = { $ne: condition.value };
|
|
33
|
+
} else if (condition.operator === 'in') {
|
|
34
|
+
filter[fieldName] = { $in: condition.value };
|
|
35
|
+
} else if (condition.operator === 'gt') {
|
|
36
|
+
filter[fieldName] = { $gt: condition.value };
|
|
37
|
+
} else if (condition.operator === 'gte') {
|
|
38
|
+
filter[fieldName] = { $gte: condition.value };
|
|
39
|
+
} else if (condition.operator === 'lt') {
|
|
40
|
+
filter[fieldName] = { $lt: condition.value };
|
|
41
|
+
} else if (condition.operator === 'lte') {
|
|
42
|
+
filter[fieldName] = { $lte: condition.value };
|
|
43
|
+
} else if (condition.operator === 'contains') {
|
|
44
|
+
filter[fieldName] = { $regex: condition.value };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return filter;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
create: async <T extends Record<string, any>>({ model, data, select: _select }: { model: string; data: T; select?: string[] }): Promise<T> => {
|
|
53
|
+
// Use model name as-is (no conversion needed)
|
|
54
|
+
const objectName = model;
|
|
55
|
+
|
|
56
|
+
// Note: select parameter is currently not supported by ObjectQL's insert operation
|
|
57
|
+
// The full record is always returned after insertion
|
|
58
|
+
const result = await dataEngine.insert(objectName, data);
|
|
59
|
+
return result as T;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
findOne: async <T>({ model, where, select, join: _join }: { model: string; where: CleanedWhere[]; select?: string[]; join?: any }): Promise<T | null> => {
|
|
63
|
+
const objectName = model;
|
|
64
|
+
const filter = convertWhere(where);
|
|
65
|
+
|
|
66
|
+
// Note: join parameter is not currently supported by ObjectQL's findOne operation
|
|
67
|
+
// Joins/populate functionality is planned for future ObjectQL releases
|
|
68
|
+
// For now, related data must be fetched separately
|
|
69
|
+
|
|
70
|
+
const result = await dataEngine.findOne(objectName, {
|
|
71
|
+
filter,
|
|
72
|
+
select,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return result ? result as T : null;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
findMany: async <T>({ model, where, limit, offset, sortBy, join: _join }: { model: string; where?: CleanedWhere[]; limit: number; offset?: number; sortBy?: { field: string; direction: 'asc' | 'desc' }; join?: any }): Promise<T[]> => {
|
|
79
|
+
const objectName = model;
|
|
80
|
+
const filter = where ? convertWhere(where) : {};
|
|
81
|
+
|
|
82
|
+
// Note: join parameter is not currently supported by ObjectQL's find operation
|
|
83
|
+
// Joins/populate functionality is planned for future ObjectQL releases
|
|
84
|
+
|
|
85
|
+
const sort = sortBy ? [{
|
|
86
|
+
field: sortBy.field,
|
|
87
|
+
order: sortBy.direction as 'asc' | 'desc',
|
|
88
|
+
}] : undefined;
|
|
89
|
+
|
|
90
|
+
const results = await dataEngine.find(objectName, {
|
|
91
|
+
filter,
|
|
92
|
+
limit: limit || 100,
|
|
93
|
+
skip: offset,
|
|
94
|
+
sort,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return results as T[];
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
count: async ({ model, where }: { model: string; where?: CleanedWhere[] }): Promise<number> => {
|
|
101
|
+
const objectName = model;
|
|
102
|
+
const filter = where ? convertWhere(where) : {};
|
|
103
|
+
|
|
104
|
+
return await dataEngine.count(objectName, { filter });
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
update: async <T>({ model, where, update }: { model: string; where: CleanedWhere[]; update: Record<string, any> }): Promise<T | null> => {
|
|
108
|
+
const objectName = model;
|
|
109
|
+
const filter = convertWhere(where);
|
|
110
|
+
|
|
111
|
+
// Find the record first to get its ID
|
|
112
|
+
const record = await dataEngine.findOne(objectName, { filter });
|
|
113
|
+
if (!record) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = await dataEngine.update(objectName, {
|
|
118
|
+
...update,
|
|
119
|
+
id: record.id,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return result ? result as T : null;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
updateMany: async ({ model, where, update }: { model: string; where: CleanedWhere[]; update: Record<string, any> }): Promise<number> => {
|
|
126
|
+
const objectName = model;
|
|
127
|
+
const filter = convertWhere(where);
|
|
128
|
+
|
|
129
|
+
// Note: Sequential updates are used here because ObjectQL's IDataEngine interface
|
|
130
|
+
// requires an ID for updates. A future optimization could use a bulk update
|
|
131
|
+
// operation if ObjectQL adds support for filter-based updates without IDs.
|
|
132
|
+
|
|
133
|
+
// Find all matching records
|
|
134
|
+
const records = await dataEngine.find(objectName, { filter });
|
|
135
|
+
|
|
136
|
+
// Update each record
|
|
137
|
+
for (const record of records) {
|
|
138
|
+
await dataEngine.update(objectName, {
|
|
139
|
+
...update,
|
|
140
|
+
id: record.id,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return records.length;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
delete: async ({ model, where }: { model: string; where: CleanedWhere[] }): Promise<void> => {
|
|
148
|
+
const objectName = model;
|
|
149
|
+
const filter = convertWhere(where);
|
|
150
|
+
|
|
151
|
+
// Note: We need to find the record first to get its ID because ObjectQL's
|
|
152
|
+
// delete operation requires an ID. Direct filter-based delete would be more
|
|
153
|
+
// efficient if supported by ObjectQL in the future.
|
|
154
|
+
const record = await dataEngine.findOne(objectName, { filter });
|
|
155
|
+
if (!record) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await dataEngine.delete(objectName, { filter: { id: record.id } });
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
deleteMany: async ({ model, where }: { model: string; where: CleanedWhere[] }): Promise<number> => {
|
|
163
|
+
const objectName = model;
|
|
164
|
+
const filter = convertWhere(where);
|
|
165
|
+
|
|
166
|
+
// Note: Sequential deletes are used here because ObjectQL's delete operation
|
|
167
|
+
// requires an ID in the filter. A future optimization could use a single
|
|
168
|
+
// delete call with the original filter if ObjectQL supports it.
|
|
169
|
+
|
|
170
|
+
// Find all matching records
|
|
171
|
+
const records = await dataEngine.find(objectName, { filter });
|
|
172
|
+
|
|
173
|
+
// Delete each record
|
|
174
|
+
for (const record of records) {
|
|
175
|
+
await dataEngine.delete(objectName, { filter: { id: record.id } });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return records.length;
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { ObjectSchema, Field } from '@objectstack/spec/data';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Auth Account Object
|
|
7
|
+
*
|
|
8
|
+
* Uses better-auth's native schema for seamless migration:
|
|
9
|
+
* - id: string
|
|
10
|
+
* - createdAt: Date
|
|
11
|
+
* - updatedAt: Date
|
|
12
|
+
* - providerId: string (e.g., 'google', 'github')
|
|
13
|
+
* - accountId: string (provider's user ID)
|
|
14
|
+
* - userId: string (link to user table)
|
|
15
|
+
* - accessToken: string | null
|
|
16
|
+
* - refreshToken: string | null
|
|
17
|
+
* - idToken: string | null
|
|
18
|
+
* - accessTokenExpiresAt: Date | null
|
|
19
|
+
* - refreshTokenExpiresAt: Date | null
|
|
20
|
+
* - scope: string | null
|
|
21
|
+
* - password: string | null (for email/password provider)
|
|
22
|
+
*/
|
|
23
|
+
export const AuthAccount = ObjectSchema.create({
|
|
24
|
+
name: 'account',
|
|
25
|
+
label: 'Account',
|
|
26
|
+
pluralLabel: 'Accounts',
|
|
27
|
+
icon: 'link',
|
|
28
|
+
description: 'OAuth and authentication provider accounts',
|
|
29
|
+
titleFormat: '{providerId} - {accountId}',
|
|
30
|
+
compactLayout: ['providerId', 'userId', 'accountId'],
|
|
31
|
+
|
|
32
|
+
fields: {
|
|
33
|
+
id: Field.text({
|
|
34
|
+
label: 'Account ID',
|
|
35
|
+
required: true,
|
|
36
|
+
readonly: true,
|
|
37
|
+
}),
|
|
38
|
+
|
|
39
|
+
createdAt: Field.datetime({
|
|
40
|
+
label: 'Created At',
|
|
41
|
+
defaultValue: 'NOW()',
|
|
42
|
+
readonly: true,
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
updatedAt: Field.datetime({
|
|
46
|
+
label: 'Updated At',
|
|
47
|
+
defaultValue: 'NOW()',
|
|
48
|
+
readonly: true,
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
providerId: Field.text({
|
|
52
|
+
label: 'Provider ID',
|
|
53
|
+
required: true,
|
|
54
|
+
description: 'OAuth provider identifier (google, github, etc.)',
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
accountId: Field.text({
|
|
58
|
+
label: 'Provider Account ID',
|
|
59
|
+
required: true,
|
|
60
|
+
description: "User's ID in the provider's system",
|
|
61
|
+
}),
|
|
62
|
+
|
|
63
|
+
userId: Field.text({
|
|
64
|
+
label: 'User ID',
|
|
65
|
+
required: true,
|
|
66
|
+
description: 'Link to user table',
|
|
67
|
+
}),
|
|
68
|
+
|
|
69
|
+
accessToken: Field.textarea({
|
|
70
|
+
label: 'Access Token',
|
|
71
|
+
required: false,
|
|
72
|
+
}),
|
|
73
|
+
|
|
74
|
+
refreshToken: Field.textarea({
|
|
75
|
+
label: 'Refresh Token',
|
|
76
|
+
required: false,
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
idToken: Field.textarea({
|
|
80
|
+
label: 'ID Token',
|
|
81
|
+
required: false,
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
accessTokenExpiresAt: Field.datetime({
|
|
85
|
+
label: 'Access Token Expires At',
|
|
86
|
+
required: false,
|
|
87
|
+
}),
|
|
88
|
+
|
|
89
|
+
refreshTokenExpiresAt: Field.datetime({
|
|
90
|
+
label: 'Refresh Token Expires At',
|
|
91
|
+
required: false,
|
|
92
|
+
}),
|
|
93
|
+
|
|
94
|
+
scope: Field.text({
|
|
95
|
+
label: 'OAuth Scope',
|
|
96
|
+
required: false,
|
|
97
|
+
}),
|
|
98
|
+
|
|
99
|
+
password: Field.text({
|
|
100
|
+
label: 'Password Hash',
|
|
101
|
+
required: false,
|
|
102
|
+
description: 'Hashed password for email/password provider',
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Database indexes for performance
|
|
107
|
+
indexes: [
|
|
108
|
+
{ fields: ['userId'], unique: false },
|
|
109
|
+
{ fields: ['providerId', 'accountId'], unique: true },
|
|
110
|
+
],
|
|
111
|
+
|
|
112
|
+
// Enable features
|
|
113
|
+
enable: {
|
|
114
|
+
trackHistory: false,
|
|
115
|
+
searchable: false,
|
|
116
|
+
apiEnabled: true,
|
|
117
|
+
apiMethods: ['get', 'list', 'create', 'update', 'delete'],
|
|
118
|
+
trash: true,
|
|
119
|
+
mru: false,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { ObjectSchema, Field } from '@objectstack/spec/data';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Auth Session Object
|
|
7
|
+
*
|
|
8
|
+
* Uses better-auth's native schema for seamless migration:
|
|
9
|
+
* - id: string
|
|
10
|
+
* - createdAt: Date
|
|
11
|
+
* - updatedAt: Date
|
|
12
|
+
* - userId: string
|
|
13
|
+
* - expiresAt: Date
|
|
14
|
+
* - token: string
|
|
15
|
+
* - ipAddress: string | null
|
|
16
|
+
* - userAgent: string | null
|
|
17
|
+
*/
|
|
18
|
+
export const AuthSession = ObjectSchema.create({
|
|
19
|
+
name: 'session',
|
|
20
|
+
label: 'Session',
|
|
21
|
+
pluralLabel: 'Sessions',
|
|
22
|
+
icon: 'key',
|
|
23
|
+
description: 'Active user sessions',
|
|
24
|
+
titleFormat: 'Session {token}',
|
|
25
|
+
compactLayout: ['userId', 'expiresAt', 'ipAddress'],
|
|
26
|
+
|
|
27
|
+
fields: {
|
|
28
|
+
id: Field.text({
|
|
29
|
+
label: 'Session ID',
|
|
30
|
+
required: true,
|
|
31
|
+
readonly: true,
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
createdAt: Field.datetime({
|
|
35
|
+
label: 'Created At',
|
|
36
|
+
defaultValue: 'NOW()',
|
|
37
|
+
readonly: true,
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
updatedAt: Field.datetime({
|
|
41
|
+
label: 'Updated At',
|
|
42
|
+
defaultValue: 'NOW()',
|
|
43
|
+
readonly: true,
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
userId: Field.text({
|
|
47
|
+
label: 'User ID',
|
|
48
|
+
required: true,
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
expiresAt: Field.datetime({
|
|
52
|
+
label: 'Expires At',
|
|
53
|
+
required: true,
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
token: Field.text({
|
|
57
|
+
label: 'Session Token',
|
|
58
|
+
required: true,
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
ipAddress: Field.text({
|
|
62
|
+
label: 'IP Address',
|
|
63
|
+
required: false,
|
|
64
|
+
maxLength: 45, // Support IPv6
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
userAgent: Field.textarea({
|
|
68
|
+
label: 'User Agent',
|
|
69
|
+
required: false,
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Database indexes for performance
|
|
74
|
+
indexes: [
|
|
75
|
+
{ fields: ['token'], unique: true },
|
|
76
|
+
{ fields: ['userId'], unique: false },
|
|
77
|
+
{ fields: ['expiresAt'], unique: false },
|
|
78
|
+
],
|
|
79
|
+
|
|
80
|
+
// Enable features
|
|
81
|
+
enable: {
|
|
82
|
+
trackHistory: false, // Sessions don't need history tracking
|
|
83
|
+
searchable: false,
|
|
84
|
+
apiEnabled: true,
|
|
85
|
+
apiMethods: ['get', 'list', 'create', 'delete'], // No update for sessions
|
|
86
|
+
trash: false, // Sessions should be hard deleted
|
|
87
|
+
mru: false,
|
|
88
|
+
},
|
|
89
|
+
});
|