diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..97205d8 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +SQLITE_DB=bookify_db.sqlite +TYPEORM_CLI=true +APP_PORT=3000 +NODE_ENV=development +APP_DOMAIN=http://localhost:3000 + +OAUTH_CLIENT_SECRET= +OAUTH_CLIENT_ID= +OAUTH_REDIRECT_URL= +JWT_SECRET= \ No newline at end of file diff --git a/README.md b/README.md index 2644183..9701841 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,51 @@ ![image](https://github.com/user-attachments/assets/c624fbc6-5673-4c85-ae4a-74d298b73089) # Get started -1. Place the `.env.development` file or `.env` file in the root dir -2. To obtain the list of meeting spaces that are allocated for your organization, use the [Google directory API](https://developers.google.com/admin-sdk/directory/reference/rest/v1/resources.calendars/list?apix_params=%7B%22customer%22%3A%22my_customer%22%2C%22maxResults%22%3A20%7D) to obtain the list and format them according to `src/calender/interfaces/room.interface.ts`. Finally place the file as `rooms.ts` file in `src/config`. -3. Run `npm run migration:run` to create the migrations -4. Run the app using: `npm run start:dev` -5. Run the client using `npm run start:client` +1. Copy the `.env.example` file as `.env` file in the root dir and fill the required keys. Obtain the OAuth credentials by following this [guide](#hosting-yourself) +2. Run `npm run migration:run` to create the migrations +3. Run the app using: `npm run start:dev` -### List available rooms +# Use cases + +#### CASE I: Quick Client Meeting Scheduling -```bash -curl - --location --globoff '{{baseUrl}}/rooms' \ - --header 'Authorization: Bearer ' ``` +Scenario: A team member gets a sudden request to set up a meeting in a conference room. -### Book room +Action: The team member opens the tool, selects the start time and minimum number of seats required, and optionally chooses a specific floor if needed. -```bash -curl - --location --globoff '{{baseUrl}}/room' \ - --header 'Content-Type: application/json' \ - --header 'Authorization: Bearer ' \ - --data '{ - "startTime": "2024-08-31T10:30:00+06:00", - "duration": 30, - "seats": 1, - "floor": 1, - "createConference": true, - "title": "Quick meeting API", - "attendees": [] - }' +Outcome: A suitable meeting room is booked immediately, saving the hassle of running through a bunch of options from the Google Calender. ``` - -### Update room - -```bash -# todo +#### CASE II: Overrunning Meeting ``` +Scenario: A team/team member is running a meeting in a room X that exceeds the scheduled time, and they need to find another room to continue without interruptions. +Action: The team member opens the tool, and has the option to either increase the time of the current room (if no collisions exists) or quickly book another room with just a click -### Delete room +Outcome: The system quickly books a room, and the team transitions smoothly without the hassle of manually browsing for room availability. +``` -```bash +#### CASE III: Floor-Specific Room Requirement +``` +*Scenario*: A manager prefers to book rooms on a particular floor to maintain proximity to their team. -curl - --location --globoff --request DELETE '{{baseUrl}}/room' \ - --header 'Content-Type: application/json' \ - --header 'Authorization: Bearer ' \ - --data '{ - "eventId": "4r4bddp2bfkgg1tic1vh84sit8" -}' +Action: The manager uses the tool, inputs the necessary seats, and specifies the floor. +Outcome: The tool books a room on the specified floor, optimizing convenience for the manager and their team. ``` +#### CASE IV: Booking During a High-Demand Period +``` +Scenario: During a peak time, meeting rooms are in high demand. Manually searching for a room would take time. -## Todo +Action: The user enters their seat requirements and start time. The tool searches and books the best available room. -- add some sort of mutex or a buffer to prevent race conditions +Outcome: A room is secured swiftly, even in high-demand periods, preventing frustration and delays. +``` -## Github actions +# Github actions The following env secrets needs to be configured in the github repository: @@ -73,7 +57,7 @@ SQLITE_DB= TYPEORM_CLI= ``` -## Deployment +# Deployment Make sure to create the following environment secrets in the Azure App service: @@ -89,7 +73,7 @@ TYPEORM_CLI= APP_DOMAIN= ``` -### Sqlite file restore & backup +# Sqlite file restore & backup From the Azure portal, head over to SSH and copy the sqlite file from `/home/site/wwwroot/bookify.sqlite` to `/home/bookify.sqlite`. @@ -106,75 +90,14 @@ docker exec -it sh # to enter a container's bash mysql -uroot -proot # to enter mysql ``` -### Migrations - -Once you get into production you'll need to synchronize model changes into the database. Typically, it is unsafe to use `synchronize: true` for schema synchronization on production once you get data in your database. Here is where migrations come to help. - -A migration is just a single file with sql queries to update a database schema and apply new changes to an existing database. There are two methods you must fill with your migration code: **up** and **down**. up has to contain the code you need to perform the migration. down has to revert whatever up changed. down method is used to revert the last migration. +# Hosting yourself -More: - -- [NestJs Database](https://docs.nestjs.com/techniques/database) -- [TypeORM](https://typeorm.io/migration) - -### Creating new migrations - -Let's say we want to change the User.username to User.fullname. We would run: -```bash -npm run migration:create --name=UserNameChange -``` -After you run the command you can see a new file generated in the "migration" directory named `{TIMESTAMP}-UserNameChange.ts` where `{TIMESTAMP}` is the current timestamp when the migration was generated. Now you can open the file and add your migration sql queries there. - -```ts -import { MigrationInterface, QueryRunner } from "typeorm" - -export class UserNameChangeTIMESTAMP implements MigrationInterface { - async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "users" RENAME COLUMN "username" TO "fullname"`, - ) - } - - async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "users" RENAME COLUMN "fullname" TO "username"`, - ) // reverts things made in "up" method - } -} -``` - -### Running migrations - -Once you have a migration to run on production, you can run them using a CLI command: - -```ts -npm run migration:run -``` - - **Note**: The `migration:run` and `migration:revert` commands only work on .js files. Thus the typescript files need to be compiled before running the commands. Alternatively, you can use `ts-node` in conjunction with `typeorm` to run .ts migration files. This has already been done in the `package.json` - -This command will execute all pending migrations and run them in a sequence ordered by their timestamps. This means all sql queries written in the up methods of your created migrations will be executed. That's all! Now you have your database schema up-to-date. - -If for some reason you want to revert the changes, you can run: - -```ts -npm run migration:revert -``` -This command will execute down in the latest executed migration. If you need to revert multiple migrations you must call this command multiple times. - - - -### Syncing code changes - -TypeORM is able to automatically generate migration files with schema changes you made in your **code**. Let's say you have a Post entity with a title column, and you have changed the name title to name. You can run following command: - -```ts -npm run migration:generate -``` -You don't need to write the queries on your own. The rule of thumb for generating migrations is that you generate them after **each** change you made to your models. +1. Create a Google cloud project or follow this [guide](https://developers.google.com/calendar/api/quickstart/js#set_up_your_environment) +1. Enable the [Admin SDK API](https://console.cloud.google.com/apis/api/admin.googleapis.com/overview) +2. Enable the [Calender API](https://console.cloud.google.com/flows/enableapi?apiid=calendar-json.googleapis.com) -## Reference +# Reference - [Google Free busy API](https://developers.google.com/calendar/api/v3/reference/freebusy/query?apix_params=%7B%22resource%22%3A%7B%22timeMin%22%3A%222024-08-27T00%3A00%3A00%2B02%3A00%22%2C%22timeMax%22%3A%222024-09-27T23%3A59%3A59%2B02%3A00%22%2C%22items%22%3A%5B%7B%22id%22%3A%22Ada%20Bit%2010%40resource.calendar.google.com%22%7D%2C%7B%22id%22%3A%22c_1888flqi3ecr4gb0k9armpk8k9ics%40resource.calendar.google.com%22%7D%2C%7B%22id%22%3A%22RESOURCE_ID_3%40resource.calendar.google.com%22%7D%5D%7D%7D ) diff --git a/client/script.js b/client/script.js index 23bad5d..38cbe31 100644 --- a/client/script.js +++ b/client/script.js @@ -2,12 +2,18 @@ let duration = 15; let seatCount = 1; -let floor = 1; +let floor = 1; // Make floors a dropdown with user's own organization's floors which are strings in the format F1, FF2 etc let currentEvent = {}; const CLIENT_ID = '1043931677993-j15eelb1golb8544ehi2meeru35q3fo4.apps.googleusercontent.com'; const REDIRECT_URI = window.location.origin; const BACKEND_ENDPOINT = REDIRECT_URI; +const SCOPES = [ + 'https://www.googleapis.com/auth/admin.directory.resource.calendar.readonly', + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +]; async function openPage(pageName, elmnt) { var i, tabcontent, tablinks; @@ -64,7 +70,7 @@ function populateTimeOptions() { } } -function populateRoomOptions(availableRooms, roomId) { +function populateRoomOptions(availableRooms, roomEmail) { const roomOptionsSelect = document.getElementById('roomOptions'); roomOptionsSelect.innerHTML = ''; @@ -72,9 +78,9 @@ function populateRoomOptions(availableRooms, roomId) { const option = document.createElement('option'); option.value = room.name + ' | S: ' + room.seats; option.text = room.name + ' | S: ' + room.seats; - option.id = room.id; + option.id = room.email; - if (room.id === roomId) { + if (room.email === roomEmail) { option.selected = true; } @@ -163,7 +169,8 @@ async function makeRequest(path, method, body, params) { function login() { console.log('login clicked'); - const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile&access_type=offline`; + const scopes = SCOPES.join(" ").trim(); + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=${scopes}&access_type=offline`; window.location.href = authUrl; } @@ -197,7 +204,7 @@ async function bookRoom() { startTime: formattedStartTime, duration: parseInt(duration), seats: parseInt(seats), - floor: parseInt(floor), + floor: `F${floor}`, timeZone: getTimeZoneString(), }); @@ -207,8 +214,8 @@ async function bookRoom() { } currentEvent.eventId = res.eventId; - currentEvent.roomId = res.roomId; - populateRoomOptions(res.availableRooms || [], res.roomId); + currentEvent.roomId = res.email; + populateRoomOptions(res.availableRooms || [], res.email); createRoomAlert(res.room, convertToLocaleTime(res.start), convertToLocaleTime(res.end), res.summary, 'info'); bookBtn.disabled = false; diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 0b5f636..9e0621c 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,5 +1,5 @@ import { OAuth2Client } from 'google-auth-library'; -import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; import { AuthService } from './auth.service'; import { LoginResponse } from './dto'; import { AuthGuard } from './auth.guard'; @@ -19,4 +19,10 @@ export class AuthController { async logout(@_OAuth2Client() client: OAuth2Client): Promise { return await this.authService.logout(client); } + + @UseGuards(AuthGuard) + @Post('/resource/tesst') + async createResources(@_OAuth2Client() client: OAuth2Client): Promise { + return await this.authService.createCalenderResources(client, "cefalo.com"); + } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 577abd7..e942c24 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,13 +1,13 @@ import { Logger, Module } from '@nestjs/common'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; -import { Auth, User } from './entities'; +import { Auth, ConferenceRoom, User } from './entities'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtService } from '@nestjs/jwt'; import { AuthGuard } from './auth.guard'; @Module({ - imports: [TypeOrmModule.forFeature([User, Auth])], + imports: [TypeOrmModule.forFeature([User, Auth, ConferenceRoom])], controllers: [AuthController], providers: [AuthService, JwtService, AuthGuard, Logger], exports: [AuthService, AuthGuard], diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index d80398e..1b06663 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,5 +1,14 @@ -import { ConflictException, ForbiddenException, Inject, Injectable, InternalServerErrorException, Logger, NotImplementedException } from '@nestjs/common'; -import { Auth, User } from './entities'; +import { + BadRequestException, + ConflictException, + ForbiddenException, + Inject, + Injectable, + InternalServerErrorException, + Logger, + NotImplementedException, +} from '@nestjs/common'; +import { Auth, ConferenceRoom, User } from './entities'; import { google } from 'googleapis'; import appConfig from '../config/env/app.config'; import { ConfigType } from '@nestjs/config'; @@ -16,6 +25,8 @@ export class AuthService { private authRepository: Repository, @InjectRepository(User) private usersRepository: Repository, + @InjectRepository(ConferenceRoom) + private conferenceRoomsRepository: Repository, @Inject(appConfig.KEY) private config: ConfigType, private jwtService: JwtService, private logger: Logger, @@ -53,18 +64,23 @@ export class AuthService { }; } + const domain = data.email.split('@')[1]; + if (!(await this.isCalenderResourceExist(domain))) { + await this.createCalenderResources(oauth2Client, domain); + } + const auth = await this.authRepository.save(authPayload); const user = await this.usersRepository.save({ id: data.id, name: data.name, email: data.email, authId: auth.id, + domain, }); const jwt = await this.createJwt(user.id, user.name, authPayload.expiryDate); return { accessToken: jwt }; } catch (error) { - console.log(error.message); this.logger.error(error.message); if (error.message.includes('refreshToken')) { @@ -126,4 +142,54 @@ export class AuthService { throw new ConflictException('Failed to refresh token'); } } + + async getCalenderResources(domain: string) { + const resources = await this.conferenceRoomsRepository.find({ + where: { + domain, + }, + order: { + seats: 'ASC', + }, + }); + + return resources; + } + + async isCalenderResourceExist(domain: string) { + return await this.conferenceRoomsRepository.exists({ where: { domain } }); + } + + async createCalenderResources(oauth2Client: OAuth2Client, domain: string) { + try { + const service = google.admin({ version: 'directory_v1', auth: oauth2Client }); + // https://developers.google.com/admin-sdk/directory/reference/rest/v1/resources.calendars/list] + const options = { customer: 'my_customer' }; + const res = await service.resources.calendars.list(options); + + if (res.status !== 200) { + throw new BadRequestException("Couldn't obtain directory resources"); + } + + const rooms: ConferenceRoom[] = []; + const { items: resources } = res.data; + for (const resource of resources) { + rooms.push({ + id: resource.resourceId, + email: resource.resourceEmail, + description: resource.userVisibleDescription, + domain: domain, + floor: resource.floorName, // in the format of F3 or F1, whatever the organization assigns + name: resource.resourceName, + seats: resource.capacity, + }); + } + + await this.conferenceRoomsRepository.save(rooms); + this.logger.log(`Conference rooms created successfully, Count: ${rooms.length}`); + } catch (err) { + this.logger.error(`Couldn't obtain directory resources`); + throw new InternalServerErrorException("Couldn't obtain directory resources"); + } + } } diff --git a/src/auth/decorators/oauth.decorator.ts b/src/auth/decorators/oauth.decorator.ts index a5a477b..6411afb 100644 --- a/src/auth/decorators/oauth.decorator.ts +++ b/src/auth/decorators/oauth.decorator.ts @@ -1,5 +1,8 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +/** + * can only be used if the endpoint has the AuthGuard applied to it + */ export const _OAuth2Client = createParamDecorator((data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.oauth2Client; diff --git a/src/auth/decorators/user.decorator.ts b/src/auth/decorators/user.decorator.ts index ac8f4cc..a0ee968 100644 --- a/src/auth/decorators/user.decorator.ts +++ b/src/auth/decorators/user.decorator.ts @@ -1,6 +1,7 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { User } from '../entities'; -export const _User = createParamDecorator((data: unknown, ctx: ExecutionContext) => { +export const _User = createParamDecorator((data: keyof User, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); - return request.user; + return data ? request.user?.[data] : request.user; }); diff --git a/src/auth/entities/ConferenceRoom.entity.ts b/src/auth/entities/ConferenceRoom.entity.ts new file mode 100644 index 0000000..c3c7102 --- /dev/null +++ b/src/auth/entities/ConferenceRoom.entity.ts @@ -0,0 +1,31 @@ +import { Column, CreateDateColumn, Entity, PrimaryColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; + +@Entity({ name: 'ConferenceRooms' }) +export class ConferenceRoom { + @PrimaryColumn() + id?: string; + + @Column() + domain?: string; + + @Column() + name?: string; + + @Column({ type: 'text', unique: true }) + email?: string; + + @Column() + seats?: number; + + @Column({ type: 'text', nullable: true }) + description?: string; + + @Column() + floor?: string; + + @CreateDateColumn() + createdAt?: Date; + + @UpdateDateColumn() + updatedAt?: Date; +} diff --git a/src/auth/entities/User.entity.ts b/src/auth/entities/User.entity.ts index 8fdc676..6f3ff74 100644 --- a/src/auth/entities/User.entity.ts +++ b/src/auth/entities/User.entity.ts @@ -19,6 +19,9 @@ export class User { @Column({ nullable: false }) name?: string; + @Column() + domain?: string; + @CreateDateColumn() createdAt?: Date; diff --git a/src/auth/entities/index.ts b/src/auth/entities/index.ts index 0ce3805..96178ab 100644 --- a/src/auth/entities/index.ts +++ b/src/auth/entities/index.ts @@ -1,2 +1,3 @@ export * from './Auth.entity'; export * from './User.entity'; +export * from './ConferenceRoom.entity'; diff --git a/src/calender/calender.controller.ts b/src/calender/calender.controller.ts index bde75ab..37788d4 100644 --- a/src/calender/calender.controller.ts +++ b/src/calender/calender.controller.ts @@ -1,8 +1,8 @@ import { OAuth2Client } from 'google-auth-library'; -import { Body, Controller, Delete, Get, NotImplementedException, Param, Patch, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Post, Put, Query, UseGuards } from '@nestjs/common'; import { CalenderService } from './calender.service'; import { AuthGuard } from '../auth/auth.guard'; -import { _OAuth2Client } from '../auth/decorators'; +import { _OAuth2Client, _User } from '../auth/decorators'; import { EventResponse, RoomResponse } from './dto'; import { DeleteResponse } from './dto/delete.response'; @@ -25,13 +25,14 @@ export class CalenderController { @Post('/room') async bookRoom( @_OAuth2Client() client: OAuth2Client, + @_User('domain') domain: string, @Body('startTime') startTime: string, // A combined date-time value (formatted according to RFC3339A). Time zone offset is required @Body('duration') durationInMins: number, @Body('seats') seats: number, @Body('timeZone') timeZone: string, @Body('createConference') createConference?: boolean, @Body('title') title?: string, - @Body('floor') floor?: number, + @Body('floor') floor?: string, @Body('attendees') attendees?: string[], ): Promise { // end time @@ -39,14 +40,19 @@ export class CalenderController { startDate.setMinutes(startDate.getMinutes() + durationInMins); const endTime = startDate.toISOString(); - const event = await this.calenderService.createEvent(client, startTime, endTime, seats, timeZone, createConference, title, floor, attendees); + const event = await this.calenderService.createEvent(client, domain, startTime, endTime, seats, timeZone, createConference, title, floor, attendees); return event; } @UseGuards(AuthGuard) @Put('/room') - async updateRoom(@_OAuth2Client() client: OAuth2Client, @Body('eventId') eventId: string, @Body('roomId') roomId: string): Promise { - return await this.calenderService.updateEvent(client, eventId, roomId); + async updateRoom( + @_OAuth2Client() client: OAuth2Client, + @_User('domain') domain: string, + @Body('eventId') eventId: string, + @Body('roomId') roomId: string, + ): Promise { + return await this.calenderService.updateEvent(client, domain, eventId, roomId); } @UseGuards(AuthGuard) diff --git a/src/calender/calender.service.ts b/src/calender/calender.service.ts index 4267c87..f931808 100644 --- a/src/calender/calender.service.ts +++ b/src/calender/calender.service.ts @@ -1,22 +1,13 @@ import { OAuth2Client } from 'google-auth-library'; -import { - ConflictException, - ForbiddenException, - Inject, - Injectable, - InternalServerErrorException, - NotFoundException, - NotImplementedException, -} from '@nestjs/common'; +import { ConflictException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ConfigType } from '@nestjs/config'; import { google } from 'googleapis'; import appConfig from '../config/env/app.config'; import { EventResponse, RoomResponse } from './dto'; import { isRoomAvailable, parseLocation } from './util/calender.util'; -import { Room } from './interfaces/room.interface'; import { AuthService } from '../auth/auth.service'; -import { rooms } from '../config/rooms'; import { DeleteResponse } from './dto/delete.response'; +import { ConferenceRoom } from '../auth/entities'; @Injectable() export class CalenderService { @@ -27,16 +18,17 @@ export class CalenderService { async createEvent( client: OAuth2Client, + domain: string, startTime: string, endTime: string, seats: number, timeZone: string, createConference?: boolean, eventTitle?: string, - floor?: number, + floor?: string, attendees?: string[], ): Promise { - const rooms: Room[] = await this.getAvailableRooms(client, startTime, endTime, seats, timeZone, floor); + const rooms: ConferenceRoom[] = await this.getAvailableRooms(client, domain, startTime, endTime, seats, timeZone, floor); if (!rooms?.length) { throw new ConflictException('No room available within specified time range'); } @@ -74,7 +66,7 @@ export class CalenderService { end: { dateTime: endTime, }, - attendees: [...attendeeList, { email: pickedRoom.id }], + attendees: [...attendeeList, { email: pickedRoom.email }], colorId: '3', ...conference, }; @@ -107,13 +99,23 @@ export class CalenderService { } as EventResponse; } - async getAvailableRooms(client: OAuth2Client, start: string, end: string, minSeats: number, timeZone: string, floor?: number): Promise { + async getAvailableRooms( + client: OAuth2Client, + domain: string, + start: string, + end: string, + minSeats: number, + timeZone: string, + floor?: string, + ): Promise { try { const calendar = google.calendar({ version: 'v3', auth: client }); - const filteredRoomIds = []; + const filteredRoomEmails = []; + const rooms = await this.authService.getCalenderResources(domain); + for (const room of rooms) { if (room.seats >= minSeats && room.floor === floor) { - filteredRoomIds.push(room.id); + filteredRoomEmails.push(room.email); } } @@ -122,22 +124,22 @@ export class CalenderService { timeMin: start, timeMax: end, timeZone, - items: filteredRoomIds.map((id) => { + items: filteredRoomEmails.map((email) => { return { - id, + id: email, }; }), }, }); const calenders = roomsFreeBusy.data.calendars; - const availableRooms: Room[] = []; - let room: Room = null; + const availableRooms: ConferenceRoom[] = []; + let room: ConferenceRoom = null; - for (const roomId of Object.keys(calenders)) { - const isAvailable = isRoomAvailable(calenders[roomId].busy, new Date(start), new Date(end)); + for (const roomEmail of Object.keys(calenders)) { + const isAvailable = isRoomAvailable(calenders[roomEmail].busy, new Date(start), new Date(end)); if (isAvailable) { - room = rooms.find((room) => room.id === roomId); + room = rooms.find((room) => room.email === roomEmail); availableRooms.push(room); } } @@ -181,14 +183,15 @@ export class CalenderService { return events; } - async updateEvent(client: OAuth2Client, eventId: string, roomId: string): Promise { + async updateEvent(client: OAuth2Client, domain: string, eventId: string, roomEmail: string): Promise { const calendar = google.calendar({ version: 'v3', auth: client }); const { data } = await calendar.events.get({ eventId: eventId, calendarId: 'primary', }); - const room = rooms.find((room) => room.id === roomId); + const rooms: ConferenceRoom[] = await this.authService.getCalenderResources(domain); + const room = rooms.find((room) => room.email === roomEmail); if (!room) { throw new NotFoundException('Room not found.'); } @@ -201,7 +204,7 @@ export class CalenderService { requestBody: { ...data, location: room.name, - attendees: [...filteredAttendees, { email: roomId }], + attendees: [...filteredAttendees, { email: roomEmail }], }, }); diff --git a/src/calender/dto/event.response.ts b/src/calender/dto/event.response.ts index 0ca9090..1e71eb5 100644 --- a/src/calender/dto/event.response.ts +++ b/src/calender/dto/event.response.ts @@ -1,4 +1,4 @@ -import { Room } from '../interfaces/room.interface'; +import { ConferenceRoom } from '../../auth/entities'; export interface EventResponse { summary?: string; @@ -7,5 +7,5 @@ export interface EventResponse { end?: string; meet?: string; roomId?: string; - availableRooms?: Room[]; + availableRooms?: ConferenceRoom[]; } diff --git a/src/calender/interfaces/room.interface.ts b/src/calender/interfaces/room.interface.ts deleted file mode 100644 index c3b910c..0000000 --- a/src/calender/interfaces/room.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface Room { - name?: string; - seats?: number; - description?: string; - floor?: number; - id?: string; -} diff --git a/src/calender/util/calender.util.ts b/src/calender/util/calender.util.ts index 3f980ba..d0e828d 100644 --- a/src/calender/util/calender.util.ts +++ b/src/calender/util/calender.util.ts @@ -1,5 +1,4 @@ import { BusyTimes } from '../interfaces/freebusy.interface'; -import { Room } from '../interfaces/room.interface'; export function isRoomAvailable(busyTimes: BusyTimes[], startTime: Date, endTime: Date) { for (const timeSlot of busyTimes) { diff --git a/src/migrations/1725427720694-migration.ts b/src/migrations/1725427720694-migration.ts deleted file mode 100644 index 59e6775..0000000 --- a/src/migrations/1725427720694-migration.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Migration1725427720694 implements MigrationInterface { - name = 'Migration1725427720694' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "Auth" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "accessToken" text NOT NULL, "refreshToken" text NOT NULL, "scope" varchar NOT NULL, "idToken" text NOT NULL, "tokenType" varchar NOT NULL, "expiryDate" bigint NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); - await queryRunner.query(`CREATE TABLE "Users" ("id" varchar PRIMARY KEY NOT NULL, "authId" integer NOT NULL, "email" varchar NOT NULL, "name" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_8832cd2dbd98b85067c5f2420b" UNIQUE ("authId"))`); - await queryRunner.query(`CREATE TABLE "temporary_Users" ("id" varchar PRIMARY KEY NOT NULL, "authId" integer NOT NULL, "email" varchar NOT NULL, "name" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_8832cd2dbd98b85067c5f2420b" UNIQUE ("authId"), CONSTRAINT "FK_8832cd2dbd98b85067c5f2420ba" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); - await queryRunner.query(`INSERT INTO "temporary_Users"("id", "authId", "email", "name", "createdAt", "updatedAt") SELECT "id", "authId", "email", "name", "createdAt", "updatedAt" FROM "Users"`); - await queryRunner.query(`DROP TABLE "Users"`); - await queryRunner.query(`ALTER TABLE "temporary_Users" RENAME TO "Users"`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "Users" RENAME TO "temporary_Users"`); - await queryRunner.query(`CREATE TABLE "Users" ("id" varchar PRIMARY KEY NOT NULL, "authId" integer NOT NULL, "email" varchar NOT NULL, "name" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_8832cd2dbd98b85067c5f2420b" UNIQUE ("authId"))`); - await queryRunner.query(`INSERT INTO "Users"("id", "authId", "email", "name", "createdAt", "updatedAt") SELECT "id", "authId", "email", "name", "createdAt", "updatedAt" FROM "temporary_Users"`); - await queryRunner.query(`DROP TABLE "temporary_Users"`); - await queryRunner.query(`DROP TABLE "Users"`); - await queryRunner.query(`DROP TABLE "Auth"`); - } - -} diff --git a/src/migrations/1725513912000-migration.ts b/src/migrations/1725513912000-migration.ts deleted file mode 100644 index 0e74b6f..0000000 --- a/src/migrations/1725513912000-migration.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Migration1725513912000 implements MigrationInterface { - name = 'Migration1725513912000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "temporary_Auth" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "accessToken" text NOT NULL, "refreshToken" text NOT NULL, "scope" varchar NOT NULL, "idToken" text NOT NULL, "tokenType" varchar NOT NULL, "expiryDate" bigint NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); - await queryRunner.query(`INSERT INTO "temporary_Auth"("id", "accessToken", "refreshToken", "scope", "idToken", "tokenType", "expiryDate", "createdAt", "updatedAt") SELECT "id", "accessToken", "refreshToken", "scope", "idToken", "tokenType", "expiryDate", "createdAt", "updatedAt" FROM "Auth"`); - await queryRunner.query(`DROP TABLE "Auth"`); - await queryRunner.query(`ALTER TABLE "temporary_Auth" RENAME TO "Auth"`); - await queryRunner.query(`CREATE TABLE "temporary_Auth" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "accessToken" text NOT NULL, "refreshToken" text, "scope" varchar NOT NULL, "idToken" text NOT NULL, "tokenType" varchar NOT NULL, "expiryDate" bigint NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); - await queryRunner.query(`INSERT INTO "temporary_Auth"("id", "accessToken", "refreshToken", "scope", "idToken", "tokenType", "expiryDate", "createdAt", "updatedAt") SELECT "id", "accessToken", "refreshToken", "scope", "idToken", "tokenType", "expiryDate", "createdAt", "updatedAt" FROM "Auth"`); - await queryRunner.query(`DROP TABLE "Auth"`); - await queryRunner.query(`ALTER TABLE "temporary_Auth" RENAME TO "Auth"`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "Auth" RENAME TO "temporary_Auth"`); - await queryRunner.query(`CREATE TABLE "Auth" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "accessToken" text NOT NULL, "refreshToken" text NOT NULL, "scope" varchar NOT NULL, "idToken" text NOT NULL, "tokenType" varchar NOT NULL, "expiryDate" bigint NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); - await queryRunner.query(`INSERT INTO "Auth"("id", "accessToken", "refreshToken", "scope", "idToken", "tokenType", "expiryDate", "createdAt", "updatedAt") SELECT "id", "accessToken", "refreshToken", "scope", "idToken", "tokenType", "expiryDate", "createdAt", "updatedAt" FROM "temporary_Auth"`); - await queryRunner.query(`DROP TABLE "temporary_Auth"`); - await queryRunner.query(`ALTER TABLE "Auth" RENAME TO "temporary_Auth"`); - await queryRunner.query(`CREATE TABLE "Auth" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "accessToken" text NOT NULL, "refreshToken" text NOT NULL, "scope" varchar NOT NULL, "idToken" text NOT NULL, "tokenType" varchar NOT NULL, "expiryDate" bigint NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); - await queryRunner.query(`INSERT INTO "Auth"("id", "accessToken", "refreshToken", "scope", "idToken", "tokenType", "expiryDate", "createdAt", "updatedAt") SELECT "id", "accessToken", "refreshToken", "scope", "idToken", "tokenType", "expiryDate", "createdAt", "updatedAt" FROM "temporary_Auth"`); - await queryRunner.query(`DROP TABLE "temporary_Auth"`); - } - -} diff --git a/src/migrations/1726041108134-migration.ts b/src/migrations/1726041108134-migration.ts new file mode 100644 index 0000000..115e529 --- /dev/null +++ b/src/migrations/1726041108134-migration.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migration1726041108134 implements MigrationInterface { + name = 'Migration1726041108134' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "Auth" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "accessToken" text NOT NULL, "refreshToken" text, "scope" varchar NOT NULL, "idToken" text NOT NULL, "tokenType" varchar NOT NULL, "expiryDate" bigint NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); + await queryRunner.query(`CREATE TABLE "Users" ("id" varchar PRIMARY KEY NOT NULL, "authId" integer NOT NULL, "email" varchar NOT NULL, "name" varchar NOT NULL, "domain" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_8832cd2dbd98b85067c5f2420b" UNIQUE ("authId"))`); + await queryRunner.query(`CREATE TABLE "ConferenceRooms" ("id" varchar PRIMARY KEY NOT NULL, "domain" varchar NOT NULL, "name" varchar NOT NULL, "email" text NOT NULL, "seats" integer NOT NULL, "description" text, "floor" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_1fa5374f9e97d0826093b72d5da" UNIQUE ("email"))`); + await queryRunner.query(`CREATE TABLE "temporary_Users" ("id" varchar PRIMARY KEY NOT NULL, "authId" integer NOT NULL, "email" varchar NOT NULL, "name" varchar NOT NULL, "domain" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_8832cd2dbd98b85067c5f2420b" UNIQUE ("authId"), CONSTRAINT "FK_8832cd2dbd98b85067c5f2420ba" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_Users"("id", "authId", "email", "name", "domain", "createdAt", "updatedAt") SELECT "id", "authId", "email", "name", "domain", "createdAt", "updatedAt" FROM "Users"`); + await queryRunner.query(`DROP TABLE "Users"`); + await queryRunner.query(`ALTER TABLE "temporary_Users" RENAME TO "Users"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "Users" RENAME TO "temporary_Users"`); + await queryRunner.query(`CREATE TABLE "Users" ("id" varchar PRIMARY KEY NOT NULL, "authId" integer NOT NULL, "email" varchar NOT NULL, "name" varchar NOT NULL, "domain" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_8832cd2dbd98b85067c5f2420b" UNIQUE ("authId"))`); + await queryRunner.query(`INSERT INTO "Users"("id", "authId", "email", "name", "domain", "createdAt", "updatedAt") SELECT "id", "authId", "email", "name", "domain", "createdAt", "updatedAt" FROM "temporary_Users"`); + await queryRunner.query(`DROP TABLE "temporary_Users"`); + await queryRunner.query(`DROP TABLE "ConferenceRooms"`); + await queryRunner.query(`DROP TABLE "Users"`); + await queryRunner.query(`DROP TABLE "Auth"`); + } + +}