1.0.0-beta9.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import db from "@server/db";
  2. import {
  3. emailVerificationCodes,
  4. passwordResetTokens,
  5. resourceOtp,
  6. resources,
  7. resourceWhitelist,
  8. targets,
  9. userInvites,
  10. users
  11. } from "@server/db/schema";
  12. import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
  13. import { eq, sql } from "drizzle-orm";
  14. import fs from "fs";
  15. import yaml from "js-yaml";
  16. import path from "path";
  17. import { z } from "zod";
  18. import { fromZodError } from "zod-validation-error";
  19. export default async function migration() {
  20. console.log("Running setup script 1.0.0-beta.9...");
  21. // make dir config/db/backups
  22. const appPath = APP_PATH;
  23. const dbDir = path.join(appPath, "db");
  24. const backupsDir = path.join(dbDir, "backups");
  25. // check if the backups directory exists and create it if it doesn't
  26. if (!fs.existsSync(backupsDir)) {
  27. fs.mkdirSync(backupsDir, { recursive: true });
  28. }
  29. // copy the db.sqlite file to backups
  30. // add the date to the filename
  31. const date = new Date();
  32. const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
  33. const dbPath = path.join(dbDir, "db.sqlite");
  34. const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
  35. fs.copyFileSync(dbPath, backupPath);
  36. await db.transaction(async (trx) => {
  37. try {
  38. // Determine which config file exists
  39. const filePaths = [configFilePath1, configFilePath2];
  40. let filePath = "";
  41. for (const path of filePaths) {
  42. if (fs.existsSync(path)) {
  43. filePath = path;
  44. break;
  45. }
  46. }
  47. if (!filePath) {
  48. throw new Error(
  49. `No config file found (expected config.yml or config.yaml).`
  50. );
  51. }
  52. // Read and parse the YAML file
  53. let rawConfig: any;
  54. const fileContents = fs.readFileSync(filePath, "utf8");
  55. rawConfig = yaml.load(fileContents);
  56. rawConfig.server.resource_session_request_param =
  57. "p_session_request";
  58. rawConfig.server.session_cookie_name = "p_session_token"; // rename to prevent conflicts
  59. delete rawConfig.server.resource_session_cookie_name;
  60. if (!rawConfig.flags) {
  61. rawConfig.flags = {};
  62. }
  63. rawConfig.flags.allow_raw_resources = true;
  64. // Write the updated YAML back to the file
  65. const updatedYaml = yaml.dump(rawConfig);
  66. fs.writeFileSync(filePath, updatedYaml, "utf8");
  67. } catch (e) {
  68. console.log(
  69. `Failed to add resource_session_request_param to config. Please add it manually. https://docs.fossorial.io/Pangolin/Configuration/config`
  70. );
  71. trx.rollback();
  72. return;
  73. }
  74. try {
  75. const traefikPath = path.join(
  76. APP_PATH,
  77. "traefik",
  78. "traefik_config.yml"
  79. );
  80. // Define schema for traefik config validation
  81. const schema = z.object({
  82. entryPoints: z
  83. .object({
  84. websecure: z
  85. .object({
  86. address: z.string(),
  87. transport: z
  88. .object({
  89. respondingTimeouts: z.object({
  90. readTimeout: z.string()
  91. })
  92. })
  93. .optional()
  94. })
  95. .optional()
  96. })
  97. .optional(),
  98. experimental: z.object({
  99. plugins: z.object({
  100. badger: z.object({
  101. moduleName: z.string(),
  102. version: z.string()
  103. })
  104. })
  105. })
  106. });
  107. const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
  108. const traefikConfig = yaml.load(traefikFileContents) as any;
  109. let parsedConfig: any = schema.safeParse(traefikConfig);
  110. if (parsedConfig.success) {
  111. // Ensure websecure entrypoint exists
  112. if (traefikConfig.entryPoints?.websecure) {
  113. // Add transport configuration
  114. traefikConfig.entryPoints.websecure.transport = {
  115. respondingTimeouts: {
  116. readTimeout: "30m"
  117. }
  118. };
  119. }
  120. traefikConfig.experimental.plugins.badger.version =
  121. "v1.0.0-beta.3";
  122. const updatedTraefikYaml = yaml.dump(traefikConfig);
  123. fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
  124. console.log("Updated Badger version in Traefik config.");
  125. } else {
  126. console.log(fromZodError(parsedConfig.error));
  127. console.log(
  128. "We were unable to update the version of Badger in your Traefik configuration. Please update it manually to at least v1.0.0-beta.3. https://github.com/fosrl/badger"
  129. );
  130. }
  131. } catch (e) {
  132. console.log(
  133. "We were unable to update the version of Badger in your Traefik configuration. Please update it manually to at least v1.0.0-beta.3. https://github.com/fosrl/badger"
  134. );
  135. trx.rollback();
  136. return;
  137. }
  138. try {
  139. const traefikPath = path.join(
  140. APP_PATH,
  141. "traefik",
  142. "dynamic_config.yml"
  143. );
  144. const schema = z.object({
  145. http: z.object({
  146. middlewares: z.object({
  147. "redirect-to-https": z.object({
  148. redirectScheme: z.object({
  149. scheme: z.string(),
  150. permanent: z.boolean()
  151. })
  152. })
  153. })
  154. })
  155. });
  156. const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
  157. const traefikConfig = yaml.load(traefikFileContents) as any;
  158. let parsedConfig: any = schema.safeParse(traefikConfig);
  159. if (parsedConfig.success) {
  160. // delete permanent from redirect-to-https middleware
  161. delete traefikConfig.http.middlewares["redirect-to-https"].redirectScheme.permanent;
  162. const updatedTraefikYaml = yaml.dump(traefikConfig);
  163. fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
  164. console.log("Deleted permanent from redirect-to-https middleware.");
  165. } else {
  166. console.log(fromZodError(parsedConfig.error));
  167. console.log(
  168. "We were unable to delete the permanent field from the redirect-to-https middleware in your Traefik configuration. Please delete it manually."
  169. );
  170. }
  171. } catch (e) {
  172. console.log(
  173. "We were unable to delete the permanent field from the redirect-to-https middleware in your Traefik configuration. Please delete it manually. Note that this is not a critical change but recommended."
  174. );
  175. }
  176. trx.run(sql`UPDATE ${users} SET email = LOWER(email);`);
  177. trx.run(
  178. sql`UPDATE ${emailVerificationCodes} SET email = LOWER(email);`
  179. );
  180. trx.run(sql`UPDATE ${passwordResetTokens} SET email = LOWER(email);`);
  181. trx.run(sql`UPDATE ${userInvites} SET email = LOWER(email);`);
  182. trx.run(sql`UPDATE ${resourceWhitelist} SET email = LOWER(email);`);
  183. trx.run(sql`UPDATE ${resourceOtp} SET email = LOWER(email);`);
  184. const resourcesAll = await trx
  185. .select({
  186. resourceId: resources.resourceId,
  187. fullDomain: resources.fullDomain,
  188. subdomain: resources.subdomain
  189. })
  190. .from(resources);
  191. trx.run(`DROP INDEX resources_fullDomain_unique;`);
  192. trx.run(`ALTER TABLE resources
  193. DROP COLUMN fullDomain;
  194. `);
  195. trx.run(`ALTER TABLE resources
  196. DROP COLUMN subdomain;
  197. `);
  198. trx.run(sql`ALTER TABLE resources
  199. ADD COLUMN fullDomain TEXT;
  200. `);
  201. trx.run(sql`ALTER TABLE resources
  202. ADD COLUMN subdomain TEXT;
  203. `);
  204. trx.run(sql`ALTER TABLE resources
  205. ADD COLUMN http INTEGER DEFAULT true NOT NULL;
  206. `);
  207. trx.run(sql`ALTER TABLE resources
  208. ADD COLUMN protocol TEXT DEFAULT 'tcp' NOT NULL;
  209. `);
  210. trx.run(sql`ALTER TABLE resources
  211. ADD COLUMN proxyPort INTEGER;
  212. `);
  213. // write the new fullDomain and subdomain values back to the database
  214. for (const resource of resourcesAll) {
  215. await trx
  216. .update(resources)
  217. .set({
  218. fullDomain: resource.fullDomain,
  219. subdomain: resource.subdomain
  220. })
  221. .where(eq(resources.resourceId, resource.resourceId));
  222. }
  223. const targetsAll = await trx
  224. .select({
  225. targetId: targets.targetId,
  226. method: targets.method
  227. })
  228. .from(targets);
  229. trx.run(`ALTER TABLE targets
  230. DROP COLUMN method;
  231. `);
  232. trx.run(`ALTER TABLE targets
  233. DROP COLUMN protocol;
  234. `);
  235. trx.run(sql`ALTER TABLE targets
  236. ADD COLUMN method TEXT;
  237. `);
  238. // write the new method and protocol values back to the database
  239. for (const target of targetsAll) {
  240. await trx
  241. .update(targets)
  242. .set({
  243. method: target.method
  244. })
  245. .where(eq(targets.targetId, target.targetId));
  246. }
  247. trx.run(
  248. sql`ALTER TABLE 'resourceSessions' ADD 'isRequestToken' integer;`
  249. );
  250. trx.run(
  251. sql`ALTER TABLE 'resourceSessions' ADD 'userSessionId' text REFERENCES session(id);`
  252. );
  253. });
  254. console.log("Done.");
  255. }