Skip to content

Commit

Permalink
Refactor session handling and update related resolvers; modify databa…
Browse files Browse the repository at this point in the history
…se schema to allow nullable personId
  • Loading branch information
jthoward64 committed Jan 16, 2025
1 parent f2c89fe commit f19f7ca
Show file tree
Hide file tree
Showing 20 changed files with 386 additions and 516 deletions.
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ export default eslintTs.config(
"@typescript-eslint/no-misused-promises": "off",
// Disabled for performance
"@typescript-eslint/no-unsafe-assignment": "off",
// Disabled for performance
"@typescript-eslint/no-base-to-string": "off",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/return-await": "error",
"@typescript-eslint/consistent-type-imports": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import random from "lodash/random";
import type { ImageSourcePropType } from "react-native";

Expand Down
2 changes: 2 additions & 0 deletions packages/mobile/src/theme/components.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable */
import type { ComponentTheme, Theme } from "native-base";
// @ts-expect-error Broken module
import originalComponentThemes from "native-base/components";

const {
Expand Down
2 changes: 1 addition & 1 deletion packages/portal/gql/graphql-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export type introspection_types = {
'String': unknown;
'TeamLegacyStatus': { name: 'TeamLegacyStatus'; enumValues: 'DemoTeam' | 'NewTeam' | 'ReturningTeam'; };
'TeamNode': { kind: 'OBJECT'; name: 'TeamNode'; fields: { 'committeeIdentifier': { name: 'committeeIdentifier'; type: { kind: 'ENUM'; name: 'CommitteeIdentifier'; ofType: null; } }; 'createdAt': { name: 'createdAt'; type: { kind: 'SCALAR'; name: 'LuxonDateTime'; ofType: null; } }; 'fundraisingEntries': { name: 'fundraisingEntries'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ListFundraisingEntriesResponse'; ofType: null; }; } }; 'fundraisingTotalAmount': { name: 'fundraisingTotalAmount'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'GlobalId'; ofType: null; }; } }; 'legacyStatus': { name: 'legacyStatus'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'ENUM'; name: 'TeamLegacyStatus'; ofType: null; }; } }; 'marathon': { name: 'marathon'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MarathonNode'; ofType: null; }; } }; 'members': { name: 'members'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MembershipNode'; ofType: null; }; }; }; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'pointEntries': { name: 'pointEntries'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'PointEntryNode'; ofType: null; }; }; }; } }; 'solicitationCode': { name: 'solicitationCode'; type: { kind: 'OBJECT'; name: 'SolicitationCodeNode'; ofType: null; } }; 'totalPoints': { name: 'totalPoints'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'type': { name: 'type'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'ENUM'; name: 'TeamType'; ofType: null; }; } }; 'updatedAt': { name: 'updatedAt'; type: { kind: 'SCALAR'; name: 'LuxonDateTime'; ofType: null; } }; }; };
'TeamResolverFilterFields': { name: 'TeamResolverFilterFields'; enumValues: 'legacyStatus' | 'marathonYear' | 'name' | 'type'; };
'TeamResolverFilterFields': { name: 'TeamResolverFilterFields'; enumValues: 'legacyStatus' | 'marathonYear' | 'name' | 'totalPoints' | 'type'; };
'TeamResolverFilterGroup': { kind: 'INPUT_OBJECT'; name: 'TeamResolverFilterGroup'; isOneOf: false; inputFields: [{ name: 'children'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INPUT_OBJECT'; name: 'TeamResolverFilterGroup'; ofType: null; }; }; }; }; defaultValue: "[]" }, { name: 'filters'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INPUT_OBJECT'; name: 'TeamResolverFilterItem'; ofType: null; }; }; }; }; defaultValue: "[]" }, { name: 'operator'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'ENUM'; name: 'FilterGroupOperator'; ofType: null; }; }; defaultValue: null }]; };
'TeamResolverFilterItem': { kind: 'INPUT_OBJECT'; name: 'TeamResolverFilterItem'; isOneOf: false; inputFields: [{ name: 'field'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'ENUM'; name: 'TeamResolverFilterFields'; ofType: null; }; }; defaultValue: null }, { name: 'filter'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INPUT_OBJECT'; name: 'SomeFilter'; ofType: null; }; }; defaultValue: null }]; };
'TeamResolverSearchFilter': { kind: 'INPUT_OBJECT'; name: 'TeamResolverSearchFilter'; isOneOf: false; inputFields: [{ name: 'fields'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'ENUM'; name: 'TeamResolverFilterFields'; ofType: null; }; }; }; defaultValue: null }, { name: 'query'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'NonEmptyString'; ofType: null; }; }; defaultValue: null }]; };
Expand Down
1 change: 0 additions & 1 deletion packages/portal/src/config/refine/resources.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-base-to-string */
import {
BellOutlined,
CalendarOutlined,
Expand Down
1 change: 0 additions & 1 deletion packages/portal/src/elements/components/sider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ export const Sider: React.FC<
},
label: (
<>
{/* eslint-disable-next-line @typescript-eslint/no-base-to-string */}
<Link to={String(list ?? "")}>{meta?.label}</Link>
{!menuCollapsed && isSelected && (
<div className="ant-menu-tree-arrow" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Session" ALTER COLUMN "personId" DROP NOT NULL;
4 changes: 2 additions & 2 deletions packages/server/prisma/schema/person.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ model Session {
ip String?
expiresAt DateTime
personId Int
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
personId Int?
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade)
}

enum AuthSource {
Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/jobs/garbageCollectLogins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Cron } from "croner";
import { logger } from "#logging/standardLogging.js";
import { JobStateRepository } from "#repositories/JobState.js";
import { LoginFlowRepository } from "#repositories/LoginFlowSession.js";
import { SessionRepository } from "#repositories/Session.js";

const jobStateRepository = Container.get(JobStateRepository);

Expand All @@ -25,6 +26,10 @@ export const garbageCollectLoginFlowSessions = new Cron(
logger.info("Garbage collecting old login flows");
const loginFlowSessionRepository = Container.get(LoginFlowRepository);
await loginFlowSessionRepository.gcOldLoginFlows();
const sessionRepository = Container.get(SessionRepository);
await sessionRepository
.gcOldSessions()
.promise.then((res) => res.unwrap());

await jobStateRepository.logCompletedJob(garbageCollectLoginFlowSessions);
} catch (error) {
Expand Down
76 changes: 30 additions & 46 deletions packages/server/src/lib/auth/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ContextFunction } from "@apollo/server";
import type { ExpressContextFunctionArgument } from "@apollo/server/express4";
import { Container } from "@freshgum/typedi";
import { CommitteeRole } from "@prisma/client";
import { CommitteeRole, type Person, type Session } from "@prisma/client";
import type {
AppAbility,
AuthorizationContext,
Expand All @@ -25,11 +25,10 @@ import { superAdminLinkbluesToken } from "#lib/typediTokens.js";
import { personModelToResource } from "#repositories/person/personModelToResource.js";
import { PersonRepository } from "#repositories/person/PersonRepository.js";

import { parseUserJwt } from "./index.js";

export interface GraphQLContext extends AuthorizationContext {
serverUrl: URL;
ability: AppAbility;
session: Session | null;
}

type UserContext = Partial<
Expand All @@ -39,28 +38,15 @@ type UserContext = Partial<
>
>;
async function getUserInfo(
userId: string
person: Person
): Promise<ConcreteResult<UserContext>> {
const outputContext: UserContext = {};
const personRepository = Container.get(PersonRepository);
const person = await personRepository.findPersonAndTeamsByUnique({
uuid: userId,
});

if (person.isErr()) {
if (person.error.tag === ErrorCode.NotFound) {
// Short-circuit if the user is not found
return Ok(outputContext);
}
return person;
}

// If we found a user, set the authenticated user
// Convert the user to a resource and set it on the context
const personResource = await personModelToResource(
person.value,
personRepository
).promise;
const personResource = await personModelToResource(person, personRepository)
.promise;
if (personResource.isErr()) {
return personResource;
}
Expand All @@ -70,7 +56,7 @@ async function getUserInfo(
// Set the teams the user is on
let teamMemberships = await personRepository.findMembershipsOfPerson(
{
id: person.value.id,
id: person.id,
},
{},
undefined,
Expand All @@ -96,7 +82,7 @@ async function getUserInfo(
// Set the effective committee roles the user has
const effectiveCommitteeRoles =
await personRepository.getEffectiveCommitteeRolesOfPerson({
id: person.value.id,
id: person.id,
});
if (effectiveCommitteeRoles.isErr()) {
return effectiveCommitteeRoles;
Expand Down Expand Up @@ -128,15 +114,18 @@ export const authenticate: ContextFunction<
token = authorizationHeader.substring("Bearer ".length);
}
}
let userId: string | undefined;
let person: Person | null = null;
let authSource: AuthSource = AuthSource.None;
if (token) {
({ userId, authSource } = parseUserJwt(token));
({ person, authSource } = req.session ?? {
authSource: AuthSource.None,
person: null,
});
}

let userContext: UserContext | undefined = undefined;
if (userId) {
const userInfo = await getUserInfo(userId);
if (person) {
const userInfo = await getUserInfo(person);
if (userInfo.isErr()) {
logger.error(`Failed to get user info: ${userInfo.error.toString()}`);
} else {
Expand All @@ -163,36 +152,30 @@ export const authenticate: ContextFunction<
logger.trace(
`graphqlContextFunction Masquerading as ${parsedId.value.id}`
);
// We need to reset the dbRole to the default one in case the masquerade user is not a committee member
const masqueradeUserInfo = await getUserInfo(parsedId.value.id);
if (masqueradeUserInfo.isErr()) {
const person = await req
.getService(PersonRepository)
.findPersonByUnique({ uuid: parsedId.value.id });
if (person.isErr()) {
logger.error(
`Failed to get masquerade user info: ${masqueradeUserInfo.error.toString()}`
`Failed to get masquerade user: ${person.error.toString()}`
);
} else if (person.value.isNone()) {
logger.error(`Failed to get masquerade user: not found`);
} else {
userContext = masqueradeUserInfo.value;
const userInfo = await getUserInfo(person.value.value);
if (userInfo.isErr()) {
logger.error(
`Failed to get masquerade user info: ${userInfo.error.toString()}`
);
} else {
userContext = userInfo.value;
}
}

superAdmin = false;
}
}

if (
superAdmin &&
(!userContext?.effectiveCommitteeRoles ||
userContext.effectiveCommitteeRoles.length === 0)
) {
userContext = {
...userContext,
effectiveCommitteeRoles: [
{
identifier: CommitteeIdentifier.techCommittee,
role: CommitteeRole.Chair,
},
],
};
}

const authorizationContext: AuthorizationContext = {
authenticatedUser: userContext?.authenticatedUser ?? null,
effectiveCommitteeRoles: userContext?.effectiveCommitteeRoles ?? [],
Expand All @@ -215,6 +198,7 @@ export const authenticate: ContextFunction<
teamMemberships: authorizationContext.teamMemberships,
userId: userContext?.authenticatedUser?.id.id ?? null,
}),
session: req.session,
};
};

Expand Down
Loading

0 comments on commit f19f7ca

Please sign in to comment.