mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-19 02:04:30 +02:00
access token endpoints and other backend support
This commit is contained in:
parent
283fb3990c
commit
72dc02ff2e
22 changed files with 905 additions and 107 deletions
|
@ -49,7 +49,14 @@ export enum ActionsEnum {
|
|||
// addUserAction = "addUserAction",
|
||||
// removeUserAction = "removeUserAction",
|
||||
// removeUserSite = "removeUserSite",
|
||||
getOrgUser = "getOrgUser"
|
||||
getOrgUser = "getOrgUser",
|
||||
"setResourcePassword" = "setResourcePassword",
|
||||
"setResourcePincode" = "setResourcePincode",
|
||||
"setResourceWhitelist" = "setResourceWhitelist",
|
||||
"getResourceWhitelist" = "getResourceWhitelist",
|
||||
"generateAccessToken" = "generateAccessToken",
|
||||
"deleteAcessToken" = "deleteAcessToken",
|
||||
"listAccessTokens" = "listAccessTokens"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { resourceSessions, ResourceSession } from "@server/db/schema";
|
||||
import {
|
||||
resourceSessions,
|
||||
ResourceSession,
|
||||
resources
|
||||
} from "@server/db/schema";
|
||||
import db from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import config from "@server/config";
|
||||
|
@ -17,12 +21,19 @@ export async function createResourceSession(opts: {
|
|||
passwordId?: number;
|
||||
pincodeId?: number;
|
||||
whitelistId?: number;
|
||||
accessTokenId?: string;
|
||||
usedOtp?: boolean;
|
||||
doNotExtend?: boolean;
|
||||
expiresAt?: number | null;
|
||||
sessionLength: number;
|
||||
}): Promise<ResourceSession> {
|
||||
if (!opts.passwordId && !opts.pincodeId && !opts.whitelistId) {
|
||||
throw new Error(
|
||||
"At least one of passwordId or pincodeId must be provided"
|
||||
);
|
||||
if (
|
||||
!opts.passwordId &&
|
||||
!opts.pincodeId &&
|
||||
!opts.whitelistId &&
|
||||
!opts.accessTokenId
|
||||
) {
|
||||
throw new Error("Auth method must be provided");
|
||||
}
|
||||
|
||||
const sessionId = encodeHexLowerCase(
|
||||
|
@ -31,11 +42,16 @@ export async function createResourceSession(opts: {
|
|||
|
||||
const session: ResourceSession = {
|
||||
sessionId: sessionId,
|
||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
||||
expiresAt:
|
||||
opts.expiresAt ||
|
||||
new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
||||
sessionLength: opts.sessionLength || SESSION_COOKIE_EXPIRES,
|
||||
resourceId: opts.resourceId,
|
||||
passwordId: opts.passwordId || null,
|
||||
pincodeId: opts.pincodeId || null,
|
||||
whitelistId: opts.whitelistId || null
|
||||
whitelistId: opts.whitelistId || null,
|
||||
doNotExtend: opts.doNotExtend || false,
|
||||
accessTokenId: opts.accessTokenId || null
|
||||
};
|
||||
|
||||
await db.insert(resourceSessions).values(session);
|
||||
|
@ -66,9 +82,18 @@ export async function validateResourceSessionToken(
|
|||
|
||||
const resourceSession = result[0];
|
||||
|
||||
if (Date.now() >= resourceSession.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
|
||||
if (Date.now() >= resourceSession.expiresAt) {
|
||||
await db
|
||||
.delete(resourceSessions)
|
||||
.where(eq(resourceSessions.sessionId, resourceSessions.sessionId));
|
||||
return { resourceSession: null };
|
||||
} else if (
|
||||
!resourceSession.doNotExtend &&
|
||||
Date.now() >=
|
||||
resourceSession.expiresAt - resourceSession.sessionLength / 2
|
||||
) {
|
||||
resourceSession.expiresAt = new Date(
|
||||
Date.now() + SESSION_COOKIE_EXPIRES
|
||||
Date.now() + resourceSession.sessionLength
|
||||
).getTime();
|
||||
await db
|
||||
.update(resourceSessions)
|
||||
|
@ -138,8 +163,7 @@ export async function invalidateAllSessions(
|
|||
|
||||
export function serializeResourceSessionCookie(
|
||||
cookieName: string,
|
||||
token: string,
|
||||
fqdn: string
|
||||
token: string
|
||||
): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
|
@ -149,8 +173,7 @@ export function serializeResourceSessionCookie(
|
|||
}
|
||||
|
||||
export function createBlankResourceSessionTokenCookie(
|
||||
cookieName: string,
|
||||
fqdn: string
|
||||
cookieName: string
|
||||
): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
|
|
|
@ -277,12 +277,32 @@ export const resourcePassword = sqliteTable("resourcePassword", {
|
|||
passwordHash: text("passwordHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||
accessTokenId: text("accessTokenId").primaryKey(),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
tokenHash: text("tokenHash").notNull(),
|
||||
sessionLength: integer("sessionLength").notNull(),
|
||||
expiresAt: integer("expiresAt"),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
createdAt: integer("createdAt").notNull()
|
||||
});
|
||||
|
||||
export const resourceSessions = sqliteTable("resourceSessions", {
|
||||
sessionId: text("id").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
expiresAt: integer("expiresAt").notNull(),
|
||||
sessionLength: integer("sessionLength").notNull(),
|
||||
doNotExtend: integer("doNotExtend", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
passwordId: integer("passwordId").references(
|
||||
() => resourcePassword.passwordId,
|
||||
{
|
||||
|
@ -300,6 +320,12 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
|||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
),
|
||||
accessTokenId: text("accessTokenId").references(
|
||||
() => resourceAccessToken.accessTokenId,
|
||||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
|
|
|
@ -4,29 +4,34 @@ import cors from "cors";
|
|||
import cookieParser from "cookie-parser";
|
||||
import config from "@server/config";
|
||||
import logger from "@server/logger";
|
||||
import { errorHandlerMiddleware, notFoundMiddleware } from "@server/middlewares";
|
||||
import {
|
||||
errorHandlerMiddleware,
|
||||
notFoundMiddleware
|
||||
} from "@server/middlewares";
|
||||
import internal from "@server/routers/internal";
|
||||
|
||||
const internalPort = config.server.internal_port;
|
||||
|
||||
export function createInternalServer() {
|
||||
const internalServer = express();
|
||||
const internalServer = express();
|
||||
|
||||
internalServer.use(helmet());
|
||||
internalServer.use(cors());
|
||||
internalServer.use(cookieParser());
|
||||
internalServer.use(express.json());
|
||||
internalServer.use(helmet());
|
||||
internalServer.use(cors());
|
||||
internalServer.use(cookieParser());
|
||||
internalServer.use(express.json());
|
||||
|
||||
const prefix = `/api/v1`;
|
||||
internalServer.use(prefix, internal);
|
||||
const prefix = `/api/v1`;
|
||||
internalServer.use(prefix, internal);
|
||||
|
||||
internalServer.use(notFoundMiddleware);
|
||||
internalServer.use(errorHandlerMiddleware);
|
||||
internalServer.use(notFoundMiddleware);
|
||||
internalServer.use(errorHandlerMiddleware);
|
||||
|
||||
internalServer.listen(internalPort, (err?: any) => {
|
||||
if (err) throw err;
|
||||
logger.info(`Internal server is running on http://localhost:${internalPort}`);
|
||||
});
|
||||
internalServer.listen(internalPort, (err?: any) => {
|
||||
if (err) throw err;
|
||||
logger.info(
|
||||
`Internal server is running on http://localhost:${internalPort}`
|
||||
);
|
||||
});
|
||||
|
||||
return internalServer;
|
||||
return internalServer;
|
||||
}
|
|
@ -16,6 +16,7 @@ const hformat = winston.format.printf(
|
|||
const transports: any = [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.colorize(),
|
||||
winston.format.splat(),
|
||||
winston.format.timestamp(),
|
||||
|
|
45
server/middlewares/helpers/canUserAccessResource.ts
Normal file
45
server/middlewares/helpers/canUserAccessResource.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import db from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { roleResources, userResources } from "@server/db/schema";
|
||||
|
||||
export async function canUserAccessResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleId
|
||||
}: {
|
||||
userId: string;
|
||||
resourceId: number;
|
||||
roleId: number;
|
||||
}): Promise<boolean> {
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
eq(roleResources.roleId, roleId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (roleResourceAccess.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userResourceAccess = await db
|
||||
.select()
|
||||
.from(userResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userResources.userId, userId),
|
||||
eq(userResources.resourceId, resourceId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (userResourceAccess.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -13,3 +13,4 @@ export * from "./verifyUserAccess";
|
|||
export * from "./verifyAdmin";
|
||||
export * from "./verifySetResourceUsers";
|
||||
export * from "./verifyUserInRole";
|
||||
export * from "./verifyAccessTokenAccess";
|
||||
|
|
123
server/middlewares/verifyAccessTokenAccess.ts
Normal file
123
server/middlewares/verifyAccessTokenAccess.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { resourceAccessToken, resources, userOrgs } from "@server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { canUserAccessResource } from "./helpers/canUserAccessResource";
|
||||
|
||||
export async function verifyAccessTokenAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const userId = req.user!.userId;
|
||||
const accessTokenId = req.params.accessTokenId;
|
||||
|
||||
if (!userId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
const [accessToken] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
|
||||
.limit(1);
|
||||
|
||||
if (!accessToken) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Access token with ID ${accessTokenId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const resourceId = accessToken.resourceId;
|
||||
|
||||
if (!resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Access token with ID ${accessTokenId} does not have a resource ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const resource = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId!))
|
||||
.limit(1);
|
||||
|
||||
if (resource.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource[0].orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Resource with ID ${resourceId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
const res = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, resource[0].orgId)
|
||||
)
|
||||
);
|
||||
req.userOrg = res[0];
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
} else {
|
||||
req.userOrgRoleId = req.userOrg.roleId;
|
||||
req.userOrgId = resource[0].orgId!;
|
||||
}
|
||||
|
||||
const resourceAllowed = await canUserAccessResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleId: req.userOrgRoleId!
|
||||
});
|
||||
|
||||
if (!resourceAllowed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying organization access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import { resources, targets, userOrgs } from "@server/db/schema";
|
|||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { canUserAccessResource } from "./helpers/canUserAccessResource";
|
||||
|
||||
export async function verifyTargetAccess(
|
||||
req: Request,
|
||||
|
@ -99,8 +100,24 @@ export async function verifyTargetAccess(
|
|||
} else {
|
||||
req.userOrgRoleId = req.userOrg.roleId;
|
||||
req.userOrgId = resource[0].orgId!;
|
||||
next();
|
||||
}
|
||||
|
||||
const resourceAllowed = await canUserAccessResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleId: req.userOrgRoleId!
|
||||
});
|
||||
|
||||
if (!resourceAllowed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
|
67
server/routers/accessToken/deleteAccessToken.ts
Normal file
67
server/routers/accessToken/deleteAccessToken.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { resourceAccessToken } from "@server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import db from "@server/db";
|
||||
|
||||
const deleteAccessTokenParamsSchema = z.object({
|
||||
accessTokenId: z.string()
|
||||
});
|
||||
|
||||
export async function deleteAccessToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = deleteAccessTokenParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { accessTokenId } = parsedParams.data;
|
||||
|
||||
const [accessToken] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(and(eq(resourceAccessToken.accessTokenId, accessTokenId)));
|
||||
|
||||
if (!accessToken) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Resource access token not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(resourceAccessToken)
|
||||
.where(and(eq(resourceAccessToken.accessTokenId, accessTokenId)));
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource access token deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
120
server/routers/accessToken/generateAccessToken.ts
Normal file
120
server/routers/accessToken/generateAccessToken.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
import { hash } from "@node-rs/argon2";
|
||||
import {
|
||||
generateId,
|
||||
generateIdFromEntropySize,
|
||||
SESSION_COOKIE_EXPIRES
|
||||
} from "@server/auth";
|
||||
import db from "@server/db";
|
||||
import { resourceAccessToken, resources } from "@server/db/schema";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/utils/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { createDate, TimeSpan } from "oslo";
|
||||
|
||||
export const generateAccessTokenBodySchema = z.object({
|
||||
validForSeconds: z.number().int().positive().optional(), // seconds
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional()
|
||||
});
|
||||
|
||||
export const generateAccssTokenParamsSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive())
|
||||
});
|
||||
|
||||
export type GenerateAccessTokenResponse = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export async function generateAccessToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
const parsedBody = generateAccessTokenBodySchema.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = generateAccssTokenParamsSchema.safeParse(req.params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
const { validForSeconds, title, description } = parsedBody.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId));
|
||||
|
||||
if (!resource) {
|
||||
return next(createHttpError(HttpCode.NOT_FOUND, "Resource not found"));
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionLength = validForSeconds
|
||||
? validForSeconds * 1000
|
||||
: SESSION_COOKIE_EXPIRES;
|
||||
const expiresAt = validForSeconds
|
||||
? createDate(new TimeSpan(validForSeconds, "s")).getTime()
|
||||
: undefined;
|
||||
|
||||
const token = generateIdFromEntropySize(25);
|
||||
|
||||
const tokenHash = await hash(token, {
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1
|
||||
});
|
||||
|
||||
const id = generateId(15);
|
||||
await db.insert(resourceAccessToken).values({
|
||||
accessTokenId: id,
|
||||
orgId: resource.orgId,
|
||||
resourceId,
|
||||
tokenHash,
|
||||
expiresAt: expiresAt || null,
|
||||
sessionLength: sessionLength,
|
||||
title: title || `${resource.name} Token ${new Date().getTime()}`,
|
||||
description: description || null,
|
||||
createdAt: new Date().getTime()
|
||||
});
|
||||
|
||||
return response<GenerateAccessTokenResponse>(res, {
|
||||
data: {
|
||||
token: `${id}.${token}`
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource access token generated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to authenticate with resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
3
server/routers/accessToken/index.ts
Normal file
3
server/routers/accessToken/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from "./generateAccessToken";
|
||||
export * from "./listAccessTokens";
|
||||
export * from "./deleteAccessToken";
|
183
server/routers/accessToken/listAccessTokens.ts
Normal file
183
server/routers/accessToken/listAccessTokens.ts
Normal file
|
@ -0,0 +1,183 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
resources,
|
||||
userResources,
|
||||
roleResources,
|
||||
resourceAccessToken
|
||||
} from "@server/db/schema";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { sql, eq, or, inArray, and, count } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import stoi from "@server/utils/stoi";
|
||||
|
||||
const listAccessTokensParamsSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(stoi)
|
||||
.pipe(z.number().int().positive().optional()),
|
||||
orgId: z.string().optional()
|
||||
})
|
||||
.refine((data) => !!data.resourceId !== !!data.orgId, {
|
||||
message: "Either resourceId or orgId must be provided, but not both"
|
||||
});
|
||||
|
||||
const listAccessTokensSchema = z.object({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("1000")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative()),
|
||||
|
||||
offset: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("0")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative())
|
||||
});
|
||||
|
||||
function queryAccessTokens(
|
||||
accessibleResourceIds: number[],
|
||||
orgId?: string,
|
||||
resourceId?: number
|
||||
) {
|
||||
const cols = {
|
||||
accessTokenId: resourceAccessToken.accessTokenId,
|
||||
orgId: resourceAccessToken.orgId,
|
||||
resourceId: resourceAccessToken.resourceId,
|
||||
sessionLength: resourceAccessToken.sessionLength,
|
||||
expiresAt: resourceAccessToken.expiresAt,
|
||||
title: resourceAccessToken.title,
|
||||
description: resourceAccessToken.description,
|
||||
createdAt: resourceAccessToken.createdAt
|
||||
};
|
||||
|
||||
if (orgId) {
|
||||
return db
|
||||
.select(cols)
|
||||
.from(resourceAccessToken)
|
||||
.where(
|
||||
and(
|
||||
inArray(resourceAccessToken.resourceId, accessibleResourceIds),
|
||||
eq(resourceAccessToken.orgId, orgId)
|
||||
)
|
||||
);
|
||||
} else if (resourceId) {
|
||||
return db
|
||||
.select(cols)
|
||||
.from(resourceAccessToken)
|
||||
.where(
|
||||
and(
|
||||
inArray(resources.resourceId, accessibleResourceIds),
|
||||
eq(resources.resourceId, resourceId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type ListAccessTokensResponse = {
|
||||
accessTokens: NonNullable<Awaited<ReturnType<typeof queryAccessTokens>>>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
export async function listAccessTokens(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = listAccessTokensSchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
parsedQuery.error.errors.map((e) => e.message).join(", ")
|
||||
)
|
||||
);
|
||||
}
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
|
||||
const parsedParams = listAccessTokensParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
parsedParams.error.errors.map((e) => e.message).join(", ")
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId, resourceId } = parsedParams.data;
|
||||
|
||||
if (orgId && orgId !== req.userOrgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const accessibleResources = await db
|
||||
.select({
|
||||
resourceId: sql<number>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`
|
||||
})
|
||||
.from(userResources)
|
||||
.fullJoin(
|
||||
roleResources,
|
||||
eq(userResources.resourceId, roleResources.resourceId)
|
||||
)
|
||||
.where(
|
||||
or(
|
||||
eq(userResources.userId, req.user!.userId),
|
||||
eq(roleResources.roleId, req.userOrgRoleId!)
|
||||
)
|
||||
);
|
||||
|
||||
const accessibleResourceIds = accessibleResources.map(
|
||||
(resource) => resource.resourceId
|
||||
);
|
||||
|
||||
let countQuery: any = db
|
||||
.select({ count: count() })
|
||||
.from(resources)
|
||||
.where(inArray(resources.resourceId, accessibleResourceIds));
|
||||
|
||||
const baseQuery = queryAccessTokens(
|
||||
accessibleResourceIds,
|
||||
orgId,
|
||||
resourceId
|
||||
);
|
||||
|
||||
const list = await baseQuery!.limit(limit).offset(offset);
|
||||
const totalCountResult = await countQuery;
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
return response<ListAccessTokensResponse>(res, {
|
||||
data: {
|
||||
accessTokens: list,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Access tokens retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ import { z } from "zod";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/utils";
|
||||
import { validateSessionToken } from "@server/auth";
|
||||
import { validateResourceSessionToken } from "@server/auth/resource";
|
||||
|
||||
export const params = z.object({
|
||||
|
|
|
@ -7,9 +7,11 @@ import { response } from "@server/utils/response";
|
|||
import { validateSessionToken } from "@server/auth";
|
||||
import db from "@server/db";
|
||||
import {
|
||||
resourceAccessToken,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resources,
|
||||
resourceWhitelist,
|
||||
User,
|
||||
userOrgs
|
||||
} from "@server/db/schema";
|
||||
|
@ -89,7 +91,12 @@ export async function verifyResourceSession(
|
|||
return notAllowed(res);
|
||||
}
|
||||
|
||||
if (!resource.sso && !pincode && !password) {
|
||||
if (
|
||||
!resource.sso &&
|
||||
!pincode &&
|
||||
!password &&
|
||||
!resource.emailWhitelistEnabled
|
||||
) {
|
||||
logger.debug("Resource allowed because no auth");
|
||||
return allowed(res);
|
||||
}
|
||||
|
@ -103,7 +110,7 @@ export async function verifyResourceSession(
|
|||
const sessionToken = sessions[config.server.session_cookie_name];
|
||||
|
||||
// check for unified login
|
||||
if (sso && sessionToken && !resource.otpEnabled) {
|
||||
if (sso && sessionToken) {
|
||||
const { session, user } = await validateSessionToken(sessionToken);
|
||||
if (session && user) {
|
||||
const isAllowed = await isUserAllowedToAccessResource(
|
||||
|
@ -125,69 +132,46 @@ export async function verifyResourceSession(
|
|||
`${config.server.resource_session_cookie_name}_${resource.resourceId}`
|
||||
];
|
||||
|
||||
if (
|
||||
sso &&
|
||||
sessionToken &&
|
||||
resourceSessionToken &&
|
||||
resource.otpEnabled
|
||||
) {
|
||||
const { session, user } = await validateSessionToken(sessionToken);
|
||||
const { resourceSession } = await validateResourceSessionToken(
|
||||
resourceSessionToken,
|
||||
resource.resourceId
|
||||
);
|
||||
|
||||
if (session && user && resourceSession) {
|
||||
if (!resourceSession.usedOtp) {
|
||||
logger.debug("Resource not allowed because OTP not used");
|
||||
return notAllowed(res, redirectUrl);
|
||||
}
|
||||
|
||||
const isAllowed = await isUserAllowedToAccessResource(
|
||||
user,
|
||||
resource
|
||||
);
|
||||
|
||||
if (isAllowed) {
|
||||
logger.debug(
|
||||
"Resource allowed because user and resource session is valid"
|
||||
);
|
||||
return allowed(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((pincode || password) && resourceSessionToken) {
|
||||
if (resourceSessionToken) {
|
||||
const { resourceSession } = await validateResourceSessionToken(
|
||||
resourceSessionToken,
|
||||
resource.resourceId
|
||||
);
|
||||
|
||||
if (resourceSession) {
|
||||
if (resource.otpEnabled && !resourceSession.usedOtp) {
|
||||
logger.debug("Resource not allowed because OTP not used");
|
||||
return notAllowed(res, redirectUrl);
|
||||
}
|
||||
return allowed(res);
|
||||
|
||||
if (
|
||||
pincode &&
|
||||
resourceSession.pincodeId === pincode.pincodeId
|
||||
) {
|
||||
logger.debug(
|
||||
"Resource allowed because pincode session is valid"
|
||||
);
|
||||
return allowed(res);
|
||||
}
|
||||
|
||||
if (
|
||||
password &&
|
||||
resourceSession.passwordId === password.passwordId
|
||||
) {
|
||||
logger.debug(
|
||||
"Resource allowed because password session is valid"
|
||||
);
|
||||
return allowed(res);
|
||||
}
|
||||
// Might not be needed
|
||||
// if (pincode && resourceSession.pincodeId) {
|
||||
// logger.debug(
|
||||
// "Resource allowed because pincode session is valid"
|
||||
// );
|
||||
// return allowed(res);
|
||||
// }
|
||||
//
|
||||
// if (password && resourceSession.passwordId) {
|
||||
// logger.debug(
|
||||
// "Resource allowed because password session is valid"
|
||||
// );
|
||||
// return allowed(res);
|
||||
// }
|
||||
//
|
||||
// if (
|
||||
// resource.emailWhitelistEnabled &&
|
||||
// resourceSession.whitelistId
|
||||
// ) {
|
||||
// logger.debug(
|
||||
// "Resource allowed because whitelist session is valid"
|
||||
// );
|
||||
// return allowed(res);
|
||||
// }
|
||||
//
|
||||
// if (resourceSession.accessTokenId) {
|
||||
// logger.debug(
|
||||
// "Resource allowed because access token session is valid"
|
||||
// );
|
||||
// return allowed(res);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,10 @@ import * as target from "./target";
|
|||
import * as user from "./user";
|
||||
import * as auth from "./auth";
|
||||
import * as role from "./role";
|
||||
import * as accessToken from "./accessToken";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import {
|
||||
verifyAccessTokenAccess,
|
||||
rateLimitMiddleware,
|
||||
verifySessionMiddleware,
|
||||
verifySessionUserMiddleware,
|
||||
|
@ -114,11 +116,13 @@ authenticated.put(
|
|||
verifyUserHasAction(ActionsEnum.createResource),
|
||||
resource.createResource
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site/:siteId/resources",
|
||||
verifyUserHasAction(ActionsEnum.listResources),
|
||||
resource.listResources
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/resources",
|
||||
verifyOrgAccess,
|
||||
|
@ -278,31 +282,59 @@ authenticated.post(
|
|||
authenticated.post(
|
||||
`/resource/:resourceId/password`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateResource), // REVIEW: group all resource related updates under update resource?
|
||||
verifyUserHasAction(ActionsEnum.setResourcePassword),
|
||||
resource.setResourcePassword
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/pincode`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateResource),
|
||||
verifyUserHasAction(ActionsEnum.setResourcePincode),
|
||||
resource.setResourcePincode
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateResource),
|
||||
verifyUserHasAction(ActionsEnum.setResourceWhitelist),
|
||||
resource.setResourceWhitelist
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.getResource),
|
||||
verifyUserHasAction(ActionsEnum.getResourceWhitelist),
|
||||
resource.getResourceWhitelist
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/access-token`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.generateAccessToken),
|
||||
accessToken.generateAccessToken
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/access-token/:accessTokenId`,
|
||||
verifyAccessTokenAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteAcessToken),
|
||||
accessToken.deleteAccessToken
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/org/:orgId/access-tokens`,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.listAccessTokens),
|
||||
accessToken.listAccessTokens
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/resource/:resourceId/access-tokens`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.listAccessTokens),
|
||||
accessToken.listAccessTokens
|
||||
);
|
||||
|
||||
unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo);
|
||||
|
||||
// authenticated.get(
|
||||
|
@ -422,3 +454,7 @@ authRouter.post("/reset-password/", auth.resetPassword);
|
|||
authRouter.post("/resource/:resourceId/password", resource.authWithPassword);
|
||||
authRouter.post("/resource/:resourceId/pincode", resource.authWithPincode);
|
||||
authRouter.post("/resource/:resourceId/whitelist", resource.authWithWhitelist);
|
||||
authRouter.post(
|
||||
"/resource/:resourceId/access-token",
|
||||
resource.authWithAccessToken
|
||||
);
|
||||
|
|
157
server/routers/resource/authWithAccessToken.ts
Normal file
157
server/routers/resource/authWithAccessToken.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
import { generateSessionToken } from "@server/auth";
|
||||
import db from "@server/db";
|
||||
import { resourceAccessToken, resources } from "@server/db/schema";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/utils/response";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie
|
||||
} from "@server/auth/resource";
|
||||
import config from "@server/config";
|
||||
import logger from "@server/logger";
|
||||
import { verify } from "@node-rs/argon2";
|
||||
import { isWithinExpirationDate } from "oslo";
|
||||
|
||||
const authWithAccessTokenBodySchema = z.object({
|
||||
accessToken: z.string()
|
||||
});
|
||||
|
||||
const authWithAccessTokenParamsSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive())
|
||||
});
|
||||
|
||||
export type AuthWithAccessTokenResponse = {
|
||||
session?: string;
|
||||
};
|
||||
|
||||
export async function authWithAccessToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
const parsedBody = authWithAccessTokenBodySchema.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = authWithAccessTokenParamsSchema.safeParse(req.params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
const { accessToken: at } = parsedBody.data;
|
||||
|
||||
const [accessTokenId, accessToken] = at.split(".");
|
||||
|
||||
try {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceAccessToken.resourceId, resourceId),
|
||||
eq(resourceAccessToken.accessTokenId, accessTokenId)
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
resources,
|
||||
eq(resources.resourceId, resourceAccessToken.resourceId)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const resource = result?.resources;
|
||||
const tokenItem = result?.resourceAccessToken;
|
||||
|
||||
if (!tokenItem) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Email is not whitelisted"
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Resource does not exist")
|
||||
);
|
||||
}
|
||||
|
||||
const validCode = await verify(tokenItem.tokenHash, accessToken, {
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1
|
||||
});
|
||||
|
||||
if (!validCode) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Invalid access token")
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
tokenItem.expiresAt &&
|
||||
!isWithinExpirationDate(new Date(tokenItem.expiresAt))
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"Access token has expired"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const token = generateSessionToken();
|
||||
await createResourceSession({
|
||||
resourceId,
|
||||
token,
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
sessionLength: tokenItem.sessionLength,
|
||||
expiresAt: tokenItem.expiresAt,
|
||||
doNotExtend: tokenItem.expiresAt ? false : true
|
||||
});
|
||||
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||
const cookie = serializeResourceSessionCookie(cookieName, token);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
return response<AuthWithAccessTokenResponse>(res, {
|
||||
data: {
|
||||
session: token
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Authenticated with resource successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to authenticate with resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -174,7 +174,6 @@ export async function authWithWhitelist(
|
|||
const cookie = serializeResourceSessionCookie(
|
||||
cookieName,
|
||||
token,
|
||||
resource.fullDomain
|
||||
);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
|
@ -188,7 +187,6 @@ export async function authWithWhitelist(
|
|||
status: HttpCode.OK
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
logger.error(e);
|
||||
return next(
|
||||
createHttpError(
|
||||
|
|
|
@ -15,3 +15,4 @@ export * from "./authWithPincode";
|
|||
export * from "./setResourceWhitelist";
|
||||
export * from "./getResourceWhitelist";
|
||||
export * from "./authWithWhitelist";
|
||||
export * from "./authWithAccessToken";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Metadata } from "next";
|
||||
import { TopbarNav } from "./components/TopbarNav";
|
||||
import { Cog, Combine, Settings, Users, Waypoints } from "lucide-react";
|
||||
import { Cog, Combine, Link, Settings, Users, Waypoints } from "lucide-react";
|
||||
import Header from "./components/Header";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import { redirect } from "next/navigation";
|
||||
|
@ -30,10 +30,15 @@ const topNavItems = [
|
|||
icon: <Waypoints className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "Access",
|
||||
title: "Users & Roles",
|
||||
href: "/{orgId}/settings/access",
|
||||
icon: <Users className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "Sharable Links",
|
||||
href: "/{orgId}/settings/links",
|
||||
icon: <Link className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "General",
|
||||
href: "/{orgId}/settings/general",
|
||||
|
@ -105,7 +110,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||
<div className="container mx-auto sm:px-0 px-3">{children}</div>
|
||||
|
||||
<footer className="w-full mt-6 py-3">
|
||||
<div className="container mx-auto flex justify-end items-center px-3 sm:px-0 text-sm text-muted space-x-3">
|
||||
<div className="container mx-auto flex justify-end items-center px-3 sm:px-0 text-sm text-muted space-x-3 select-none">
|
||||
<div>Built by Fossorial</div>
|
||||
<a
|
||||
href="https://github.com/fosrl/pangolin"
|
||||
|
@ -117,7 +122,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="w-5 h-5"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.385-1.333-1.755-1.333-1.755-1.09-.744.082-.73.082-.73 1.205.085 1.84 1.24 1.84 1.24 1.07 1.835 2.807 1.305 3.492.997.107-.775.42-1.305.763-1.605-2.665-.305-5.467-1.335-5.467-5.93 0-1.31.468-2.382 1.236-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23a11.52 11.52 0 013.006-.403c1.02.005 2.045.137 3.006.403 2.29-1.552 3.295-1.23 3.295-1.23.654 1.653.242 2.873.12 3.176.77.838 1.235 1.91 1.235 3.22 0 4.605-2.805 5.623-5.475 5.92.43.37.814 1.1.814 2.22v3.293c0 .32.217.693.825.576C20.565 21.795 24 17.298 24 12 24 5.373 18.627 0 12 0z" />
|
||||
</svg>
|
||||
|
|
|
@ -45,7 +45,7 @@ import { Alert, AlertDescription } from "@app/components/ui/alert";
|
|||
import { formatAxiosError } from "@app/lib/utils";
|
||||
import { AxiosResponse } from "axios";
|
||||
import LoginForm from "@app/components/LoginForm";
|
||||
import { AuthWithPasswordResponse, AuthWithWhitelistResponse } from "@server/routers/resource";
|
||||
import { AuthWithPasswordResponse, AuthWithAccessTokenResponse } from "@server/routers/resource";
|
||||
import { redirect } from "next/dist/server/api-utils";
|
||||
import ResourceAccessDenied from "./ResourceAccessDenied";
|
||||
import { createApiClient } from "@app/api";
|
||||
|
@ -166,7 +166,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||
|
||||
const onWhitelistSubmit = (values: any) => {
|
||||
setLoadingLogin(true);
|
||||
api.post<AxiosResponse<AuthWithWhitelistResponse>>(
|
||||
api.post<AxiosResponse<AuthWithAccessTokenResponse>>(
|
||||
`/auth/resource/${props.resource.id}/whitelist`,
|
||||
{ email: values.email, otp: values.otp }
|
||||
)
|
||||
|
|
|
@ -43,8 +43,8 @@ export default async function ResourceAuthPage(props: {
|
|||
);
|
||||
}
|
||||
|
||||
const hasAuth = authInfo.password || authInfo.pincode || authInfo.sso;
|
||||
const isSSOOnly = authInfo.sso && !authInfo.password && !authInfo.pincode;
|
||||
const hasAuth = authInfo.password || authInfo.pincode || authInfo.sso || authInfo.whitelist;
|
||||
const isSSOOnly = authInfo.sso && !authInfo.password && !authInfo.pincode && !authInfo.whitelist;
|
||||
|
||||
const redirectUrl = searchParams.redirect || authInfo.url;
|
||||
|
||||
|
@ -70,8 +70,6 @@ export default async function ResourceAuthPage(props: {
|
|||
AxiosResponse<CheckResourceSessionResponse>
|
||||
>(`/resource-session/${params.resourceId}/${sessionId}`);
|
||||
|
||||
console.log("resource session already exists and is valid");
|
||||
|
||||
if (res && res.data.data.valid) {
|
||||
doRedirect = true;
|
||||
}
|
||||
|
@ -96,7 +94,6 @@ export default async function ResourceAuthPage(props: {
|
|||
await authCookieHeader(),
|
||||
);
|
||||
|
||||
console.log(res.data);
|
||||
doRedirect = true;
|
||||
} catch (e) {
|
||||
userIsUnauthorized = true;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue