mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-26 01:25:15 +02:00
* WIP on mobile connections todo: register devices todo: data sync or connection * improve connection flow and registration add streaming from service TODO: user scoping * dev build mobile support * fix path * handle relative URLs * handle localhost access in product * add device de-register * sync styles * move UI to be out of the normal path since beta only * Add user scoping to mobile connection requests Remigrate DB for user associations Implement temp token registration to prevent unauthorized device registration requests cleanup middlewares
231 lines
7.9 KiB
JavaScript
231 lines
7.9 KiB
JavaScript
const prisma = require("../utils/prisma");
|
|
const { v4: uuidv4 } = require("uuid");
|
|
const ip = require("ip");
|
|
|
|
/**
|
|
* @typedef {Object} TemporaryMobileDeviceRequest
|
|
* @property {number|null} userId - User id to associate creation of key with.
|
|
* @property {number} createdAt - Timestamp of when the token was created.
|
|
* @property {number} expiresAt - Timestamp of when the token expires.
|
|
*/
|
|
|
|
/**
|
|
* Temporary map to store mobile device requests
|
|
* that are not yet approved. Generates a simple JWT
|
|
* that expires and is tied to the user (if provided)
|
|
* This token must be provided during /register event.
|
|
* @type {Map<string, TemporaryMobileDeviceRequest>}
|
|
*/
|
|
const TemporaryMobileDeviceRequests = new Map();
|
|
|
|
const MobileDevice = {
|
|
platform: "server",
|
|
validDeviceOs: ["android"],
|
|
tablename: "desktop_mobile_devices",
|
|
writable: ["approved"],
|
|
validators: {
|
|
approved: (value) => {
|
|
if (typeof value !== "boolean") return "Must be a boolean";
|
|
return null;
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Looks up and consumes a temporary token that was registered
|
|
* Will return null if the token is not found or expired.
|
|
* @param {string} token - The temporary token to lookup
|
|
* @returns {TemporaryMobileDeviceRequest|null} Temp token details
|
|
*/
|
|
tempToken: (token = null) => {
|
|
try {
|
|
if (!token || !TemporaryMobileDeviceRequests.has(token)) return null;
|
|
const tokenData = TemporaryMobileDeviceRequests.get(token);
|
|
if (tokenData.expiresAt < Date.now()) return null;
|
|
return tokenData;
|
|
} catch (error) {
|
|
return null;
|
|
} finally {
|
|
TemporaryMobileDeviceRequests.delete(token);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Registers a temporary token for a mobile device request
|
|
* This is just using a random token to identify the request
|
|
* @security Note: If we use a JWT the QR code that encodes it becomes extremely complex
|
|
* and noisy as QR codes have byte limits that could be exceeded with JWTs. Since this is
|
|
* a temporary token that is only used to register a device and is short lived we can use UUIDs.
|
|
* @param {import("@prisma/client").users|null} user - User to get connection URL for in Multi-User Mode
|
|
* @returns {string} The temporary token
|
|
*/
|
|
registerTempToken: function (user = null) {
|
|
let tokenData = {};
|
|
if (user) tokenData.userId = user.id;
|
|
else tokenData.userId = null;
|
|
|
|
// Set short lived expiry to this mapping
|
|
const createdAt = Date.now();
|
|
tokenData.createdAt = createdAt;
|
|
tokenData.expiresAt = createdAt + 3 * 60_000;
|
|
|
|
const tempToken = uuidv4().split("-").slice(0, 3).join("");
|
|
TemporaryMobileDeviceRequests.set(tempToken, tokenData);
|
|
|
|
// Run this on register since there is no BG task to do this.
|
|
this.cleanupExpiredTokens();
|
|
return tempToken;
|
|
},
|
|
|
|
/**
|
|
* Cleans up expired temporary registration tokens
|
|
* Should run quick since this mapping is wiped often
|
|
* and does not live past restarts.
|
|
*/
|
|
cleanupExpiredTokens: function () {
|
|
const now = Date.now();
|
|
for (const [token, data] of TemporaryMobileDeviceRequests.entries()) {
|
|
if (data.expiresAt < now) TemporaryMobileDeviceRequests.delete(token);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the connection URL for the mobile app to use to connect to the backend.
|
|
* Since you have to have a valid session to call /mobile/connect-info we can pre-register
|
|
* a temporary token for the user that is passed back to /mobile/register and can lookup
|
|
* who a device belongs to so we can scope it's access token.
|
|
* @param {import("@prisma/client").users|null} user - User to get connection URL for in Multi-User Mode
|
|
* @returns {string}
|
|
*/
|
|
connectionURL: function (user = null) {
|
|
let baseUrl = "/api/mobile";
|
|
if (process.env.NODE_ENV === "production") baseUrl = "/api/mobile";
|
|
else
|
|
baseUrl = `http://${ip.address()}:${process.env.SERVER_PORT || 3001}/api/mobile`;
|
|
|
|
const tempToken = this.registerTempToken(user);
|
|
baseUrl = `${baseUrl}?t=${tempToken}`;
|
|
return baseUrl;
|
|
},
|
|
|
|
/**
|
|
* Creates a new device for the mobile app
|
|
* @param {object} params - The params to create the device with.
|
|
* @param {string} params.deviceOs - Device os to associate creation of key with.
|
|
* @param {string} params.deviceName - Device name to associate creation of key with.
|
|
* @param {number|null} params.userId - User id to associate creation of key with.
|
|
* @returns {Promise<{device: import("@prisma/client").desktop_mobile_devices|null, error:string|null}>}
|
|
*/
|
|
create: async function ({ deviceOs, deviceName, userId = null }) {
|
|
try {
|
|
if (!deviceOs || !deviceName)
|
|
return { device: null, error: "Device OS and name are required" };
|
|
if (!this.validDeviceOs.includes(deviceOs))
|
|
return { device: null, error: `Invalid device OS - ${deviceOs}` };
|
|
|
|
const device = await prisma.desktop_mobile_devices.create({
|
|
data: {
|
|
deviceName: String(deviceName),
|
|
deviceOs: String(deviceOs).toLowerCase(),
|
|
token: uuidv4(),
|
|
userId: userId ? Number(userId) : null,
|
|
},
|
|
});
|
|
return { device, error: null };
|
|
} catch (error) {
|
|
console.error("Failed to create mobile device", error);
|
|
return { device: null, error: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Validated existing API key
|
|
* @param {string} id - Device id (db id)
|
|
* @param {object} updates - Updates to apply to device
|
|
* @returns {Promise<{device: import("@prisma/client").desktop_mobile_devices|null, error:string|null}>}
|
|
*/
|
|
update: async function (id, updates = {}) {
|
|
const device = await this.get({ id: parseInt(id) });
|
|
if (!device) return { device: null, error: "Device not found" };
|
|
|
|
const validUpdates = {};
|
|
for (const [key, value] of Object.entries(updates)) {
|
|
if (!this.writable.includes(key)) continue;
|
|
const validation = this.validators[key](value);
|
|
if (validation !== null) return { device: null, error: validation };
|
|
validUpdates[key] = value;
|
|
}
|
|
// If no updates, return the device.
|
|
if (Object.keys(validUpdates).length === 0) return { device, error: null };
|
|
|
|
const updatedDevice = await prisma.desktop_mobile_devices.update({
|
|
where: { id: device.id },
|
|
data: validUpdates,
|
|
});
|
|
return { device: updatedDevice, error: null };
|
|
},
|
|
|
|
/**
|
|
* Fetches mobile device by params.
|
|
* @param {object} clause - Prisma props for search
|
|
* @returns {Promise<import("@prisma/client").desktop_mobile_devices[]>}
|
|
*/
|
|
get: async function (clause = {}, include = null) {
|
|
try {
|
|
const device = await prisma.desktop_mobile_devices.findFirst({
|
|
where: clause,
|
|
...(include !== null ? { include } : {}),
|
|
});
|
|
return device;
|
|
} catch (error) {
|
|
console.error("FAILED TO GET MOBILE DEVICE.", error);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Deletes mobile device by db id.
|
|
* @param {number} id - database id of mobile device
|
|
* @returns {Promise<{success: boolean, error:string|null}>}
|
|
*/
|
|
delete: async function (id) {
|
|
try {
|
|
await prisma.desktop_mobile_devices.delete({
|
|
where: { id: parseInt(id) },
|
|
});
|
|
return { success: true, error: null };
|
|
} catch (error) {
|
|
console.error("Failed to delete mobile device", error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Gets mobile devices by params
|
|
* @param {object} clause
|
|
* @param {number|null} limit
|
|
* @param {object|null} orderBy
|
|
* @returns {Promise<import("@prisma/client").desktop_mobile_devices[]>}
|
|
*/
|
|
where: async function (
|
|
clause = {},
|
|
limit = null,
|
|
orderBy = null,
|
|
include = null
|
|
) {
|
|
try {
|
|
const devices = await prisma.desktop_mobile_devices.findMany({
|
|
where: clause,
|
|
...(limit !== null ? { take: limit } : {}),
|
|
...(orderBy !== null ? { orderBy } : {}),
|
|
...(include !== null ? { include } : {}),
|
|
});
|
|
return devices;
|
|
} catch (error) {
|
|
console.error("FAILED TO GET MOBILE DEVICES.", error.message);
|
|
return [];
|
|
}
|
|
},
|
|
};
|
|
|
|
module.exports = { MobileDevice };
|