Skip to content

Commit

Permalink
feat: show popular sign in reasons at signin
Browse files Browse the repository at this point in the history
  • Loading branch information
Gobot1234 committed May 8, 2024
1 parent f7e4fbc commit ce8cc3b
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 177 deletions.
243 changes: 124 additions & 119 deletions apps/anvil/src/sign-in/sign-in.controller.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,127 @@
import {CheckAbilities} from "@/auth/authorization/decorators/check-abilities-decorator";
import {IsAdmin,IsRep} from "@/auth/authorization/decorators/check-roles-decorator";
import {CaslAbilityGuard} from "@/auth/authorization/guards/casl-ability.guard";
import {TrainingService} from "@/training/training.service";
import {UsersService} from "@/users/users.service";
import {User} from "@/shared/decorators/user.decorator";
import {sign_in as sign_in_} from "@ignis/types";
import type {List, Location, LocationStatus} from "@ignis/types/sign_in";
import type {User as User_} from "@ignis/types/users";
import {Body, Controller, Get, Param, ParseIntPipe, Patch, Post, UseGuards, UseInterceptors} from "@nestjs/common";
import {Logger} from "@nestjs/common";
import {AuthGuard} from "@nestjs/passport";
import {FinaliseSignInDto, UpdateSignInDto} from "./dto/sigs-in-dto";
import {SignInService} from "./sign-in.service";
import {ldapLibraryToUcardNumber} from "@/shared/functions/utils";
import {IdempotencyCache} from "@/shared/decorators/idempotency.decorator";
import {IdempotencyCacheInterceptor} from "@/shared/interceptors/idempotency-cache.interceptor";
import { CheckAbilities } from "@/auth/authorization/decorators/check-abilities-decorator";
import { IsAdmin, IsRep } from "@/auth/authorization/decorators/check-roles-decorator";
import { CaslAbilityGuard } from "@/auth/authorization/guards/casl-ability.guard";
import { TrainingService } from "@/training/training.service";
import { UsersService } from "@/users/users.service";
import { User } from "@/shared/decorators/user.decorator";
import { sign_in as sign_in_ } from "@ignis/types";
import type { List, Location, LocationStatus } from "@ignis/types/sign_in";
import type { User as User_ } from "@ignis/types/users";
import { Body, Controller, Get, Param, ParseIntPipe, Patch, Post, UseGuards, UseInterceptors } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { FinaliseSignInDto, UpdateSignInDto } from "./dto/sigs-in-dto";
import { SignInService } from "./sign-in.service";
import { ldapLibraryToUcardNumber } from "@/shared/functions/utils";
import { IdempotencyCache } from "@/shared/decorators/idempotency.decorator";
import { IdempotencyCacheInterceptor } from "@/shared/interceptors/idempotency-cache.interceptor";

