mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-04 10:05:53 +02:00
shorten share links and add migration
This commit is contained in:
parent
302094771b
commit
74d6b3d902
12 changed files with 231 additions and 63 deletions
|
@ -3,10 +3,63 @@ import {
|
|||
Resource,
|
||||
ResourceAccessToken,
|
||||
resourceAccessToken,
|
||||
resources
|
||||
} from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { isWithinExpirationDate } from "oslo";
|
||||
import { verifyPassword } from "./password";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
|
||||
export async function verifyResourceAccessTokenSHA256({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}): Promise<{
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
tokenItem?: ResourceAccessToken;
|
||||
resource?: Resource;
|
||||
}> {
|
||||
const accessTokenHash = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(accessToken))
|
||||
);
|
||||
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(and(eq(resourceAccessToken.tokenHash, accessTokenHash)))
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
);
|
||||
|
||||
const tokenItem = res?.resourceAccessToken;
|
||||
const resource = res?.resources;
|
||||
|
||||
if (!tokenItem || !resource) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Access token does not exist for resource"
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
tokenItem.expiresAt &&
|
||||
!isWithinExpirationDate(new Date(tokenItem.expiresAt))
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Access token has expired"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
tokenItem,
|
||||
resource
|
||||
};
|
||||
}
|
||||
|
||||
export async function verifyResourceAccessToken({
|
||||
resource,
|
||||
|
|
|
@ -2,7 +2,7 @@ import path from "path";
|
|||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.1.0";
|
||||
export const APP_VERSION = "1.2.0";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
|
|
@ -20,6 +20,8 @@ import { fromError } from "zod-validation-error";
|
|||
import logger from "@server/logger";
|
||||
import { createDate, TimeSpan } from "oslo";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
|
||||
export const generateAccessTokenBodySchema = z
|
||||
.object({
|
||||
|
@ -90,11 +92,13 @@ export async function generateAccessToken(
|
|||
? createDate(new TimeSpan(validForSeconds, "s")).getTime()
|
||||
: undefined;
|
||||
|
||||
const token = generateIdFromEntropySize(25);
|
||||
const token = generateIdFromEntropySize(12);
|
||||
|
||||
const tokenHash = await hashPassword(token);
|
||||
const tokenHash = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
|
||||
const id = generateId(15);
|
||||
const id = generateId(8);
|
||||
const [result] = await db
|
||||
.insert(resourceAccessToken)
|
||||
.values({
|
||||
|
|
|
@ -566,3 +566,8 @@ authRouter.post(
|
|||
"/resource/:resourceId/access-token",
|
||||
resource.authWithAccessToken
|
||||
);
|
||||
|
||||
authRouter.post(
|
||||
"/access-token",
|
||||
resource.authWithAccessToken
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import { resources } from "@server/db/schemas";
|
||||
import { Resource, resources } from "@server/db/schemas";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
@ -10,13 +10,17 @@ import { z } from "zod";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import { createResourceSession } from "@server/auth/sessions/resource";
|
||||
import logger from "@server/logger";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import {
|
||||
verifyResourceAccessToken,
|
||||
verifyResourceAccessTokenSHA256
|
||||
} from "@server/auth/verifyResourceAccessToken";
|
||||
import config from "@server/lib/config";
|
||||
import stoi from "@server/lib/stoi";
|
||||
|
||||
const authWithAccessTokenBodySchema = z
|
||||
.object({
|
||||
accessToken: z.string(),
|
||||
accessTokenId: z.string()
|
||||
accessTokenId: z.string().optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
@ -24,13 +28,15 @@ const authWithAccessTokenParamsSchema = z
|
|||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
.optional()
|
||||
.transform(stoi)
|
||||
.pipe(z.number().int().positive().optional())
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type AuthWithAccessTokenResponse = {
|
||||
session?: string;
|
||||
redirectUrl?: string | null;
|
||||
};
|
||||
|
||||
export async function authWithAccessToken(
|
||||
|
@ -64,23 +70,62 @@ export async function authWithAccessToken(
|
|||
const { accessToken, accessTokenId } = parsedBody.data;
|
||||
|
||||
try {
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
let valid;
|
||||
let tokenItem;
|
||||
let error;
|
||||
let resource: Resource | undefined;
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||
);
|
||||
if (accessTokenId) {
|
||||
if (!resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Resource ID is required"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [foundResource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!foundResource) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||
);
|
||||
}
|
||||
|
||||
const res = await verifyResourceAccessToken({
|
||||
resource: foundResource,
|
||||
accessTokenId,
|
||||
accessToken
|
||||
});
|
||||
|
||||
valid = res.valid;
|
||||
tokenItem = res.tokenItem;
|
||||
error = res.error;
|
||||
resource = foundResource;
|
||||
} else {
|
||||
const res = await verifyResourceAccessTokenSHA256({
|
||||
accessToken
|
||||
});
|
||||
|
||||
valid = res.valid;
|
||||
tokenItem = res.tokenItem;
|
||||
error = res.error;
|
||||
resource = res.resource;
|
||||
}
|
||||
|
||||
const { valid, error, tokenItem } = await verifyResourceAccessToken({
|
||||
resource,
|
||||
accessTokenId,
|
||||
accessToken
|
||||
});
|
||||
if (!tokenItem || !resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"Access token does not exist for resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
|
@ -96,18 +141,9 @@ export async function authWithAccessToken(
|
|||
);
|
||||
}
|
||||
|
||||
if (!tokenItem || !resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"Access token does not exist for resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const token = generateSessionToken();
|
||||
await createResourceSession({
|
||||
resourceId,
|
||||
resourceId: resource.resourceId,
|
||||
token,
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
isRequestToken: true,
|
||||
|
@ -118,7 +154,8 @@ export async function authWithAccessToken(
|
|||
|
||||
return response<AuthWithAccessTokenResponse>(res, {
|
||||
data: {
|
||||
session: token
|
||||
session: token,
|
||||
redirectUrl: `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
|
|
@ -18,6 +18,7 @@ import m13 from "./scripts/1.0.0-beta13";
|
|||
import m15 from "./scripts/1.0.0-beta15";
|
||||
import m16 from "./scripts/1.0.0";
|
||||
import m17 from "./scripts/1.1.0";
|
||||
import m18 from "./scripts/1.2.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
|
@ -35,7 +36,8 @@ const migrations = [
|
|||
{ version: "1.0.0-beta.13", run: m13 },
|
||||
{ version: "1.0.0-beta.15", run: m15 },
|
||||
{ version: "1.0.0", run: m16 },
|
||||
{ version: "1.1.0", run: m17 }
|
||||
{ version: "1.1.0", run: m17 },
|
||||
{ version: "1.2.0", run: m18 }
|
||||
// Add new migrations here as they are created
|
||||
] as const;
|
||||
|
||||
|
|
23
server/setup/scripts/1.2.0.ts
Normal file
23
server/setup/scripts/1.2.0.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import db from "@server/db";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
const version = "1.2.0";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
try {
|
||||
db.transaction((trx) => {
|
||||
trx.run(
|
||||
sql`ALTER TABLE 'resources' ADD 'enabled' integer DEFAULT true NOT NULL;`
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`Migrated database schema`);
|
||||
} catch (e) {
|
||||
console.log("Unable to migrate database schema");
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue