From 7bd84e0de0738858d817d8abf9f5a00d1b012f18 Mon Sep 17 00:00:00 2001 From: pike Date: Sat, 5 Apr 2025 09:42:04 +0200 Subject: [PATCH 1/3] feat: Add authservice implementing sauth, providing generateToken, getToken,verifyToken,login,logout and setPassword fix: https://github.com/commonpike/fairpost/issues/132 --- .env.dist | 2 +- package-lock.json | 55 ++++++++++++++++++++ package.json | 2 + src/cli.ts | 2 + src/mappers/UserMapper.ts | 13 +++++ src/models/Operator.ts | 4 ++ src/models/User.ts | 40 +++++++++++++++ src/models/User/UserData.ts | 36 ++++++++++++++ src/services/AuthService.ts | 94 +++++++++++++++++++++++++++++++++++ src/services/Fairpost.ts | 70 ++++++++++++++++++++++++++ src/services/Server.ts | 57 +++++++++++++++++++-- src/types/CommandArguments.ts | 1 + src/types/UserDto.ts | 1 + 13 files changed, 371 insertions(+), 6 deletions(-) create mode 100644 src/services/AuthService.ts diff --git a/.env.dist b/.env.dist index 738a8fe..6fb8b78 100644 --- a/.env.dist +++ b/.env.dist @@ -13,6 +13,7 @@ FAIRPOST_STORAGE_AUTH=json FAIRPOST_USER_HOMEDIR=users/%user% FAIRPOST_USER_JSONPATH=storage.json FAIRPOST_USER_FEEDPATH=feed +FAIRPOST_USER_AUTH=fairpost # fairpost / cognito # cli oauth client settings FAIRPOST_OAUTH_USERAGENT=Fairpost 1.0 @@ -22,7 +23,6 @@ FAIRPOST_OAUTH_PORT=8000 # rest api server settings FAIRPOST_SERVER_HOSTNAME=localhost FAIRPOST_SERVER_PORT=8000 -FAIRPOST_SERVER_AUTH=none FAIRPOST_SERVER_CORS=* # user feed settings (defaults) diff --git a/package-lock.json b/package-lock.json index 8c71016..c3b6855 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@flystorage/file-storage": "^1.0.1", "@flystorage/local-fs": "^1.0.0", "@googleapis/youtube": "^20.0.0", + "argon2": "^0.41.1", + "cookie": "^1.0.2", "dotenv": "^16.0.3", "fast-xml-parser": "^4.3.2", "google-auth-library": "^9.4.2", @@ -779,6 +781,15 @@ "node": ">= 8" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -1144,6 +1155,21 @@ "dev": true, "license": "MIT" }, + "node_modules/argon2": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz", + "integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "node-addon-api": "^8.1.0", + "node-gyp-build": "^4.8.1" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1330,6 +1356,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2627,6 +2662,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -2664,6 +2708,17 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/package.json b/package.json index 51a1b22..40a71d4 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "@flystorage/file-storage": "^1.0.1", "@flystorage/local-fs": "^1.0.0", "@googleapis/youtube": "^20.0.0", + "argon2": "^0.41.1", + "cookie": "^1.0.2", "dotenv": "^16.0.3", "fast-xml-parser": "^4.3.2", "google-auth-library": "^9.4.2", diff --git a/src/cli.ts b/src/cli.ts index d56172b..9db80fd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -23,6 +23,7 @@ const COMMAND = process.argv[2]?.includes("@") // options const DRY_RUN = !!getOption("dry-run"); const OPERATOR = (getOption("operator") as string) ?? "admin"; +const PASSWORD = (getOption("password") as string) ?? undefined; const PLATFORMS = ((getOption("platforms") as string)?.split(",") as PlatformId[]) ?? undefined; const SOURCES = (getOption("sources") as string)?.split(",") ?? undefined; @@ -54,6 +55,7 @@ async function main() { const output = await Fairpost.execute(operator, user, COMMAND, { dryrun: DRY_RUN, user: USER, + password: PASSWORD, platforms: PLATFORMS, platform: PLATFORM, sources: SOURCES, diff --git a/src/mappers/UserMapper.ts b/src/mappers/UserMapper.ts index 0b5d7eb..1a2e51a 100644 --- a/src/mappers/UserMapper.ts +++ b/src/mappers/UserMapper.ts @@ -31,6 +31,13 @@ export default class UserMapper extends AbstractMapper { set: ["manageUsers"], required: false, }, + report: { + type: "json", + label: "Report", + get: ["any"], + set: ["none"], + required: false, + }, }; /** @@ -52,6 +59,12 @@ export default class UserMapper extends AbstractMapper { case "loglevel": dto[field] = this.user.data.get("settings", "LOGGER_LEVEL"); break; + case "report": + dto[field] = { + todo: "report", + // TODO - this should be a report object + }; + break; } } return dto; diff --git a/src/models/Operator.ts b/src/models/Operator.ts index 50f2ecd..0556124 100644 --- a/src/models/Operator.ts +++ b/src/models/Operator.ts @@ -40,6 +40,10 @@ export default class Operator { } const permissions = { manageUsers: this.authenticated && this.roles.includes("admin"), + manageAccount: + !!user && + this.authenticated && + (this.id === user.id || this.roles.includes("admin")), manageFeed: !!user && this.authenticated && diff --git a/src/models/User.ts b/src/models/User.ts index 09c08fd..31ed477 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,3 +1,5 @@ +import { basename } from "path"; + import * as platformClasses from "../platforms/index.ts"; import { PlatformId } from "../platforms/index.ts"; @@ -56,6 +58,44 @@ export default class User { this.mapper = new UserMapper(this); } + /** + * getUsers + * + * get all users, but do not init them all the way; + * filter them for public. This is dumb and heavy now. + * https://github.com/commonpike/fairpost/issues/135 + * @param publicOnly - wether users should be public + * @returns new user object + */ + public static async getUsers(publicOnly: boolean): Promise { + const users: User[] = []; + const globalfs = new GlobalFs(); + if (!process.env.FAIRPOST_USER_HOMEDIR) { + throw new Error("FAIRPOST_USER_HOMEDIR not set in env"); + } + const srcdir = process.env.FAIRPOST_USER_HOMEDIR.replace("%user%", ""); + const listing = await globalfs.list(srcdir).toArray(); + const ids = listing + .map((entry) => { + if (entry.isDirectory) { + return basename(entry.path); + } + }) + .filter((id) => id !== undefined); + for (const id of ids) { + const user = new User(id); + await user.files.init(); + await user.data.init(); + if ( + !publicOnly || + user.data.get("settings", "IS_PUBLIC", "false") !== "false" + ) { + users.push(user); + } + } + return users; + } + /** * getUser * diff --git a/src/models/User/UserData.ts b/src/models/User/UserData.ts index 6d7e067..874d26e 100644 --- a/src/models/User/UserData.ts +++ b/src/models/User/UserData.ts @@ -134,6 +134,42 @@ export default class UserData { // dont forget to call save() } + public del(store: StorageType, key: string) { + const storageKey = StorageKeys[store]; + const storage = process.env[storageKey] ?? "none"; + switch (storage) { + case "env": + return this.delEnv(store, key); + case "json-env": + case "json": + return this.delJson(store, key); + default: + throw new Error("UserData: Storage " + storage + " not implemented"); + } + } + + private delEnv(store: StorageType, key: string) { + const ui = process.env.FAIRPOST_UI ?? "none"; + if (ui === "cli") { + console.log("Remove this value from your users .env file:"); + console.log(); + console.log("FAIRPOST_" + key); + console.log(); + } else { + throw new Error("UserData.setEnv: UI " + ui + " not supported"); + } + } + + private delJson(store: StorageType, key: string) { + if (!(store in this.jsonData)) { + this.jsonData[store] = {}; + } + if (key in this.jsonData[store]) { + delete this.jsonData[store][key]; + } + // dont forget to call save() + } + private async loadJson() { if (await this.user.files.isFile(this.jsonPath)) { const contents = await this.user.files.readFile(this.jsonPath); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts new file mode 100644 index 0000000..54c9e97 --- /dev/null +++ b/src/services/AuthService.ts @@ -0,0 +1,94 @@ +import argon2 from "argon2"; +import { randomBytes } from "crypto"; +import User from "../models/User.ts"; + +/** + * AuthService handles authentication; wrapping + * sauth (simple auth) or eg amazon cognito + * given by FAIRPOST_USER_AUTH + */ + +export default class AuthService { + public static async setPassword(user: User, pass: string) { + switch (process.env.FAIRPOST_USER_AUTH) { + default: { + const crypted = await argon2.hash(pass); + user.log.info("AuthService", "Setting password .."); + user.data.set("auth", "FAIRPOST_PASSWORD", crypted); + user.data.save(); + } + } + } + + public static async generateToken( + user: User, + ): Promise<{ token: string; timeout: Date }> { + switch (process.env.FAIRPOST_USER_AUTH) { + default: { + const token = randomBytes(32).toString("hex"); + const timeout = new Date(); + timeout.setHours(timeout.getHours() + 1); + user.data.set("auth", "FAIRPOST_ACCESS_TOKEN", token); + user.data.set("auth", "FAIRPOST_ACCESS_EXPIRY", timeout.toISOString()); + user.data.save(); + return { token, timeout }; + } + } + } + + public static async getToken(user: User): Promise { + switch (process.env.FAIRPOST_USER_AUTH) { + default: { + const timeout = user.data.get("auth", "FAIRPOST_ACCESS_EXPIRY", ""); + if (timeout) { + if (new Date() < new Date(timeout)) { + return user.data.get("auth", "FAIRPOST_ACCESS_TOKEN"); + } else { + AuthService.logout(user); + throw user.log.error("AuthService", "getToken: token timed out"); + } + } else { + throw user.log.error("AuthService", "getToken: no token available"); + } + } + } + } + + public static async login(user: User, pass?: string): Promise { + switch (process.env.FAIRPOST_USER_AUTH) { + default: { + if (pass) { + const crypted = user.data.get("auth", "FAIRPOST_PASSWORD"); + if (await argon2.verify(crypted, pass)) { + const result = await AuthService.generateToken(user); + return result.token; + } else { + throw user.log.error("AuthService.login", "Wrong password"); + } + } else { + throw user.log.error("AuthService.login", "Missing password"); + } + } + } + } + public static async verifyToken(user: User, token: string): Promise { + switch (process.env.FAIRPOST_USER_AUTH) { + default: { + // masquerading: + // const authUserId = user.data.get('auth','managed-by',user.id); + const userToken = await AuthService.getToken(user); + if (token !== userToken) { + user.log.error("AuthService", "verifyToken: tokens dont match"); + return false; + } + return true; + } + } + } + + public static async logout(user: User) { + user.data.del("auth", "FAIRPOST_ACCESS_TOKEN"); + user.data.del("auth", "FAIRPOST_ACCESS_EXPIRY"); + user.data.save(); + } +} diff --git a/src/services/Fairpost.ts b/src/services/Fairpost.ts index 0ffdee8..c018133 100644 --- a/src/services/Fairpost.ts +++ b/src/services/Fairpost.ts @@ -19,6 +19,7 @@ import { import Post from "../models/Post.ts"; import Server from "../services/Server.ts"; +import AuthService from "../services/AuthService.ts"; import Operator from "../models/Operator.ts"; import User from "../models/User.ts"; @@ -92,9 +93,72 @@ class Fairpost { throw new Error("user is required for command " + command); } const newUser = await User.createUser(args.user); + if (args.password) { + AuthService.setPassword(newUser, args.password); + } output = await newUser.mapper.getDto(operator); break; } + + case "login": { + if (!user) { + throw new Error("user is required for command " + command); + } + if (!args.password) { + throw new Error("password is required for command " + command); + } + const token = await AuthService.login(user, args.password); + output = { success: !!token }; + break; + } + + case "logout": { + if (!permissions.manageAccount) { + throw new Error("Missing permissions for command " + command); + } + if (!user) { + throw new Error("user is required for command " + command); + } + await AuthService.logout(user); + output = { success: true }; + break; + } + + case "set-password": { + if (!permissions.manageAccount) { + throw new Error("Missing permissions for command " + command); + } + if (!user) { + throw new Error("user is required for command " + command); + } + if (!args.password) { + throw new Error("password is required for command " + command); + } + await AuthService.setPassword(user, args.password); + output = { success: true }; + break; + } + + case "refresh-token": { + if (!permissions.manageAccount) { + throw new Error("Missing permissions for command " + command); + } + if (!user) { + throw new Error("user is required for command " + command); + } + const result = await AuthService.generateToken(user); + output = { success: true, result: result.token }; + break; + } + + case "get-users": { + const users = await User.getUsers(!permissions.manageUsers); + output = await Promise.all( + users.map((user) => user.mapper.getDto(operator)), + ); + break; + } + case "get-user": { if (!user) { throw new Error("Missing user for command " + command); @@ -103,6 +167,7 @@ class Fairpost { } break; } + case "get-feed": { if (!permissions.manageFeed) { throw new Error("Missing permissions for command " + command); @@ -704,6 +769,11 @@ class Fairpost { `${cmd} @userid prepare-posts [--sources=xxx,xxx|--source=xxx] [--platforms=xxx,xxx|--platform=xxx]`, `${cmd} @userid schedule-next-posts [--date=xxxx-xx-xx] [--sources=xxx,xxx] [--platforms=xxx,xxx] `, `${cmd} @userid publish-due-posts [--sources=xxx,xxx] [--platforms=xxx,xxx] [--dry-run]`, + "\n# account mgmt:", + `${cmd} @userid login --password=xxx`, + `${cmd} @userid logout`, + `${cmd} @userid set-password --password=xxx`, + `${cmd} @userid refresh-token`, "\n# admin only:", `${cmd} @userid create-user`, `${cmd} serve`, diff --git a/src/services/Server.ts b/src/services/Server.ts index db7ed68..add4c19 100644 --- a/src/services/Server.ts +++ b/src/services/Server.ts @@ -1,7 +1,9 @@ +import cookie from "cookie"; import { createReadStream } from "fs"; import { createServer, IncomingMessage, ServerResponse } from "http"; import Fairpost from "./Fairpost.ts"; +import AuthService from "./AuthService.ts"; import { JSONReplacer } from "../utilities.ts"; import { PlatformId } from "../platforms/index.ts"; import { PostStatus } from "../types/index.ts"; @@ -37,7 +39,10 @@ export default class Server { ); response.setHeader("Access-Control-Request-Method", "*"); response.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type"); + response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Headers", "*"); + if (request.method === "OPTIONS") { response.writeHead(200); response.end(); @@ -63,6 +68,7 @@ export default class Server { "", ]; const userid = username.replace("@", ""); + const password = parsed.searchParams.get("password") || undefined; const dryrun = parsed.searchParams.get("dry-run") === "true"; const date = parsed.searchParams.get("date"); const post = parsed.searchParams.get("post"); @@ -80,6 +86,7 @@ export default class Server { (parsed.searchParams.get("status") as PostStatus) || undefined; const args = { + password: password, dryrun: dryrun || undefined, platforms: platforms, platform: platform, @@ -93,11 +100,12 @@ export default class Server { let output = undefined; let error = false as boolean | unknown; try { - const operator = Server.getOperator(userid, request); const user = await User.getUser(userid); + const operator = await Server.getOperator(user, request); output = await Fairpost.execute(operator, user, command, args); code = 200; Fairpost.logger.trace("Server.handleRequest", "success", request.url); + await Server.addFairpostSession(response, user, command); } catch (e) { Fairpost.logger.error("Server.handleRequest", "error", request.url); code = 500; @@ -128,11 +136,50 @@ export default class Server { ), ); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public static getOperator(userid: string, request: IncomingMessage) { - if (process.env.FAIRPOST_SERVER_AUTH === "none") { - return new Operator(userid, ["user"], "api", true); + public static async getOperator(user: User, request: IncomingMessage) { + if (process.env.FAIRPOST_USER_AUTH === "fairpost") { + const cookies = cookie.parse(request.headers.cookie || ""); + if ("FairpostSession" in cookies) { + if ( + await AuthService.verifyToken(user, cookies["FairpostSession"] ?? "") + ) { + return new Operator(user.id, ["user"], "api", true); + } + } + return new Operator(user.id, ["anonymous"], "api", false); } return new Operator("anonymous", ["anonymous"], "api", true); } + + public static async addFairpostSession( + response: ServerResponse, + user: User, + command: string, + ) { + if (process.env.FAIRPOST_USER_AUTH === "fairpost") { + if (["login", "refresh-token"].includes(command)) { + const token = await AuthService.getToken(user); + response.setHeader( + "Set-Cookie", + cookie.serialize("FairpostSession", token, { + httpOnly: true, + secure: true, + sameSite: "strict", + maxAge: 60 * 60, // 1 hour + }), + ); + } + if (command === "logout") { + response.setHeader( + "Set-Cookie", + cookie.serialize("FairpostSession", "", { + httpOnly: true, + secure: true, + sameSite: "strict", + maxAge: 0, + }), + ); + } + } + } } diff --git a/src/types/CommandArguments.ts b/src/types/CommandArguments.ts index f0e80b7..92c2444 100644 --- a/src/types/CommandArguments.ts +++ b/src/types/CommandArguments.ts @@ -8,6 +8,7 @@ import { PostStatus } from "./index.ts"; export default interface CommandArguments { dryrun?: boolean; user?: string; + password?: string; platforms?: PlatformId[]; platform?: PlatformId; sources?: string[]; diff --git a/src/types/UserDto.ts b/src/types/UserDto.ts index c108d0e..bb9537e 100644 --- a/src/types/UserDto.ts +++ b/src/types/UserDto.ts @@ -3,4 +3,5 @@ export default interface UserDto { id: string; homedir?: string; loglevel?: string; + report?: object; // TODO be more specific } From 1a24319eebb21466acc897c6e0eef0a945cadcef Mon Sep 17 00:00:00 2001 From: pike Date: Sat, 5 Apr 2025 11:07:47 +0200 Subject: [PATCH 2/3] feat: Allow get-users without user --- src/mappers/PlatformMapper.ts | 4 +++ src/services/AuthService.ts | 4 +-- src/services/Server.ts | 55 ++++++++++++++++++++++++----------- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/mappers/PlatformMapper.ts b/src/mappers/PlatformMapper.ts index 8097923..4f70b3c 100644 --- a/src/mappers/PlatformMapper.ts +++ b/src/mappers/PlatformMapper.ts @@ -59,6 +59,10 @@ export default class PlatformMapper extends AbstractMapper { case "active": dto[field] = !!this.platform.active; break; + case "model": + case "id": + case "user_id": + break; default: switch (this.mapping[field].type) { case "string": diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 54c9e97..0d1532e 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -4,8 +4,8 @@ import User from "../models/User.ts"; /** * AuthService handles authentication; wrapping - * sauth (simple auth) or eg amazon cognito - * given by FAIRPOST_USER_AUTH + * fairposts simple username/password auth or eg amazon cognito + * as decided by by FAIRPOST_USER_AUTH */ export default class AuthService { diff --git a/src/services/Server.ts b/src/services/Server.ts index add4c19..76617ca 100644 --- a/src/services/Server.ts +++ b/src/services/Server.ts @@ -63,11 +63,21 @@ export default class Server { request.url ?? "/", `${request.headers.protocol}://${request.headers.host}`, ); - const [username, command] = parsed.pathname?.split("/").slice(1) ?? [ - "", - "", - ]; - const userid = username.replace("@", ""); + + // read userid and command from path + let username = undefined, + userid = undefined, + command = undefined; + const [part1, part2] = parsed.pathname?.split("/").slice(1) ?? ["", ""]; + if (part1.startsWith("@")) { + username = part1; + userid = part1.replace("@", ""); + command = part2; + } else { + command = part1; + } + + // read other params from query const password = parsed.searchParams.get("password") || undefined; const dryrun = parsed.searchParams.get("dry-run") === "true"; const date = parsed.searchParams.get("date"); @@ -100,12 +110,18 @@ export default class Server { let output = undefined; let error = false as boolean | unknown; try { - const user = await User.getUser(userid); - const operator = await Server.getOperator(user, request); + let user = undefined; + if (userid !== undefined) { + user = await User.getUser(userid); + } + const operator = await Server.getOperator(request, user); + output = await Fairpost.execute(operator, user, command, args); code = 200; Fairpost.logger.trace("Server.handleRequest", "success", request.url); - await Server.addFairpostSession(response, user, command); + if (user !== undefined) { + await Server.addFairpostSession(response, user, command); + } } catch (e) { Fairpost.logger.error("Server.handleRequest", "error", request.url); code = 500; @@ -136,17 +152,22 @@ export default class Server { ), ); } - public static async getOperator(user: User, request: IncomingMessage) { - if (process.env.FAIRPOST_USER_AUTH === "fairpost") { - const cookies = cookie.parse(request.headers.cookie || ""); - if ("FairpostSession" in cookies) { - if ( - await AuthService.verifyToken(user, cookies["FairpostSession"] ?? "") - ) { - return new Operator(user.id, ["user"], "api", true); + public static async getOperator(request: IncomingMessage, user?: User) { + if (user !== undefined) { + if (process.env.FAIRPOST_USER_AUTH === "fairpost") { + const cookies = cookie.parse(request.headers.cookie || ""); + if ("FairpostSession" in cookies) { + if ( + await AuthService.verifyToken( + user, + cookies["FairpostSession"] ?? "", + ) + ) { + return new Operator(user.id, ["user"], "api", true); + } } + return new Operator(user.id, ["anonymous"], "api", false); } - return new Operator(user.id, ["anonymous"], "api", false); } return new Operator("anonymous", ["anonymous"], "api", true); } From 56cc5240005ef1b4df890ab258423a08ac9c3177 Mon Sep 17 00:00:00 2001 From: pike Date: Sat, 5 Apr 2025 23:13:41 +0200 Subject: [PATCH 3/3] fix: Testing UI cors --- .env.dist | 1 + README.md | 6 ++++++ src/services/Server.ts | 8 ++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.env.dist b/.env.dist index 6fb8b78..4df2fd7 100644 --- a/.env.dist +++ b/.env.dist @@ -24,6 +24,7 @@ FAIRPOST_OAUTH_PORT=8000 FAIRPOST_SERVER_HOSTNAME=localhost FAIRPOST_SERVER_PORT=8000 FAIRPOST_SERVER_CORS=* +FAIRPOST_SERVER_SECURE=false # user feed settings (defaults) FAIRPOST_FEED_INTERVAL=6 #days diff --git a/README.md b/README.md index f6203a1..3b31776 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,12 @@ fairpost: @userid prepare-posts [--sources=xxx,xxx|--source=xxx] [--platforms=x fairpost: @userid schedule-next-posts [--date=xxxx-xx-xx] [--sources=xxx,xxx] [--platforms=xxx,xxx] fairpost: @userid publish-due-posts [--sources=xxx,xxx] [--platforms=xxx,xxx] [--dry-run] +# account mgmt: +fairpost: @userid login --password=xxx`, +fairpost: @userid logout`, +fairpost: @userid set-password --password=xxx`, +fairpost: @userid refresh-token`, + # admin only: fairpost: create-user --userid=xxx fairpost: get-user --userid=xxx diff --git a/src/services/Server.ts b/src/services/Server.ts index 76617ca..580b314 100644 --- a/src/services/Server.ts +++ b/src/services/Server.ts @@ -184,8 +184,12 @@ export default class Server { "Set-Cookie", cookie.serialize("FairpostSession", token, { httpOnly: true, - secure: true, - sameSite: "strict", + secure: + process.env.FAIRPOST_SERVER_SECURE === "false" ? false : true, + sameSite: + process.env.FAIRPOST_SERVER_SECURE === "false" + ? "none" + : "strict", maxAge: 60 * 60, // 1 hour }), );