@Controller("location/:location")
@UseInterceptors(IdempotencyCacheInterceptor)
@UseGuards(AuthGuard("jwt"), CaslAbilityGuard)
export class SignInController {
constructor(
private readonly trainingService: TrainingService,
private readonly signInService: SignInService,
private readonly userService: UsersService,
private readonly logger: Logger,
) {
}

@Get()
@IsRep()
async getList(@Param("location") location: Location): Promise<List> {
return this.signInService.getList(location);
}
constructor(
private readonly trainingService: TrainingService,
private readonly signInService: SignInService,
private readonly userService: UsersService,
private readonly logger: Logger,
) {}

@Get("sign-in/:ucard_number")
@IsRep()
async signInOptions(
@Param("location") location: Location,
@Param("ucard_number") ucard_number: string,
): Promise<sign_in_.User> {
this.logger.log(
`Retrieving sign-in options for UCard number: ${ucard_number} at location: ${location}`,
SignInController.name,
);
const user = await this.signInService.getUserForSignIn(location, ucard_number);

if (user?.is_rep) {
return {
// reasons,
training: await this.signInService.getTrainings(user.id, location),
...user,
...{infractions: []},
};
}
@Get()
@IsRep()
async getList(@Param("location") location: Location): Promise<List> {
return this.signInService.getList(location);
}

const extras = await this.signInService.preSignInChecks(location, user.ucard_number);
@Get("sign-in/:ucard_number")
@IsRep()
async signInOptions(
@Param("location") location: Location,
@Param("ucard_number") ucard_number: string,
): Promise<sign_in_.User> {
this.logger.log(
`Retrieving sign-in options for UCard number: ${ucard_number} at location: ${location}`,
SignInController.name,
);
const user = await this.signInService.getUserForSignIn(location, ucard_number);

// const [trainings, reasons] = await Promise.all([
// this.trainingService.getUserxxTrainingForLocation(user.username, location),
// this.signInService.getSignInReasons(),
// ]);
return {
// reasons,
training: await this.signInService.getTrainings(user.id, location),
...user,
...extras,
};
if (user?.is_rep) {
return {
// reasons,
training: await this.signInService.getTrainings(user.id, location),
...user,
...{ infractions: [] },
};
}

@Post("sign-in/:ucard_number")
@IsRep()
@IdempotencyCache(60)
async signIn(
@Param("location") location: Location,
@Param("ucard_number") ucard_number: string,
@Body() finaliseSignInDto: FinaliseSignInDto,
) {
this.logger.log(`Signing in UCard number: ${ucard_number} at location: ${location}`, SignInController.name);
const ucard_number_ = ldapLibraryToUcardNumber(ucard_number);
if (await this.signInService.isRep(ucard_number_)) {
return await this.signInService.repSignIn(location, ucard_number_, finaliseSignInDto.reason_id);
}
const extras = await this.signInService.preSignInChecks(location, user.ucard_number);

return await this.signInService.signIn(
location,
ucard_number_,
finaliseSignInDto.tools,
finaliseSignInDto.reason_id,
);
}
// const [trainings, reasons] = await Promise.all([
// this.trainingService.getUserxxTrainingForLocation(user.username, location),
// this.signInService.getSignInReasons(),
// ]);
return {
// reasons,
training: await this.signInService.getTrainings(user.id, location),
...user,
...extras,
};
}

@Patch("sign-in/:ucard_number")
@IsRep()
async updateVisitPurpose(
@Param("location") location: Location,
@Param("ucard_number", ParseIntPipe) ucard_number: number,
@Body() updateSignInDto: UpdateSignInDto,
) {
this.logger.log(
`Updating visit purpose for UCard number: ${ucard_number} at location: ${location}`,
SignInController.name,
);
return await this.signInService.updateVisitPurpose(
location,
ucard_number,
updateSignInDto.tools,
updateSignInDto.reason_id,
);
@Post("sign-in/:ucard_number")
@IsRep()
@IdempotencyCache(60)
async signIn(
@Param("location") location: Location,
@Param("ucard_number") ucard_number: string,
@Body() finaliseSignInDto: FinaliseSignInDto,
) {
this.logger.log(`Signing in UCard number: ${ucard_number} at location: ${location}`, SignInController.name);
const ucard_number_ = ldapLibraryToUcardNumber(ucard_number);
if (await this.signInService.isRep(ucard_number_)) {
return await this.signInService.repSignIn(location, ucard_number_, finaliseSignInDto.reason_id);
}

@Post("sign-out/:ucard_number")
@IsRep()
@IdempotencyCache(60)
async signOut(@Param("location") location: Location, @Param("ucard_number") ucard_number: string) {
this.logger.log(`Signing out UCard number: ${ucard_number} at location: ${location}`, SignInController.name);
return await this.signInService.signOut(location, ldapLibraryToUcardNumber(ucard_number));
}
return await this.signInService.signIn(
location,
ucard_number_,
finaliseSignInDto.tools,
finaliseSignInDto.reason_id,
);
}

@Get("status")
async getLocationStatus(@Param("location") location: Location): Promise<LocationStatus> {
this.logger.log(`Retrieving sign-in status for location: ${location}`, SignInController.name);
return await this.signInService.getStatusForLocation(location);
}
@Patch("sign-in/:ucard_number")
@IsRep()
async updateVisitPurpose(
@Param("location") location: Location,
@Param("ucard_number", ParseIntPipe) ucard_number: number,
@Body() updateSignInDto: UpdateSignInDto,
) {
this.logger.log(
`Updating visit purpose for UCard number: ${ucard_number} at location: ${location}`,
SignInController.name,
);
return await this.signInService.updateVisitPurpose(
location,
ucard_number,
updateSignInDto.tools,
updateSignInDto.reason_id,
);
}

@Post("sign-out/:ucard_number")
@IsRep()
@IdempotencyCache(60)
async signOut(@Param("location") location: Location, @Param("ucard_number") ucard_number: string) {
this.logger.log(`Signing out UCard number: ${ucard_number} at location: ${location}`, SignInController.name);
return await this.signInService.signOut(location, ldapLibraryToUcardNumber(ucard_number));
}

@Get("status")
async getLocationStatus(@Param("location") location: Location): Promise<LocationStatus> {
this.logger.log(`Retrieving sign-in status for location: ${location}`, SignInController.name);
return await this.signInService.getStatusForLocation(location);
}

@Post("queue/add/:id")
@IdempotencyCache(60)
Expand All @@ -131,14 +130,20 @@ export class SignInController {
return await this.signInService.addToQueue(location, id);
}

@Post("queue/remove/:id")
@IsRep()
@CheckAbilities(["READ"], "ALL") // FIXME: needs an any rather than all guard
@IdempotencyCache(60)
async removeFromQueue(@Param("location") location: Location, @Param("id") user_id: string) {
this.logger.log(`Removing user with ID: ${user_id} from queue at location: ${location}`, SignInController.name);
await this.signInService.removeFromQueue(location, user_id);
}
@Post("queue/remove/:id")
@IsRep()
@CheckAbilities(["READ"], "ALL") // FIXME: needs an any rather than all guard also allows for users to remove themselves
@IdempotencyCache(60)
async removeFromQueue(@Param("location") location: Location, @Param("id") user_id: string) {
this.logger.log(`Removing user with ID: ${user_id} from queue at location: ${location}`, SignInController.name);
await this.signInService.removeFromQueue(location, user_id);
}

// FIXME: events sign in
// FIXME: events sign in

@Get("/common-reasons")
@IsRep()
async getPopularSignInReasons(@Param("location") location: Location) {
return this.signInService.getPopularSignInReasons(location);
}
}
22 changes: 13 additions & 9 deletions apps/anvil/src/sign-in/sign-in.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
import { Logger } from "@nestjs/common";
import { Cron, CronExpression } from "@nestjs/schedule";
import { CardinalityViolationError, InvalidValueError } from "edgedb";
import {ldapLibraryToUcardNumber} from "@/shared/functions/utils";
import { ldapLibraryToUcardNumber } from "@/shared/functions/utils";

export const REP_ON_SHIFT = "Rep On Shift";
export const REP_OFF_SHIFT = "Rep Off Shift";
Expand All @@ -41,15 +41,15 @@ function castLocation(location: Location) {
function formatInfraction(infraction: Infraction) {
switch (infraction.type) {
case "PERM_BAN":
return `User is permanently banned from the iForge. Reason: ${infraction.reason}`
return `User is permanently banned from the iForge. Reason: ${infraction.reason}`;
case "TEMP_BAN":
return `User is banned from the iForge for ${infraction.duration}. Reason: ${infraction.reason}`
return `User is banned from the iForge for ${infraction.duration}. Reason: ${infraction.reason}`;
case "WARNING":
return `User has an unresolved warning. Reason: ${infraction.reason}`
return `User has an unresolved warning. Reason: ${infraction.reason}`;
case "RESTRICTION":
return `User has an unresolved restriction. Reason: ${infraction.reason}`
return `User has an unresolved restriction. Reason: ${infraction.reason}`;
case "TRAINING_ISSUE":
return `User has an unresolved training issue. Reason: ${infraction.reason}`
return `User has an unresolved training issue. Reason: ${infraction.reason}`;
default:
throw new Error(`Unknown infraction type: ${infraction.type}`);
}
Expand Down Expand Up @@ -823,18 +823,22 @@ export class SignInService implements OnModuleInit {
);
}

async getPopularSignInReasons() {
async getPopularSignInReasons(location: Location) {
return await this.dbService.query(
e.select(
e.group(
e.select(e.sign_in.SignIn, (sign_in) => ({
filter: e.op(sign_in.created_at, "<", e.op(e.datetime_current(), "-", e.cal.relative_duration("3d"))),
filter: e.op(
e.op(sign_in.created_at, "<", e.op(e.datetime_current(), "-", e.cal.relative_duration("3d"))),
"and",
e.op(sign_in.location, "=", castLocation(location)),
),
})),
(sign_in) => ({
by: { reason: sign_in.reason },
}),
).elements.reason,
() => ({ limit: 5 }),
() => ({ limit: 5, id: true, name: true, category: true }),
),
);
}
Expand Down
29 changes: 29 additions & 0 deletions apps/forge/src/components/signin/actions/SignInReason.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

import { Category } from "@/components/icons/SignInReason";
import { cn } from "@/lib/utils";
import { PartialReason } from "@ignis/types/sign_in";
import { Badge } from "@ui/components/ui/badge";
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@ui/components/ui/tooltip";

export const SignInReason = ({ reason, className }: { reason: PartialReason; className?: string }) => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="text-center font-mono mt-2 flex justify-center">
<Badge
variant="default"
className={cn("max-w-48 rounded-sm shadow-lg justify-center items-center", className)}
>
{<Category category={reason.category} className="mr-1" />}
{reason.category === "UNIVERSITY_MODULE" ? reason.name.split(" ")[0] : reason.name}
</Badge>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{reason.name}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
Loading

0 comments on commit ce8cc3b

Please sign in to comment.