File size: 8,123 Bytes
f8b5d42 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
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 };
|