fix: complete ORM port — token/oauth-client API mismatches, use published orm 0.2.8
- Use published @simpleworkjs/orm ^0.2.8 (fixes redis adapter write path)
and model-redis ^1.6.0 instead of a local file: link that broke docker
npm ci with a misleading "no lockfile" error.
- OtpToken.issue/verify: replace nonexistent find()/listDetail() with
list({where}).
- routes/auth.js: ImpersonationToken.listDetail() -> list({where}).
- routes/token.js: drop listDetail() call; 404 on missing token instead
of returning {results: null} with 200 (orm get() returns null, does
not throw like model-redis Table.get did).
- OAuthClient: Resource has no is_valid column, so every client read as
disabled and all /oauth/authorize requests 400'd — validity now lives
in metadata (absent = valid). Also generate a unique slug on create
(Resource.slug is required+unique) and use Resource.get() for lookup.
- User.login: 401 cleanly when neither uid nor username is supplied.
- models/index.js: log ORM init and surface init failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ async function initORM() {
|
|||||||
};
|
};
|
||||||
ormConf.redis = conf.redis;
|
ormConf.redis = conf.redis;
|
||||||
|
|
||||||
|
console.log('[initORM] Starting ORM initialization...');
|
||||||
|
try {
|
||||||
await init({
|
await init({
|
||||||
conf: { orm: ormConf },
|
conf: { orm: ormConf },
|
||||||
models: [
|
models: [
|
||||||
@@ -30,6 +32,12 @@ async function initORM() {
|
|||||||
Token, AuthToken, InviteToken, ImpersonationToken, PasswordResetToken, OtpToken, ServiceToken
|
Token, AuthToken, InviteToken, ImpersonationToken, PasswordResetToken, OtpToken, ServiceToken
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
console.log('[initORM] ORM initialized successfully');
|
||||||
|
console.log('[initORM] Resource.orm =', !!Resource.orm, 'Token.orm =', !!Token.orm);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[initORM] ORM initialization failed:', err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.initORM = initORM;
|
module.exports.initORM = initORM;
|
||||||
|
|||||||
@@ -17,10 +17,17 @@ class OAuthClient {
|
|||||||
const client_id = crypto.randomUUID();
|
const client_id = crypto.randomUUID();
|
||||||
const client_secret_hash = await bcrypt.hash(raw_secret, 10);
|
const client_secret_hash = await bcrypt.hash(raw_secret, 10);
|
||||||
|
|
||||||
|
// Generate a unique slug from the client name
|
||||||
|
let slug = data.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'oauth-client';
|
||||||
|
// Ensure uniqueness by appending a suffix if needed
|
||||||
|
const existing = await Resource.list({ where: { slug } });
|
||||||
|
if (existing.length) slug = `${slug}-${client_id.slice(0, 8)}`;
|
||||||
|
|
||||||
const r = await Resource.create({
|
const r = await Resource.create({
|
||||||
id: client_id,
|
id: client_id,
|
||||||
kind: 'oauth',
|
kind: 'oauth',
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
slug: slug,
|
||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
owner: data.created_by,
|
owner: data.created_by,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -37,10 +44,13 @@ class OAuthClient {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
static async get(client_id) {
|
static async get(client_id) {
|
||||||
const resources = await Resource.list({ where: { id: client_id, kind: 'oauth' } });
|
let r;
|
||||||
if (!resources.length) throw new Error('OAuthClient not found');
|
try {
|
||||||
|
r = await Resource.get(client_id);
|
||||||
const r = resources[0];
|
} catch (_) {
|
||||||
|
throw new Error('OAuthClient not found');
|
||||||
|
}
|
||||||
|
if (r.kind !== 'oauth') throw new Error('OAuthClient not found');
|
||||||
// Map metadata to top-level properties to satisfy routes/oauth.js without rewriting it
|
// Map metadata to top-level properties to satisfy routes/oauth.js without rewriting it
|
||||||
r.client_id = r.id;
|
r.client_id = r.id;
|
||||||
r.client_secret_hash = r.metadata.client_secret_hash;
|
r.client_secret_hash = r.metadata.client_secret_hash;
|
||||||
@@ -48,6 +58,8 @@ class OAuthClient {
|
|||||||
r.scopes = r.metadata.scopes || ['openid', 'profile', 'email', 'groups'];
|
r.scopes = r.metadata.scopes || ['openid', 'profile', 'email', 'groups'];
|
||||||
r.allowed_groups = r.metadata.allowed_groups || [];
|
r.allowed_groups = r.metadata.allowed_groups || [];
|
||||||
r.token_lifetime = r.metadata.token_lifetime || { ...defaultLifetime };
|
r.token_lifetime = r.metadata.token_lifetime || { ...defaultLifetime };
|
||||||
|
// Resource has no is_valid column; validity lives in metadata (absent = valid)
|
||||||
|
r.is_valid = r.metadata.is_valid !== false;
|
||||||
r.verifySecret = async (secret) => bcrypt.compare(secret, r.client_secret_hash);
|
r.verifySecret = async (secret) => bcrypt.compare(secret, r.client_secret_hash);
|
||||||
|
|
||||||
r.rotateSecret = async () => {
|
r.rotateSecret = async () => {
|
||||||
@@ -64,11 +76,11 @@ class OAuthClient {
|
|||||||
if (data.scopes !== undefined) r.metadata.scopes = data.scopes;
|
if (data.scopes !== undefined) r.metadata.scopes = data.scopes;
|
||||||
if (data.allowed_groups !== undefined) r.metadata.allowed_groups = data.allowed_groups;
|
if (data.allowed_groups !== undefined) r.metadata.allowed_groups = data.allowed_groups;
|
||||||
if (data.token_lifetime !== undefined) r.metadata.token_lifetime = data.token_lifetime;
|
if (data.token_lifetime !== undefined) r.metadata.token_lifetime = data.token_lifetime;
|
||||||
|
if (data.is_valid !== undefined) r.metadata.is_valid = data.is_valid;
|
||||||
|
|
||||||
const updateData = { metadata: r.metadata };
|
const updateData = { metadata: r.metadata };
|
||||||
if (data.name !== undefined) updateData.name = data.name;
|
if (data.name !== undefined) updateData.name = data.name;
|
||||||
if (data.description !== undefined) updateData.description = data.description;
|
if (data.description !== undefined) updateData.description = data.description;
|
||||||
if (data.is_valid !== undefined) updateData.is_valid = data.is_valid;
|
|
||||||
|
|
||||||
return originalUpdate(updateData);
|
return originalUpdate(updateData);
|
||||||
};
|
};
|
||||||
@@ -81,6 +93,10 @@ class OAuthClient {
|
|||||||
return Promise.all(resources.map(r => this.get(r.id)));
|
return Promise.all(resources.map(r => this.get(r.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async listDetail() {
|
||||||
|
return this.list();
|
||||||
|
}
|
||||||
|
|
||||||
static async verifySecret(client_id, secret) {
|
static async verifySecret(client_id, secret) {
|
||||||
const client = await this.get(client_id);
|
const client = await this.get(client_id);
|
||||||
return client.verifySecret(secret);
|
return client.verifySecret(secret);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class OtpToken extends Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async issue(uid, method) {
|
static async issue(uid, method) {
|
||||||
const existing = await this.find({uid});
|
const existing = await this.list({where: {uid}});
|
||||||
for (const t of existing) {
|
for (const t of existing) {
|
||||||
if (t.is_valid) await t.update({is_valid: false});
|
if (t.is_valid) await t.update({is_valid: false});
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ class OtpToken extends Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async verify(uid, code) {
|
static async verify(uid, code) {
|
||||||
const tokens = await this.listDetail({uid});
|
const tokens = await this.list({where: {uid}});
|
||||||
const match = tokens.find(t => t.is_valid && !t.isExpired && t.code === code);
|
const match = tokens.find(t => t.is_valid && !t.isExpired && t.code === code);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
await match.update({is_valid: false});
|
await match.update({is_valid: false});
|
||||||
|
|||||||
@@ -891,6 +891,12 @@ User.invite = async function(data = {}){
|
|||||||
|
|
||||||
User.login = async function(data){
|
User.login = async function(data){
|
||||||
try{
|
try{
|
||||||
|
if (!data.uid && !data.username) {
|
||||||
|
let error = new Error('Invalid Credentials, login failed.');
|
||||||
|
error.name = 'LDAPLoginFailed';
|
||||||
|
error.status = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
let user = await this.get(data.uid || data.username);
|
let user = await this.get(data.uid || data.username);
|
||||||
|
|
||||||
const loginClient = makeClient();
|
const loginClient = makeClient();
|
||||||
|
|||||||
Generated
+1546
-339
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -24,7 +24,7 @@
|
|||||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@simpleworkjs/conf": "^1.2.0",
|
"@simpleworkjs/conf": "^1.2.0",
|
||||||
"@simpleworkjs/orm": "file:../../../simpleworkjs/orm",
|
"@simpleworkjs/orm": "^0.2.8",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"ldapts": "^8.1.2",
|
"ldapts": "^8.1.2",
|
||||||
"lru-cache": "^11.5.1",
|
"lru-cache": "^11.5.1",
|
||||||
"marked": "^9.1.6",
|
"marked": "^9.1.6",
|
||||||
"model-redis": "^0.4.0",
|
"model-redis": "^1.6.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"nodemailer": "^9.0.0",
|
"nodemailer": "^9.0.0",
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ router.post('/impersonate/:uid', middleware.auth, async function(req, res, next)
|
|||||||
const target = await User.get(req.params.uid);
|
const target = await User.get(req.params.uid);
|
||||||
|
|
||||||
// Clean up any existing impersonation for this target
|
// Clean up any existing impersonation for this target
|
||||||
const existing = await ImpersonationToken.listDetail({ target_uid: target.uid });
|
const existing = await ImpersonationToken.list({ where: { target_uid: target.uid } });
|
||||||
for (const old of existing) {
|
for (const old of existing) {
|
||||||
if (old.is_valid && !old.isExpired) {
|
if (old.is_valid && !old.isExpired) {
|
||||||
try { await target.removeTempPassword(old.temp_hash); } catch(_) {}
|
try { await target.removeTempPassword(old.temp_hash); } catch(_) {}
|
||||||
@@ -237,7 +237,7 @@ router.delete('/impersonate/:uid', middleware.auth, async function(req, res, nex
|
|||||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||||
|
|
||||||
const target = await User.get(req.params.uid);
|
const target = await User.get(req.params.uid);
|
||||||
const existing = await ImpersonationToken.listDetail({ target_uid: target.uid });
|
const existing = await ImpersonationToken.list({ where: { target_uid: target.uid } });
|
||||||
|
|
||||||
let revoked = 0;
|
let revoked = 0;
|
||||||
for (const token of existing) {
|
for (const token of existing) {
|
||||||
|
|||||||
+10
-4
@@ -23,8 +23,10 @@ router.get('/', async function(req, res, next){
|
|||||||
|
|
||||||
router.get('/:name', async function(req, res, next){
|
router.get('/:name', async function(req, res, next){
|
||||||
try{
|
try{
|
||||||
|
// ORM models: list() on the redis adapter always returns full rows;
|
||||||
|
// detail is handled by serialization (isPrivate fields are excluded).
|
||||||
return res.json({
|
return res.json({
|
||||||
results: await tokens[req.params.name][req.query.detail ? "listDetail" : "list"]()
|
results: await tokens[req.params.name].list()
|
||||||
});
|
});
|
||||||
}catch(error){
|
}catch(error){
|
||||||
next(error);
|
next(error);
|
||||||
@@ -34,9 +36,13 @@ router.get('/:name', async function(req, res, next){
|
|||||||
|
|
||||||
router.get('/:name/:token', async function(req, res, next){
|
router.get('/:name/:token', async function(req, res, next){
|
||||||
try{
|
try{
|
||||||
return res.json({
|
const result = await tokens[req.params.name].get(req.params.token);
|
||||||
results: await tokens[req.params.name].get(req.params.token)
|
if (!result) {
|
||||||
});
|
const error = new Error('Token not found');
|
||||||
|
error.status = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res.json({ results: result });
|
||||||
}catch(error){
|
}catch(error){
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user