From 68426022ef7ef66f8100fe6c44c7e0791b57d2fa Mon Sep 17 00:00:00 2001 From: okjodom Date: Tue, 3 Dec 2024 21:22:49 +0300 Subject: [PATCH] feat: init authentication service --- .github/workflows/publish-auth.yml | 69 ++++++++++++++ apps/auth/Dockerfile | 32 +++++++ apps/auth/src/auth.controller.spec.ts | 17 ++++ apps/auth/src/auth.controller.ts | 12 +++ apps/auth/src/auth.module.ts | 12 +++ apps/auth/src/auth.service.ts | 8 ++ apps/auth/src/guards/jwt-auth.guard.ts | 3 + apps/auth/src/guards/local-auth.guard.ts | 3 + .../src/interfaces/token-payload.interface.ts | 3 + apps/auth/src/main.ts | 33 +++++++ apps/auth/src/strategies/jwt.strategy.ts | 28 ++++++ apps/auth/src/strategies/local.strategy.ts | 19 ++++ apps/auth/src/users/users.controller.spec.ts | 33 +++++++ apps/auth/src/users/users.controller.ts | 24 +++++ apps/auth/src/users/users.module.ts | 18 ++++ apps/auth/src/users/users.repository.ts | 13 +++ apps/auth/src/users/users.service.spec.ts | 35 ++++++++ apps/auth/src/users/users.service.ts | 48 ++++++++++ apps/auth/test/app.e2e-spec.ts | 24 +++++ apps/auth/test/jest-e2e.json | 9 ++ apps/auth/tsconfig.app.json | 9 ++ bun.lockb | Bin 331654 -> 341808 bytes compose.yml | 16 ++++ libs/common/src/auth/index.ts | 1 + libs/common/src/auth/jwt-auth.guard.ts | 69 ++++++++++++++ libs/common/src/database/index.ts | 1 + libs/common/src/database/user.schema.ts | 37 ++++++++ libs/common/src/decorators/auth.decorator.ts | 17 ++++ libs/common/src/decorators/index.ts | 1 + libs/common/src/dto/auth.dto.ts | 38 ++++++++ libs/common/src/dto/index.ts | 1 + libs/common/src/index.ts | 2 + libs/common/src/types/index.ts | 1 + libs/common/src/types/proto/auth.ts | 84 ++++++++++++++++++ nest-cli.json | 9 ++ package.json | 10 +++ proto/auth.proto | 42 +++++++++ 37 files changed, 781 insertions(+) create mode 100644 .github/workflows/publish-auth.yml create mode 100644 apps/auth/Dockerfile create mode 100644 apps/auth/src/auth.controller.spec.ts create mode 100644 apps/auth/src/auth.controller.ts create mode 100644 apps/auth/src/auth.module.ts create mode 100644 apps/auth/src/auth.service.ts create mode 100644 apps/auth/src/guards/jwt-auth.guard.ts create mode 100644 apps/auth/src/guards/local-auth.guard.ts create mode 100644 apps/auth/src/interfaces/token-payload.interface.ts create mode 100644 apps/auth/src/main.ts create mode 100644 apps/auth/src/strategies/jwt.strategy.ts create mode 100644 apps/auth/src/strategies/local.strategy.ts create mode 100644 apps/auth/src/users/users.controller.spec.ts create mode 100644 apps/auth/src/users/users.controller.ts create mode 100644 apps/auth/src/users/users.module.ts create mode 100644 apps/auth/src/users/users.repository.ts create mode 100644 apps/auth/src/users/users.service.spec.ts create mode 100644 apps/auth/src/users/users.service.ts create mode 100644 apps/auth/test/app.e2e-spec.ts create mode 100644 apps/auth/test/jest-e2e.json create mode 100644 apps/auth/tsconfig.app.json create mode 100644 libs/common/src/auth/index.ts create mode 100644 libs/common/src/auth/jwt-auth.guard.ts create mode 100644 libs/common/src/database/user.schema.ts create mode 100644 libs/common/src/decorators/auth.decorator.ts create mode 100644 libs/common/src/decorators/index.ts create mode 100644 libs/common/src/dto/auth.dto.ts create mode 100644 libs/common/src/types/proto/auth.ts create mode 100644 proto/auth.proto diff --git a/.github/workflows/publish-auth.yml b/.github/workflows/publish-auth.yml new file mode 100644 index 0000000..8c84947 --- /dev/null +++ b/.github/workflows/publish-auth.yml @@ -0,0 +1,69 @@ +name: publish-auth + +on: + push: + paths: + - apps/auth/** + - libs/** + - package.json + - bun.lockb + - .github/workflows/publish-auth.yml + workflow_dispatch: + +jobs: + testing: + uses: ./.github/workflows/wait-for-tests.yml + with: + test-job-name: test + + docker: + needs: testing + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + bitsacco/auth + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Login to Docker Hub + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + uses: docker/login-action@v2 + with: + username: okjodom + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Build and push auth + uses: docker/build-push-action@v4 + with: + file: apps/auth/Dockerfile + push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Checkout repository content + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + uses: actions/checkout@v4 + + # This workflow requires the repository content to be locally available to read the README + - name: Update the Docker Hub description + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + uses: peter-evans/dockerhub-description@v3 + with: + username: okjodom + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + repository: bitsacco/auth + readme-filepath: ./apps/auth/README.md diff --git a/apps/auth/Dockerfile b/apps/auth/Dockerfile new file mode 100644 index 0000000..a1a653b --- /dev/null +++ b/apps/auth/Dockerfile @@ -0,0 +1,32 @@ +FROM oven/bun:latest AS development + +WORKDIR /usr/src/app + +COPY package.json ./ +COPY bun.lockb ./ +COPY tsconfig.json tsconfig.json +COPY nest-cli.json nest-cli.json + +COPY apps/auth apps/auth +COPY libs libs +COPY proto proto + +RUN bun install +RUN bun build:auth + +FROM oven/bun:latest AS production + +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} + +WORKDIR /usr/src/app + +COPY package.json ./ +COPY bun.lockb ./ + +RUN bun install --production + +COPY --from=development /usr/src/app/dist ./dist +COPY --from=development /usr/src/app/proto ./proto + +CMD ["sh", "-c", "bun run dist/apps/auth/main.js"] diff --git a/apps/auth/src/auth.controller.spec.ts b/apps/auth/src/auth.controller.spec.ts new file mode 100644 index 0000000..35e27a6 --- /dev/null +++ b/apps/auth/src/auth.controller.spec.ts @@ -0,0 +1,17 @@ +import { TestingModule } from '@nestjs/testing'; +import { createTestingModuleWithValidation } from '@bitsacco/testing'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; + +describe('AuthController', () => { + let authController: AuthController; + + beforeEach(async () => { + const app: TestingModule = await createTestingModuleWithValidation({ + controllers: [AuthController], + providers: [AuthService], + }); + + authController = app.get(AuthController); + }); +}); diff --git a/apps/auth/src/auth.controller.ts b/apps/auth/src/auth.controller.ts new file mode 100644 index 0000000..758fe3b --- /dev/null +++ b/apps/auth/src/auth.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AuthService } from './auth.service'; + +@Controller() +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Get() + getHello(): string { + return this.authService.getHello(); + } +} diff --git a/apps/auth/src/auth.module.ts b/apps/auth/src/auth.module.ts new file mode 100644 index 0000000..c0f1210 --- /dev/null +++ b/apps/auth/src/auth.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { UsersService } from './users/users.service'; +import { UsersController } from './users/users.controller'; + +@Module({ + imports: [], + controllers: [AuthController, UsersController], + providers: [AuthService, UsersService], +}) +export class AuthModule {} diff --git a/apps/auth/src/auth.service.ts b/apps/auth/src/auth.service.ts new file mode 100644 index 0000000..53400c8 --- /dev/null +++ b/apps/auth/src/auth.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AuthService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/apps/auth/src/guards/jwt-auth.guard.ts b/apps/auth/src/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..47feae7 --- /dev/null +++ b/apps/auth/src/guards/jwt-auth.guard.ts @@ -0,0 +1,3 @@ +import { AuthGuard } from '@nestjs/passport'; + +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/apps/auth/src/guards/local-auth.guard.ts b/apps/auth/src/guards/local-auth.guard.ts new file mode 100644 index 0000000..26a1d73 --- /dev/null +++ b/apps/auth/src/guards/local-auth.guard.ts @@ -0,0 +1,3 @@ +import { AuthGuard } from '@nestjs/passport'; + +export class LocalAuthGuard extends AuthGuard('local') {} diff --git a/apps/auth/src/interfaces/token-payload.interface.ts b/apps/auth/src/interfaces/token-payload.interface.ts new file mode 100644 index 0000000..4f4fc6f --- /dev/null +++ b/apps/auth/src/interfaces/token-payload.interface.ts @@ -0,0 +1,3 @@ +export interface TokenPayload { + userId: string; +} diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts new file mode 100644 index 0000000..6a70ab1 --- /dev/null +++ b/apps/auth/src/main.ts @@ -0,0 +1,33 @@ +import { join } from 'path'; +import { Logger } from 'nestjs-pino'; +import { NestFactory } from '@nestjs/core'; +import { ConfigService } from '@nestjs/config'; +import { ReflectionService } from '@grpc/reflection'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { AuthModule } from './auth.module'; + +async function bootstrap() { + const app = await NestFactory.create(AuthModule); + + const configService = app.get(ConfigService); + + const auth_url = configService.getOrThrow('AUTH_GRPC_URL'); + const auth = app.connectMicroservice({ + transport: Transport.GRPC, + options: { + package: 'auth', + url: auth_url, + protoPath: join(__dirname, '../../../proto/auth.proto'), + onLoadPackageDefinition: (pkg, server) => { + new ReflectionService(pkg).addToServer(server); + }, + }, + }); + + // setup pino logging + app.useLogger(app.get(Logger)); + + await app.startAllMicroservices(); +} + +bootstrap(); diff --git a/apps/auth/src/strategies/jwt.strategy.ts b/apps/auth/src/strategies/jwt.strategy.ts new file mode 100644 index 0000000..8ae700b --- /dev/null +++ b/apps/auth/src/strategies/jwt.strategy.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { TokenPayload } from '../interfaces/token-payload.interface'; +import { UsersService } from '../users/users.service'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + configService: ConfigService, + private readonly usersService: UsersService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([ + (request: any) => + request?.cookies?.Authentication || + request?.Authentication || + request?.headers?.Authentication, + ]), + secretOrKey: configService.get('JWT_SECRET'), + }); + } + + async validate({ userId }: TokenPayload) { + return this.usersService.findUser({ id: userId }); + } +} diff --git a/apps/auth/src/strategies/local.strategy.ts b/apps/auth/src/strategies/local.strategy.ts new file mode 100644 index 0000000..48fe905 --- /dev/null +++ b/apps/auth/src/strategies/local.strategy.ts @@ -0,0 +1,19 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-local'; +import { UsersService } from '../users/users.service'; + +@Injectable() +export class LocalStategy extends PassportStrategy(Strategy) { + constructor(private readonly usersService: UsersService) { + super({ usernameField: 'email' }); + } + + async validate(email: string, password: string) { + try { + return await this.usersService.verifyUser(email, password); + } catch (err) { + throw new UnauthorizedException(err); + } + } +} diff --git a/apps/auth/src/users/users.controller.spec.ts b/apps/auth/src/users/users.controller.spec.ts new file mode 100644 index 0000000..a607648 --- /dev/null +++ b/apps/auth/src/users/users.controller.spec.ts @@ -0,0 +1,33 @@ +import { TestingModule } from '@nestjs/testing'; +import { createTestingModuleWithValidation } from '@bitsacco/testing'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; + +describe('UsersController', () => { + let controller: UsersController; + let service: UsersService; + + beforeEach(async () => { + const module: TestingModule = await createTestingModuleWithValidation({ + controllers: [UsersController], + providers: [ + { + provide: UsersService, + useValue: { + create: jest.fn(), + validateCreateUserRequestDto: jest.fn(), + verifyUser: jest.fn(), + findUser: jest.fn(), + }, + }, + ], + }); + + controller = module.get(UsersController); + service = module.get(UsersService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/auth/src/users/users.controller.ts b/apps/auth/src/users/users.controller.ts new file mode 100644 index 0000000..90ddb03 --- /dev/null +++ b/apps/auth/src/users/users.controller.ts @@ -0,0 +1,24 @@ +import { + CreateUserRequestDto, + CurrentUser, + UserDocument, +} from '@bitsacco/common'; +import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from '../guards/jwt-auth.guard'; +import { UsersService } from './users.service'; + +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Post() + async createUser(@Body() createUserDto: CreateUserRequestDto) { + return this.usersService.create(createUserDto); + } + + @Get() + @UseGuards(JwtAuthGuard) + async findUser(@CurrentUser() user: UserDocument) { + return user; + } +} diff --git a/apps/auth/src/users/users.module.ts b/apps/auth/src/users/users.module.ts new file mode 100644 index 0000000..f1766aa --- /dev/null +++ b/apps/auth/src/users/users.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule, UserDocument, UserSchema } from '@bitsacco/common'; +import { UsersRepository } from './users.repository'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; + +@Module({ + imports: [ + DatabaseModule, + DatabaseModule.forFeature([ + { name: UserDocument.name, schema: UserSchema }, + ]), + ], + controllers: [UsersController], + providers: [UsersService, UsersRepository], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/apps/auth/src/users/users.repository.ts b/apps/auth/src/users/users.repository.ts new file mode 100644 index 0000000..a4bf077 --- /dev/null +++ b/apps/auth/src/users/users.repository.ts @@ -0,0 +1,13 @@ +import { Model } from 'mongoose'; +import { InjectModel } from '@nestjs/mongoose'; +import { Injectable, Logger } from '@nestjs/common'; +import { AbstractRepository, UserDocument } from '@bitsacco/common'; + +@Injectable() +export class UsersRepository extends AbstractRepository { + protected readonly logger = new Logger(UsersRepository.name); + + constructor(@InjectModel(UserDocument.name) userModel: Model) { + super(userModel); + } +} diff --git a/apps/auth/src/users/users.service.spec.ts b/apps/auth/src/users/users.service.spec.ts new file mode 100644 index 0000000..7516535 --- /dev/null +++ b/apps/auth/src/users/users.service.spec.ts @@ -0,0 +1,35 @@ +import { TestingModule } from '@nestjs/testing'; +import { createTestingModuleWithValidation } from '@bitsacco/testing'; +import { UsersService } from './users.service'; +import { UsersRepository } from './users.repository'; + +describe('UsersService', () => { + let service: UsersService; + let mockUsersRepository: UsersRepository; + + beforeEach(async () => { + mockUsersRepository = { + create: jest.fn(), + find: jest.fn(), + findOne: jest.fn(), + findOneAndUpdate: jest.fn(), + findOneAndDelete: jest.fn(), + } as unknown as UsersRepository; + + const module: TestingModule = await createTestingModuleWithValidation({ + providers: [ + UsersService, + { + provide: UsersRepository, + useValue: mockUsersRepository, + }, + ], + }); + + service = module.get(UsersService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/auth/src/users/users.service.ts b/apps/auth/src/users/users.service.ts new file mode 100644 index 0000000..34e81c9 --- /dev/null +++ b/apps/auth/src/users/users.service.ts @@ -0,0 +1,48 @@ +import { + Injectable, + UnauthorizedException, + UnprocessableEntityException, +} from '@nestjs/common'; +import * as bcrypt from 'bcryptjs'; +import { UsersRepository } from './users.repository'; +import { CreateUserRequestDto, GetUserDto } from '@bitsacco/common'; + +@Injectable() +export class UsersService { + constructor(private readonly usersRepository: UsersRepository) {} + + private async validateCreateUserRequestDto( + createUserDto: CreateUserRequestDto, + ) { + try { + await this.usersRepository.findOne({ + where: [{ phone: createUserDto.phone }, { npub: createUserDto.npub }], + }); + } catch (err) { + return; + } + throw new UnprocessableEntityException('User details already exists.'); + } + + async createUser(createUserDto: CreateUserRequestDto) { + await this.validateCreateUserRequestDto(createUserDto); + return this.usersRepository.create({ + ...createUserDto, + pin: await bcrypt.hash(createUserDto.pin, 10), + phoneVerified: false, + }); + } + + async verifyUser(phone: string, pin: string) { + const user = await this.usersRepository.findOne({ phone }); + const pinIsValid = await bcrypt.compare(pin, user.pin); + if (!pinIsValid) { + throw new UnauthorizedException('Credentials are not valid.'); + } + return user; + } + + async findUser({ id }: GetUserDto) { + return this.usersRepository.findOne({ _id: id }); + } +} diff --git a/apps/auth/test/app.e2e-spec.ts b/apps/auth/test/app.e2e-spec.ts new file mode 100644 index 0000000..6eb0624 --- /dev/null +++ b/apps/auth/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AuthModule } from './../src/auth.module'; + +describe('AuthController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AuthModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/auth/test/jest-e2e.json b/apps/auth/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/apps/auth/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/auth/tsconfig.app.json b/apps/auth/tsconfig.app.json new file mode 100644 index 0000000..01d9c9a --- /dev/null +++ b/apps/auth/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/auth" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/bun.lockb b/bun.lockb index dec3f490cbae4797355b172381d6b5439dec621e..e9d59cb7a636d506cc2d93f219206071d3dc4fdf 100755 GIT binary patch delta 71847 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 GUH=a*5BC`W delta 66242 zcmeFaeVk2I|Nno^8Ar2)l7!KON~lH=X2zH^ZgZcT2vZY-VFojU88L|&Bvcan(lTn2 zN>S-vs8lLNrBW#~HI+nEDya*V`aNHJucNuHZ=dV?`FwuA$K&@;=h184>-}77ulv3B z+WR=W$G5K#Utv5KEMHD^qqJCTh5wE?xPEy&8fE^A64 z@EaM`fgixCBIVfAu(|oUS$Tzlz{LD9nFZN_z&l~LL-3qI`9z;i^eb4E^I{FZoVTc# z__&Grqg0^n z4aKVS#$ZX`rzGD-^tKWImY^icI})!(-)l3T9rS(89epYqyXn>17Z)}S1ma0H{9L~m z9>r3nlKxor#?bTZ9G=p)y$+i+HLqxN{+O(h9H;hXp6|E+8La9%svtj4=_X`O$;!$N z1a6~vr5m50BOHjkz%Qo;mfkNe`K6gZ4EwPv@CR#i@~32t$tehY*xb+H4Xg@y!P-Z# znq6gB4f2?*yvb9u0)boc8L^Vt6s(TR$tx&iq6MblE8Xl1{rF7$I)UOqNlyZ5af&@) z$3=dHUd3vJGYg9HMpN?y;?xyDkv;%<`gEeueX}8tJU8 zgt0kO3JOPCfBq%J)7TQ~S2(pGtDp#=0>)%b@`h+Q3DqOpE_FLpzo5pE)_y(*Tf5_{ zw-+PuDt;DL&3TPiQKA77N2RhUEhqws5M;J)$#fs!k*s^OcdenwdZ?Gm!4 z7Y4E;{()Hqc{yXp1_Fgelb9b25L^ZN=RqKFEx!6$$LKQ%zb1Y+@)a*ED4Zy6sE~Sc zF80hJUPM;bBq9m`T1ld(V)PWONlVltWt@7-aBvsD#lK_E!Y}XY7x3q~=8lg&r?^jd zKN%yLFm__*xK8*gx9;VBy+>hHui0dyc2CBt_)%CDH@+aDC^L6rAalyNNtsg$vVNs_ zoe|z;gX-?Z*GT71omi-=#dPMa`l3fKFaP3_+zDfHrqHksJ}9}ew_jj%J^ce;Q|{!t zN-b2uZ{Vs>PC@3BDVarq&+ygmo>;|?np!X_zld-@pSL1^#b-^QG$pH`AkgayO~r13 zk|YAEaSNusX27uXrHF`g_P&1-;$ReZ#q+%iFjP?onIbw>J7CYz_F+ zgZ%i1t7XD)V+F0i* z-=ApXM_?7-AFJapwf-$b{ccLd*F-O3pmb(!8RoZoJyxw->)YazCx`nNz6Y^tUKv*P zFHQGtR>A1ZiJ4=vMo-L{nnOmtuC`}P>!du~KsO3h`ic2@1eL!m0&hsi|6c0WDHN4Y2BwW%U*I(K3llCB}^Ya?t3TOS*QYa$)aB;N!AdlbO#w04WNPhnNiTC56Qnd#Q3c|q}M zLBHH-S)(Rpj-C)0#9l{dMlY-;#Mg9#I`R{&4sQ!r`=a%EnO;=+Q^))1S`e=hIemg} zGsj=2ehfT{&vm?H1y<>A!BS3f$Tsj&~Msqwb8 zw`0%7e*}9jb`a@R!8B}fT>>p{@Ikao_uH;L2v<*xnvyv>i&l)B=~wgttmeWotm2ug z_ylC*yh;c3vI+N z3j%>l0o$>yv1_pEfdyFg%qXlfNWtpBv#`yuztVGBk3Pi8zXYqEsE^g83}aQ%9(1`H z6um-E$;upEn2E6}&b)-x=~Xb%yFUwj zKzeocqYR z)O-D6+gLl8YN_ML7Gzz=jbg!>_xZ;~m#-PK{QO7fB;@3c&A%+*kU;ew<*i~|!tVDg zl$lqQn^`zIJ8iX};k3+&6SU54UF}v-?|Ugn^@*Mm%^&o$$(o)uI+KZ0j<0k0Ev&ln zC9LW_DYGy;VM12XdYgV)cFyQ*(&rR#B(}|3znj7j`Nus>x*8mJFIK~lpT`z5H>)_1 z%b7SjVQgLr6;VgbeAq9r3BC#(g;jq){D?o`(GFdXuZrbnPRP<(KObMmPs3^e=056I zGzVWDc@0+i4SCG3NY-=~0M>^<2U}2d#3ri4X~CKBnjG*0C%NpekNY*xV&&jsQ50+A z15f$|Uxn3#c@nD%1fTMIa?=z3ao=NA!OyUYe@B+`ONKn{CwLj48t%eZfxp>;C$9Go z%)qKA*r4T4ye=y+?iv47$iS*=`(xG8=%s!i$7@D}H~LfWCmN-pKC;2L`?19;u=gha zzzTc7aeTGpJFGHT`>bE^Jy?yMv35)rcY1|^!03E(=JO@#RgrU__bZ$=WlH`O_W$eg zmCrWjtm6A__ABu2=D<+z3f2W6fvwdExH9AsPp)={ zi(m3Ho-i?M>J%z+<1F`$GtX)B?+-<$*mI_O(9dqq%l=^_fy&S8Vxe%+R>!WS{Vm%4JA^vcz zmW1dE(;ija2X)wda1g7NVPRSh04SZJ7b*jkVH~nT`U=J9ZSx`6`J|$~v zLC)Bsz$e73;B{~L6X-Zr6Q&HS0?x9>?bzYRN3WX=-}Wb7&bU18^qG>;tHH@BUqBzw z7jxh7dnT5QG}C_D>Sq+);vB};xa1Thcn_)??DRYME4T)}(%K_%RftnfXIkK(P4}U! zPKnP6Xb8q|SC_z(hxhG)Z(u#1Ke}*APF_}4!PLSWE?dzn{^KQfV%Q6PRsjzpxWwz) z#rI!8JE>25Qz5BOQdfCzlr>0dw%TabJ52vN$~&;xVk9g6Mt?# zftBAMtF9kwAItX8$RtzS-j z9R60U7MtjCgERN}HHz-+qdWSV`~8YVca7CarwsF_WKxAdVA)rGM)R?%=nSmlCt=mn zu~=37=mEDw!wZVhK(%ZRJeKno{kM ze|%x?B(L}d_)0fAJ9Ekyo-iN(*3U0`2lxZN;=l53aYDjVvYET%rn005qRT<_ z5pV4u{EGbky+88N9l{a(Q^_d0SIo`H%bAo}Q1CTe1=RV`PhSnIj*nL0UvE{pF|jwb zCBGc;J1lxn8{I-3|Jq;I5`XruAGuk%V+&~OyZ9<-Y=NHbOv&u~PrsveYdJP&V%EjP ztAe$UMtdZGLe7}XDGTv6Fdt*TJT!8JUK0N?t>{-gubrjrgw@2zt;i2LkQAqp7Gf6fea~xhFL-xYRB0lIFY#OSUY@9o{u9 znBtapO>=6W5eQrb_r}l}gV)x}ha;Wkcvs-XxO=)JI{U2`k{52vlG)p<`7DLJ9H>`l zTU|H3d&Iez1*{v8%63f*&Tz}Sr-e41>Be0iaSjuuGGY|vG^^)()!od8l45bxjG()x zdtzvLJvXjL#CZ>>cGFVYcF|dWX<>Iya-uU8Ps2d3QQysY>Jx9coD(PGGU?G|R^<*} zE}3bGnj<~aLXXyW<9bEH`-y7r#!pRg&TbH`ni@O+@6wZpEOHz6PIKOYscUO^mG9Uv z5V#z#npd?scmwcG@x1r&bhP8HN=gh5WMS{(R{q*CmVoNWgrmmq;|;=N$|TVjjo8uP zRr4lL5uPR={gjj#+R(^d(Kix0(a2377;%=I9i4tnY1KxH-GEM7Od_igPU?|7hwLDm}-oxH95=2vk*Q zXNSblwR|}K#>7?W7#E5lQbxL%W;l;+HOJPif2gkjx>r`pixgA&8>=aZ$IF(}2EL`Z{4e{)8?jMoW| z3C!??nvsJWpP1sbA*9(Gax-b?SUe3m)uXqbY35c8k2n`y5D0YklBn>BcxiqTTJW&- z=*j$~Se*7mF-15Nnl$&lu-AiEo%HtfPjtSMklD^`r`R@SDf}Ms-}P37Q8N=r?WWbLU%<*B-G(Tw*s^DLO1T(i1X=%{;9_R z^+T&kDg zrjLv`gA@D-2IRzFn&4K981vEPcP8OdVQKQSD02}9~l z-xNYVr|-iqUMFuQe_tv5^wjoLWZe}M=pkz00V#KM-aq1=JFsouO>&0r?g^TebUez`e@>d+q z?c>&C5XUEm+NHQFawE>utYoS^L&5&S>FCDgMVv_-WY?LFiJ^Ntx-0S`p~O_Tg18wh zAp^XUSEVO9yYc+K)WnF3xGVA_&Sb`xveYBA@?ku`^J!)1+lZS!DH1w6&0R4m626wx z{z9*+3ka#IG-!OH^D3SePWqZ0e#G-9nwFh51eLOyn@NrHI=OLEBF;*nW|TJwP9>h| z=uOvf0;e$3kM(N=A-n95?L7qTRe)3Q|ZE%;i)~O(zayCA@}EPq(aZO6ZIpZpE}nXh07) zZhFLdpoc#@{H5j-Jmu^i?VQP~L$$d)Yf;L^QxzRA=jC_<)#}hsJ>3;WEIPfSmDL68 zCOi$ccaaFa*UL@6J`$?l+g)*e#K~ZPqfueyW6G5FcH?G5Lbdw1>1mPhz&>6hxpXcg zq-KR_d{QiqKT4{}dGwoRw0{~6$Lr*sj-`bBkyU?vgxA(B>zv}$?-xDo_v}cDJ?Ur~ zntpD@%t+{(E8MtQ5$7RJT5Z0_cxX~Aj+R{TQMsVp532zX>AO>(%(%lj)bZW za90#ZoOaYs^Nv}_k~ zqb5*6VyN*jw_<)IblosFZUKAPVJ9@ zKRsCb@G|gJ+v?urzR!Bhw%e0padZ}V`w*x8)&AJUdI<;Nbs)~)mfnh|@e6B#ah}Ih zmZxa>bsoDW8mFp!hNn1Y6K!pj5%n}9PdXYZ7f)x7moU5w&tIVHUK^d<%3vTKXEdpF zjKO2JtLyGaip5a|{*6&$8SVOC!!1WBs0HnP=(g zf~N}tZDbxz!n@cnm)+`Jcp5!NSHDo~IJd%$gfp{sH7y&F;yg%5(JX&Vx;@$Miho3$ zCOLk;a9;IK42{Wg)q!ec_A=SfM*_@V>{AGn6V(&Hr&+o5Q*C#rqc$#=zN|=Q>%n!|&L5WW4WWP4t zXHvJ@@igxkq3ons9OdCp$?8+0Q&JbRE_i;~O=l&B7EEy~?uvwW0{gm^w|C^eGrcSl zezbu5JT1xLdW9V9EzScA-MG6Wp??&*>6m?m?uxs)i=KLNSC)|&`emvcw=xp$aGj=B z{K^z(0ipI@MYL&q0q=5;*O}&5i4I6j4EMtuDIQ)&h;7}fjJpVGIxX1p*JBl}mD)}jU&EOv}o%9~WOYuA|PtF&3 zS}tSUO!h?WZ}8(BH?uS`H2DU%;=YKp4(Ru~)~X-zI&gqLJ{QmQr9iaRM$fCES^pZZy^a+DI@F8h0x>+zHUEnuoVj@QQXROlCY$$0Tv*}~)I z1Omg|_>L*gON6+B>zw3-YdJR%;F=|m9+`!g8a*cT)?Bw@E!&bC-MEL?tlj7jQaz;% z--pNj{q~OfitcLfkUeDA;wFC#yh5FPysjtX^5zEuEG~>BXL_j7&2GiRkx-pm+_*;~ z;nlY=u5SDzDWRBK-4%~SoL;y3ee9ntbMe$pu11_YoA9&{a6`lW_948HZso%%;ae63 z0%Mg)_%NY#FVyEYud`vd5bEZZJ(A+=CZuuq`}T~*oU$IzAf*0d0;MH}7vpvC^8J#~ zFfX@(x9jX8kMJTw3?jMhAfypF)y-sI7GC0?uKtMh!Rt$$zl||?Y+fEt3GG_qu6QC6 zI%BC@@kAti$5JlQUjE+_Dsd~HNO2~*fdI>w=3ic-Q;x@}X1xLb2n6z@Ug*Vtxam(t zoP=e803+@v2|tQ=qnq&*j~DLX%fpj=8Qv)I@KHi}Uh}dH-@mEQ&@SV~t&ccemireq zuk29ia(Bi0NVr<5chW!7QGr2T%by|CkF5PEAG4zBU|)`>`OEk*7jMMVTs*^D2{)fi z$;@;Dclsx)pGz;i?p~gXyA4mLv>&$%k4>=kQt$Fp($mb`Ie03Mg`7t>I0HOKyPWgO zqQi<8z6y`~#!e~DU4%NH%r53`f1P13#KXwd^4!W>JI1bLo|A}~myzfc;%Oc-bBdA_ zrABd?XNV5rX~Dp|KPmQ}lNScTMRtKkqMWh z`|#56ysiy@ftTXO@9G$PfAmaiO0QmtXM2@v`x3lMIfM%zXW^T8YOr^s?VPeYx?fc7 z`{8vX&R_7B;PI$BF(vdi{~+R}h~qrqXGCA|6rWvM@OjX z;G|d_4Uj)1x8q&m-8p?q$X}H;sEr<}I&ELU8^$qyV;ZgVH~XxNjNW8CbplPJp)ca8 zq28+J9L3WJG27V9rabDVZ;LpKAN6~NnodlL#Ze0XSz_JCq8l5njU(_ndUd;#kmdyW z(405%y5jlgYwgGVhEp4^PCfDb)m4LVJznRNDWAjhhh!BGJ*qw7#=RK{r#<04R2Z4! zOegeb;yO=O=_Y49UQaUepP#>sr&;Ycx#m;;XnM~+oZfgTCyShi_h)CnfyXyA{ZpJX zo{lyllWJazr?K;A+;Y4>2VxK2pZT4=zG@w3;`Qb@X7~u6E93cdh0Y5#c*aeCC*s`n z3|A{JiI$~r@#xX6DWMh{+_-lmPVNT3WPi9G#5;MmI&m8V-g9cNn?t!9-MF0*=UHIC zlg&G2Q?wbHGsEySmVPtt!uvC`TF+KpRfpmI8MhJ7ulOFOCr$@4VM)Syu4>~8@Y;H9 z*>fe^9lVS2*v@w5TOK@}4BnL>eA)BbRA-D!ab^F3_|0V#);rPJiPwk2{e8z}FZvfS-@6%4^`<(kd#~bY z)%K@o+)GuPI~-5vCiB05y*yq=Jb#tih38LSom!`C@jH|}nf62QE+HkeZWi|qc&*)x z#VO8uLbizv!XZ4>gU9haglzuu$*yI}=f3P#lt-KgflA`vS$~743b2Lcoa^(7dQQ*f zmJt$XpTZ5uPCWI!ztn|a^_xJY+4ZI1@htttQPk?jnFBALQ=J@ z-mOIUl+d!RZp9~&aQJop^@|?4gr^Z2;yrwMi_jpB@NUJN3 zPKKs`94+MqLYz!XQ=HnL_}3SIbo20Zp^f$Sa$9ViH;2QaN)}H&@D8O_x)nb}oEHJ3 zNz5tJJ~4Fer*8V;NNCij?h4FGyq_cC-Jg1MJ0&HQ^_g4oW5n6_nZNT6ddFP+xto3@ z;^cqsZ-yD}Nr|BcK6fjQL_!sxyKz79AZE|W^N7229U(xnI6aMhUaINdLUX`&EA=K zss!s7SBU5EbgD5>zb3^VjGp*Jg?r((_ZE_3LOPAS72er^*XE>m5YNBzD!Y1L`^y(y z$YOCdURzQ!?dB$iSKuXjgD~eC|FrP7uHg^yM!E5WJH~z+J$G1JLQ}qVSNtAv9s{au zp0AZBI&}|4^H*uZ@zjGf`k}<|5U{({Br8GSCC9D1eNwL;p zK+N~!-}c9K&Y`lMF#s2CS<;@tUi}k!SE9Qn@IizMVxi}@@4RoLyHVp+)C!|j&B)_w@U9kF8 zwPE}o=yWs`Dg7{Ohhz2m54QOKmcYv}-Am>DYX~p&SH23q+NP^&#jmlvs;v$i6);cM z44z@0Zyt;dhRliP!TP3Vi(qVVh{$pFAX#OSZEcR_vdSdi@=4ejRDksPE3148k?d4! zud{X>0Y(IYz_1_V$PfvZ0t?q_i>f%1Ei0v{9V?5h}Gw>tkQji zG>V@feX3f;vuE)>vhqJg4*JUbL5UymBDiLHpZ|?jhjPL5K2@y>ZbyO;`h4&E(c$=4qf$w^&xeI@Ygh zh0d^CR>8Xb(Q;Vda#_{qY^+ec<^RDJdk36LLah)j?13$L<(k$SOF%`m*Yo z!Pb|RKg{~F$~V2(2ClL8S}gws^qXk4Xq5Fw3-bA&SRFEkKbk_5ti8eJBdh$2qZOD9 zP>tr=1b<}}KaY4Nxyh!JRdBww3oMsai*L1lRV%c}=jV6}b2jy6+WRF^3R>8-t|KG7n`nXMB)i#E|16S0$LH!KP5B!%_75jkHwXl0^hW~G@ z8g{_ub3k)cJ^!@@e`OW_t&RUHtN8DTS4Drso`wwqH3h0E(pqmdvA%;<`dV5el<;(G z>tgvYP)~oXRlU_TCRjtxDhszdUXcNe4GPk$>|HSH$BzxTd z#8QFcfL?i`PgPq7zb{-Z=!aF3{x;qJ7gqTW@bY8){lH&ab@@;d*2m`A3dkxr$@*2T z*0Jks{B*1eDzf%^th#)r^=DzLY5vb4pwC}fWiZ#q%PNDLu;TMAm(?7(1uL}3@~T#7 zF@JRYQX4PZ8vkFwzxzu24|{-Y(8M>emo{UutjfO=tNib_{6ATB)crP{tlG2M`m*vL zu>N1zVy~fVNT32Awh3i*?SBd@w4Ohz*e0ybz83_o6@S_KvI@Rx?Q53HD!9%1vZ~nj z5Tyo9zqY~pIXc@b;L86qtdj1r>AsM|M^?w}#R~1STvmL)wO?6#z{bm}qrQ`G4pE%9 zTpza4vO4@n>&wRDpGNJ3>Zot5Jrmmu|8%V08K!D?!>Zc7Y`Use`S!NFiY@l~^9maw ztAYk#y)y-?D7|}3A6XSJ!uqmGpJ{zr6*LMflx4ZB(v7peto)o9|8haV2NP@pStZD| zHs9LG))rVh)!J#+7Fj#P+L_iCTRYp@xz^5$M$EThp|!VQHKP_|`7f|kfBp-rba&Wz zS@nRyY9D=%<+9?dtRJ<-(SWSR@NulVc)hjHV$~DRWA%|$Mw_kwS60WrY~!n1p{@K; zey{uS#nFIgb-j4s9w@7VKESGlyRn+PpW1j?#eZh)=a$Q=fG?~st8{y<{nB#ZGX4sv z#s{&A`<6dC;E)_^)s_FWepRb&vP~_Y@OQ2K#M-udF?QRmHx>s)dKJ{1-T^Ki2AOIf1X0vKpBv zeRXB_UszS(6ylY|saW}@XOmz}?;@bLK#H7(RZexV>hn0PKC-Gwy!B<3uCew1lU1KL zBi#kqJ~n+7>tB5P*$7z`G{E}*$*RHwZTi7By{z~UYp=3gRuvv*`EbjJhwUV|+D2Ss zBdS{UL>62bjI;5wDk$6fvhs7RFRO-3v^>}Hs#f{skFg0RSv%P#tZLPT(=3iAde@vmF^rcH31wB#VXRvCv*mwfmF}O!Yli+|)BTlI1A^32=|fog)ru_$TU#BgkF0vY zv9=~w2h_%DAw3VPkF3&NV0~E?&>XAXM0*>bY8f(2tNMM3;^m89BZm)GPrQ9l-K?w0_auLRXF`AcZ@w#0KkB#;`ujT*Z=U`AoymXt zu0#_?@!G}4=(My}!N0#Vu^saFcP684PyCft)ZgEk*be%u?@$y~^}7;W&u@(V`#Y1r zzcbNyD{M~x{?6pDzbnz{GKVhGY4rDZChCoU`3~jp?@Z{AzrQp2`#Y1rzccy!JCkHR zGT~Fz{{5ZF-`|;d5C8uD&P3mp=p(D`)ZgEk_)l&B{?5csjH=(A2>qYmm1z2i^UPQY z{{GJ7@9#|hfBDWt7y6|e$~y3BfTF@SmF0a<3Jz%hY#69D7P>3CuN3rviHB1LjQy%riR$jtR884lv)$z7DWv5@4UeLeqL0 zVAy29vT1-@%pQT*DS+41#_KMO22or?h31%TB>fF>N zn?QI5V3}Dv12BCmpwE)vj9oc z0Xqa%nuf)I0|GOP0jtb*f#pSjgc87grlZ>a{=qjL4l0|{cZ$2W=d}aWX}Se5O~7$ng?i53|Kc0 z@RT_&uuUN2Cct{L_9npe5B$X21b~nKuKrnC$|~Zv-UV0(iv~-2&)24^Szv)wH}7a7@`Jq0D9gIs1(>|S{lGH zfkg)Jl_?ikvjmX39B|MqTn-qv6mUr38>>)GW`xntun~bfJrZdv@np( zBF6)!#@&$JBDr@%_z~Z;A`6y7;#WdW1kCuAkfc(`4v{|sChi`{0g;*a(1@Vfeh-aU zz51sgsO27$$I;PhHfCl#f);$2IYmN(S6UcZFP|vJ=5HNie zpw=2deKULwpvAp_%>oULvlg&hAa^aGk=Z1$;66b7L$p6WXr3Ia9=RVf6%uT0wmd|V z0|Lz+1~fI39|kO64cH}co@x3BpyvaCd5-{^nVka11lp|wG&kkz0Baruq&^C0X_6lU z3|j*zdkk=qIVcdj7SQi;K!PcK9I#Q~guo@H*Asy3hXCuI0JJv81sXgI$aoTPnOXZJ zV4Fa#rvUBD@TUON9|3F@NHosVfEMcjxlaR<%_f1}0`cnsDJEwDj0tW;VHUK)Cq78uMj{_ez{Y>d|fQByR-_djU|k6)?dZ6o`Eh(C;-st|@&Duuw8-%h|HQ@RtdQQ(BYV$D28!1NuY*!%%0jB$1WTD%R& z-32H$n*??X#D565)8u>zSnv*Dhd`NW_z@uKUBJwb04vRQfdc{wy8)|A(Qd%73O}W4^fz%4XgJxj`V9oo0Ljr3}^2dN-9{|ce20UyI3dHUL^!o&` z&Xj%v*eGy9;4#yy5|I5NU|l8P33FVa!AF3MPXSMvwVwjE3Do)wu-**+3^08+V6(sm z<9rTiQ4Yxc9I(l364)&ezX$M~$=L%~PyyH>u-P>H0+94EVCENq7tMBo0|E(q0b5Mb zUcmBC0F?r-n3i7xdR77!eF@lV$_0)Ir0xT}ZWitXtoamhNMO53-VYe|8K7)G;7xN- zAog=Wzpns0OzBsEjRGeG-Z8xn0J8T0)*S%sG{*%Rd;!Qf2zcMDJqXw)Q0r^JE;IaV z!1TR<%>o}8=NmwaF9EsV0LslKf!zY}-vT~1Io|>n>;vo&s5A`^0h0CuW*!23X0{6) z5J>nAu*VdA2Uz|Upi*G3Y56^%=K;W??*aQvxxg`j)E@v}nT0<9)*J*J5;$m*4+DmM z4JbRzb?uv=`T8)|wb*YU{eFZT3Yrx^LN6M$N#^aNm|zzKoVOt0So z*+&8Eeh1Vs#|0W317!RGsB6~#0oVpuRx89-WZCc#6XiJWX59M53F5c-h4i^WKtr=h zV7EYg2++vnga8YE1?&)rHw|L|NxuPR#sC_d?SNnt)3h3JA#jQ5#VfZ>b_lS}0kk&91scQvGHL)WGiz%Awh7ca70}KMKNT>&8ep?PqH$^h zT7&_)H37+HlfZ6)cy(Ti$*BccP#v&CAk{Ri4M;i#Ftauw&1@GqAdqkxptC7D4X`{G zP$|&Wv^*Wq(*Z0x9njsB3mg+jtpn&`7S;i*sR1}7(90yB0T^~FpzI7lA9GM3wkDuo zT|hrmS{JZU;DkVb)9Xw?b}hiVGXVq5ae)T40U7lGSDLl;0NVs=odpjEkTMw^z606otHENTSEGUWot1X9lij57<*2CS(EI3$o`lH&ox&H|Lh116Y* z0ly!H60lL=gur6c>q0>Gd4P2n0+yKL0u9awWLyMr&Dx6q+XQM| z3|MA{UksSu46s?i7$*VH;sQW!0-)4v64)&e-wJT2$!P^x&>XNspv*M91d!AMF!K_? zO0!+yfIz~ffK{gGQo!<-fJ%Y;Ov~1Qo)-cZwFay<V7T92%BtVPSfZQa&CbLOkw?KR{;5n0%3|Pu-U1oR}!1NTrW`U24(-qL7W2m;t>l*5!hIo!LS|53$VQR% zBHxEhs2?P|8zieAD3dlB*ts*~%%o+V5(=Ufi?GHH`GB1m? z=mBXq0CGHJrVN1W7TG28Ysj2C5VD{rWZpo?iI90uB&ipq-5|&xAv0$X4b9}M0lNiu2{bZIuK_H$5-{%?K)l&0kTe+3E(6fm%+3HD z5ZEWs)U>`9uzUz$*|mW4%pQTBR{^?@05mg8MgWcp{4CJibRG#*d6D6n4O5)&K^$W8}jjRv$fPY5))8c=Tx;4+gr2Cz+F zt3W$bCkrtB8o<;nK%&_q&>{oSY%CzzOdbo^EwD=<#WWoUSa2<1-Z((2*(s1T0?;ly zbakyC_?K*T`PG+a&E$EZp^koi$SfLo4Z^vYZg_JmjZHwKlA}E%biK9nij7 zXiI3rGZRChnj6|)A9^=fV|-Wt|0y}W>xSAlgf0w*m-CB{+GbTw=n^w=W+*n?RL6S- zn0MuG>%|W(l;6>y@-x@m6*_&xf#T3lo?B}_KNR8Dqb0v#&-0U+nm2}ehHe}e|OGOriRyE!z)>lzcx z3AtwEZTN#1^BXDo7gg7{+8uA-aQAJYD`G;+du;e_N$9&!t}6( z8$w5-oqcwA*Q=DSYStI475(2XFW9PqiYDD~S6S%Bm|EM&L>=Yj?bmn%HU1>%Hfevp zcX%HE?`V~@_-ktJjeBhLf9(~N9*T;`b2$ofc~wCKEK#B z%I#8*@LO!l^xfTMmi-1(8hx3ZY}p?+oqj8&-@54sT3#w8WLgJKv%jK>0jMJSCqAcJ zR?RZ~60fdhVaxPGqxzOrw@i&_Y}qN6>DLf1t44fcVcrim^`@GeEp%)`{pM^QOqHl% znSSo|HZ9QSRLk^BtU;F5v`lY9iN3+7mStza?oeiYYQuC$UA2Fyg{Rwu>Sl)2`{;!b zN~oU%-jCG1b!|H4P~btN&zUw|eZp%z!W^^d^fPOHm#>bfZ<&7Oe#mPu{olaCIG}#) zrY>&?Q(=wJ6x)UIHr?5<0?W>^OmDI11k*4yw(K0joo%`%mNkZ5WLZ-&jeiq#nT6-t zgiT@k)ule?!PE=qqD%rB*Jd`|d4wk@lFtQ}oln@c$2PaD8EhF$L*2r%3kVkb)SY*|afnrQm8!fF&RM4D*&T!K~bBBY6?PaCY#U5xbV5kAF% z%PdU5``i}X*0NTxILq`B5FLC8`UPo{>5LZBY50@^d=g>&=e;OqJxmj?gH6|l@M|_* zipS`Gov&pUcC-oG!g^SiYFRrSjP!|E)}HXANRu(mvP8oAfvqN4C(DuuM}H#P8ODF! znh?|vWcBH06LugReLqik%k-nk$+j0Rx2z-VF6F|fhh?dR2ibA&X&Eo#3_NIuxR+&V zuz4^Ib+O)+qK0-t18u@SmUV_{hU?SUvMz+*A*}K5XIWRmr*o{vU$0nEe%(-An_qv+ zy2BnOtdCy7Lb_tT7(TGh!htqn513w;q0b=8dJ@)qX*7ARw5%6l$F_K|WxZj$$ysw^ zh-G~U>lIuY|EnzPOL(=;KaKxTpnA9;>P140|8Se|3c_zAjlW(Hqjdd|e&VZfy~d^+ zK)A1E8I}!%U18a^mJNbM--I+m?`NSry@x-biKfp;%LWtHWYH(nvLS@KS~d!+%3Ot1 z34O-cbnJD!SFMO;S*9P#K1o{`MbEz7rT1dP`SrFn%-vT!6&1uGN13P%NJBE5HL zBzB5rpWCi_q>p(!E9P8t>(H1Vigi8J=uIjTS(qvz3P^a6Sjy@a-) zm(hJlyO}j;EppL6&@yxfnueyMA~XZtfM%juh>;JJ=OO`)mG>$G?TMH7o@rL!i`A({7t(b?RKW3>(Df$R|L#JdRIX@x*A<$a<7T0 zTdemcq#{7UY^06RM3js4?>|3Bd(anXFWQOTM(?0E&_0%z{pc%n04+vK zkX{h-9MYz07I~E*y(48h8jp(640MA|LjB9*Nl5Rw97P9DAzXm2L+5dzUgLBO9Y?va zOmr0*iUy$mXbvhtdK=GCq)pl{=vVX;dHjsDNz?5_fP8`|gkp3#(XQ+iq+OSG^V*eb z7ydPgzeR_Tc3+>PIuvpSs)y>M2BfhH4bm%l%FrCxOzdb{ItJ2k@mQQ&|s8?bf>31u5P?^ zqjf%d9qm9nk@m3l&{-%BH9}{jcyu4}+Rr|Mw1qvW*Zm!)h2NnwXmuR5Z-gAw52m~A z(~yH=Q4IP9>GiG$k$z+S0J@r<>51))`k=n3ANm0P4jPAUL${(us58=gSmvvM?G&_& z+I@uj0Q;f-D1?&HsVI!rknSP08qG&HqUmTEEon_)k<%Osjx&Spt zEl^9OThfcr#V7&k*7Fi{DQX?V`0E~1x0GL?Pmx|y*Ag{DkK=2v`yKio9YH^#pV67L zxE?wS#naN+*g0q|dI*g|1JFP;2wjQ#qg{0UyC|M;W8F?PCGZB_s8_OGMK}LO=6cK8 zUi2l}hx)*JAl;?S#J?8lWv6@bKSLW(L-L73BTxpq3Jpc)q6<)SG=k%vC+$}pJC8~a zE++6HwYeXyMh~Da=pd2bqVLdtq~{!kNY5-XIp%gUSq8reRSE0Ty! zMo$oa6g`F>K|gWK59mhnDxS}u1?X1vIT`8gLcEtN&;zS?F6kA2)v>Y2rI12&7x7A? zm*UMsOORgu*8{yn9&<#s7k1_Mr1o9V+w@(jDI?TK@(1pw*}sdYc+PZfmJK zHr=7=jw}W(B!hYA9x@$F1ro6zp&Sa*9h}0FUTpXpRyR+F(O!26Et7q&t1xG~Et+8M_&+N4ja!P0}W`kGPr?(2KANdltJND=xcR z6Npa}Wyq&S$ElaKi8N{3#8hUn39AOxl!zXpJ$(f_m82hH-$UBFZ$(?sP3R@0zR+CP zN-zyO1FQL;i8S}M1ZgSK#a)ZKF7&NX0@9Ln5q6(0sVxb#K$@ePM!NcciQYuo(dxRW z>*Hdi^-Js6Eod&9iL|?&fW~g992@gWusDnG7&HovMkA4~o!1~eyd8nnbyPOfhLu)K z6&r}O8NLW*Bm2bnk8=oLh_qS09?d}cC=Y4F9EaFE*Y=*}D1o-&llZMeOylALf|Jn{ zG!+%1>rix;u6oW!b5IFlYIwVKF-d!YOHf0ky}<&ceZoy>9=g$n8)3E9*FqN~-4$?m zz=Kl)ThR+>1u8|_iJXPBE4c&d9K9Dkh}NM8kj`1%W~@f{p}Wyt=uV_vjM86#R-t>) zN_0O`9y;DXhUPqkvlcy!v>@m-+lV%xC(#o&{519%v>rW$Fb>|lJ5_%E6ie5&l)GO!>v>m;UwxQQ-crW$~vV8ePtI_A^GxRBX z3ss^W=o6&gdmrsY@1SBWEL!;ALm#7Zq>I4^Xg5;gkI;uU9A)wquCQ@YuH%)TDt8w8 zC)$sGMn9n=C^~Pk#ea+gXaes;U!e|YJkmk`KGAhxH2sCdA41=v1Lz=9{5ROI(GTc5 z^gTL^eng7bN98G>qngggt)mJkLK!~-rfl7NS%t3z;{vDk_nvlPt zzDPTx3$Q_~?hQ_{esydZRYNh_>4dCP0~?*`x@^`(&5*{dKK6X1%cd^PDoB^rd$5hL z=O9&9*Z#Y)ccC<->-nA7v$5J6XgjC1Uc25^@M7)bYNJ|62PiXTqRdr+IG9c)Rj2`q zRz|1aJxCjks)ec$rHSUFibN~=0O2OYH%1Q;J}*f2YN5I=+N~1^YY4`h)V!FBilaC> zG8d9O%1TLgF}~)Rm=24^Eve$aV8dGM(@=EbZ%tVH(oU9NhK-<(C^{MsF5&0-A}&p)qJQiYC5}Z~;=99F&bz zc(jt^32Vkp!RDfg)=tJwLis2UT}K)yWh|~&IcoDi4-GUY(4foARJ)mA$hW>$;qTA78v;?_oy+Mi;P{QR%2bCf5yU|_f zPNc3~ffTRw(R7Meocw<;D4IrG=~f{Pmz1pTT8-{U51_jbZRyom zjek3{b26K%hj29w@`>7vU5_3{nof^mA3^KTW9SL=IC>I2g`P&5RvL}x2ya3g&@*VG zwa;RoM|uvbw8sdaMp#v1Di(X^hYD3D8Vx-P8-nz3RX3NNksh|{S6y+?*f{J9ST#WD6{mvrJV)(5jIY99#;(`&QK3804@em+!{g{3qzsRsZ_zjC zYqSL&LdVEUSoZjibRL=d&1u#O-{u{`Hx;2#y>hM znT_7;*L?V=5+k+rXLM9P2W$35pWOdHwS5O%6-V&@d-om|M2ez95|?_Ci~Y%%)#&h1@5gyi@CeLjDZaqn(t zXJ=+-W@mTrfbm&kHePPS2Fb>jA0-djUjWu3J77LMvj7+Xtf5Z;3D=u>{Pno_8|5az z7r+Zfpd&pvn!v_xy|43IO<6p(sFgDTN{SCR;pX#tNfk#;i~lVn7|^ zayj-VR15{MbnKTLMpz=v9qsU}i29t*FtqavCcvtE19%N!4=IIbyj{w5GN2NmBA^1m z4ZzHDf37HbN7b2kwoBuJp&XzLfS14J0qy{1g#DiPYB{KAL%#O6r zhNEeLY54)n0Csa93FFV3y;|_#j(8K8RqY9=2H-K|V8U+nGp@NV=df^K;pe=0%t@va zU~$c~xIHt^HsOzZW{er-Q=}Tav%y?yR>>80@GQH890{3gX0kRQyQB?q&$w*>*`;lP z`(^-+fNYprlx%|Q#(+kEx`46SCD({)gjlupfK(sQ0Fa$2#?|ncP*VV_nWfe0n&TOF z#G#iN(T1lL?wJi|`r)IQqWof?$X!ArzTx(J@ z1DcQ>aLq?A?EpepjK3y@rji2Sl*d^cl1@O*u5$L0Z~#lt8^Cd(2g>ZRpeycq5@W`g zjy7qr8&-!fUGSWlzd#>jU73#!_nYKz}^z2j~mnXBu$8 z8h~d!Q)>90@pnMa{|IDV@)uZT7yBj*Q8+P7E|fa~Zs=dP0~ebB z92hq8L%=#fLBLwT8o+8m1nNHkmg0IoU=|<-FcUBXFdZ-rFclCDm;&H>W|En51H|Hb zHefE`7r@v#_=l}~9?FFP0xSS50W1P62C#IRR4fHc76)KS+yHFR@wi_GXbM;X=!!P0 zP%;C2JpnKgupajt09yguIsR|M#TEdoF9E<6e9tATZV%t1;a-$Z048z(Wo2}F2qjB* z1dswa4B&|?9p!I;RKPI+GtYSZj2YrmJcs^&#|?9J0>E+mILhYegbjzaz^Xn8NCU91 zGp5dnlE^%9@Oe%4i^LT5PHC zj>Zc-&j36DJO(@hux6g4%#V^2m1nqq3U~+L^>!x8w*YSYN`3Yx%H_a)iIVHNAFdn3 z{?AtZ1~;z(KjNAXec6pZ6p8uH`pSAb^_=u%EpcsG;nfb7pCF6tV79f%jC2JrAV1a8*V@!kV zCj)r*LDL35=li0-EdsCu*aB<-ig29&S>q-EotSaW@1*jn_6jsAgwhJYJ6BxCny|;c z6F>&A!SXp@2|#fG#|;ORTwfBv=YORDZUAoYijrx#psaGpVcmx+FL2j|z}Bca zba-v&)NNO86wn9xczF5Y|5kKhoWaqzGZ5fvQM0RcnqGcqt?EE9nLPYFe08CyL5ERc zS1Jz~H>&jCN>h`+hskpo)gF(glY!t0gw(Y3CnZ}2c>uxR!_&i4>`)^{M1@|iy{&Ck z|NB`rhiLkEgSS4Fl0iYbp`&Z#4b7c+YqbdQ=i5eo@7*<_0q{LNd_7EFx_ne`0`P5U zI2R7IVS>TYsXUa!;w~!}Ri*NzULipD_3-lW#&FP0An<1qdx6JmR+ixXs9pK(9yYux z>4Ut$pbwG~syYz}VL&Jbg#7*wGF}y~HxdXY4{w%W9CZf*zL^`vcuRq21-xfv=A;i9 z^j9e+1VMqgjy3~9KByAvK0ab{vti@l!i?YpEuPiW=ZOXf`7SUVK*FI@&Fwb_&(B5z zUmrK0WN>h@f(h9Gr>^Mic zoNgWJJv^v5kRdy3FLT$ZDzwKy2;0x z_pF9XdXumsh^63}X&R%CrBy)32dLwcpz(ngVr+MPjJR8^{4q;gJ;!tYL2uSm4GzBi zLJ;$H&qw!R>nP8~L>7r#`#- zs-d}=|K<-5$@b)!^!9_BJOm=!rH{An5o(y}9*Z_Usxz?XE!3Di%&h897E~n$LKdcO zG2q&XM#O;YO0J7)Yi8bFqncL zw6n*WPRAM?%twL2I=JTedh!|1O3xVs{{iC+Y6_!f|IEai`_3NT!8K+cd)*Cda-I%b zehLhB*A-q@el8Q_eMq!rj5F4h0SxLl-B8kL3{=Nmv|R4hvTxr>vn9Q^M|FkJR9ZG2 zYEZHBD#>+*p^xuK#B3hMf?sAY3m$q~uV`A4WrA9hn)xEK&Tq$IIEuN$VPfgP4A7r% zN6qJe-*_PMF#i7BqQm#)51E1JBNV#Xj;v+^BMBJzKWba=U5OzBDz8^M0S+XxqdLHl z?%C0e&nVy7(U#eeFm|S)Bz{%Lnwf^)dS|kkg>j33L9o(GU#%8YA*eL0B@!`=iFBl= zZbpyNCf09BgnaJG_H0F$0cG14L07(lnXScX&Ma6?6dj!fTd|}U`wWg$WHvf-q^h$K zajU|+S&)ktR~~M^(b_RfkgcG~y1rbq?Y#b%Mx8{BYBLgzLQ_eVRO4Tz=#)1_d=-jz z`Z6?j4vK2zg+kM)Q+*t)u#E(`-zoZR?MG*H$B_o23VgYyEKIb@)#@R=o_x;=WzT{9 zO=ZfQ3j=Hq3?^YtIV4}r|8YDUk+))iJyFAYy!f4amB_S!1zDy!P^N%+aP3jRU}+;C zU2fd^w_yl5UQjfM>!zTFr-tz#R&D>=IAgX^KMW4fjFqQp^WZz*5|3oV2SR(9l0e*? zH%3#H+0tMz1KYTP_Sxc~2qW=-^~?CNVW(NP`o%7E zaJ|7*y5f@9kPMPu4lXUC*uJ)&e^efBeMGSuFU(rHp{1$k0z;@YyELUOFgWyE3QSvc zbMBl=r%vk%7Pe>mQq-1+8lH6KXM{!^xnrDSuiu0xJO-yxmO`1)amKD4k9I6Z8w{{N z=8=|e0>jSnC^?+=zrg(p`|0RLfeYbg32xM9A*PJ>WoQYP-OJKxJU8zuD@K0z?6Bn4 zp^uv?9DDL4`R%YVgKQHI%8~OTw9PChdVLjGmUfT~t5MPh4E;ycuyl1-M$W5LW0wbN zctU_4EXz|E+R7!7q}u@_`JT7MqMZ*yfuYV)pdJqlNiCgXD+tWXj^19D1!p!=I8+#! zO3xQza#>n|YAy!JjX>lmDqr|?)vx_dMud9U5)W3O2w<2`0E5lksr1zkEu1^dQ!o@S zyMP*=ep16rTc0_*Yp24F*7hLU`sSP;nGKM~EH@4lC5z&2-?l=J=dD_EjR$6IUvFiS za-_0L3_j)~l||UiJTfL?$;Gs@z(Ur@$;3(2u(AeKiOF!gQREzIkmxYq7pl;VB^a!m zRVanaCspVzO8H9_;m~dO^*j~!#h8K?7&2~QS(WN9H9V3!dQj9d=%j~-z-+Pbpx<4~ z*o3THAl!o@c0$b~JZN+r40A*^x)*0K`An%MbaXxT*z7`9`Kxj$P*NUUD(JKJJ`{G_ zS*%WcrljAmKA~Al4JGLa{?R%%=iTvRm=IfYHT-&npx?W{fzu<@c$j>2ue~UJ8D{Z8 z-XzC^wFBN1?rC(8j(Jln+?!7!A>|17Y;&z&?RU22e2v4D57xb?VSjHhqVA%7$qfoC zH9R~zwXHPBhg^S!LL!09vtKFs@3FJjKRm8rD)LS7p=a?>vWEEv*2?DOux!-A>1*4V zR7{ARgXX{RmCuaT4QOO0M$cMU;J&}gF*MNIO#8u)CY#o)bjq77mxGH@W(rsine*l% zZ*71^EZWL>w}k{r+{axDlAzw#sN72rnaPZd|-`9*)~f_wSfv4WSvxJ|k$)oB(m z%ulO}F?3)IMNI8ga2np4UdO8Dy5i+4?rGxM}mFkpr1|*eYFIXDB7LsY`Fy zK|47YmOSrbkRkhBJw=p)FWA?kphSZM7Im}N8yxL(#BSp(X#0TI1uL*YgtgUZ&b zQ^x&!nUuzo(=@-e@) z+GGfl9yg{fn+&e1>hYUyo}!)P(S-6Pz$d)Os0oE9V8&|Pl)^t@<&cqJC~1y| z2G~BcElEt68(VmD*MAlLPVl4&^8 zq0I(I^B0iffVJHFLg2DhAN-VjRT-r`nqq_twMewyV(6!r(pykXOt|vJ7J{DvM^;4E zzHtfZLL?70){;UE4(8jy;C%gf?JH>qqZjW`yFqx!dsY?Gxg)jM4vjr)L7v+XU`Jr~ zSMr_U*Rv;fF5XHdgemYHy5O3AovU>^aMoBhYq4Mmq)2XS6DTkikFH{I|L{9y#p{LT zK5PLsJPl85Ss=XTgoT~5mJ@KYG_;jRActT*4G(zHD=BOi(m$T3p$uKc!1>#eO!nN zfZ_kU)*K{NXmZ_&6h<4Uya@_rhnhG1Ad-nuZ!T0HyCI?jGb`lWq1HUyaR0~fZ42S? zURbKbopq1B$Z8knf~7xF`CSH6NTVh~n?Fpwkl@^*sU^n-pyB8L;)yvonr!vE+fc?X zL@Q53VIDP8=it>f#t+|`wXRVEo>ZzWetD|0o3tU(Fz#z7%rgwSN|9~p^KOHK$Nw9< zRDTZ^P6gXh=pKWqL>_JN%Xtl@e7~*K=7Mh{l2%iL^m`57_TM(W-i{jYHJA!&vzOS} zlp-qZSeF#ci>LQVz(+yA}mt$*wLyi|}9?v+Thv?B#uVHR4`kplL^s<(B- z4nHtA4)od)meC^)nCiyBLcE5^89^uglB*5hEsH=6?|Z<;nslaEw3RekU@Hmtxlcn% zh69XG<A|{%0l^WmqIsCi@dSYo%jyE>f7ZkH7%-G|W*BPN;~)9=7*c7@s{&bV z$9AC`XzM!@7`DJ@z9{CqI>B$pWMM2*YYO!bUzsvFT5L!w6KiLxe9+)62`L<;ur4(6 zAVPihU`jn`h%zthE|gb#zx~6O4{FDQq`xvix^-$z-)8-bRhu(4O6?44-QJxd4?&b< zU@$uk?$kQEf63jw3WgHE&!{z#GqRrqGi%9@@k22`mJ znV$sW#X`gy-?5gZWX?Z=!-1#RDK?>oS1yyfqy~>`Zu!I>+pio}eLZW0P$1fVvyIOi zv^aV|C=q+S(&7+W0z_HrA&e@xNsmHqQ`=raCu~%Rzm7fVbQ1J*A(WhxVL}f=DYLk- z7_~@74wV^7VR)!7Pm_`j-sal9#lS79Znpfq{>o>CiXZp)18O+d1O!R@&%G#cQ>5H$|jCFBd|E()+yF z@!|)cp_R}8yI)vA>ykl=XZ||zVaFZchyAILVoJKxz3Cv6y4IU)kAT*b-gNq*p_ql2 zm+sHr6fYTZ3Cr$7bq=G4Jk~lUKb=b-q4};CuMAvLUq1ppaH5AntJsHT97g))1B_ze z!TOI+uXcQxw?*L&YXDDOlRk6})a4(5!CbEC^?Y{y%O5)e16eDpJR5iJN5E}jUkU=Z z=89;+Q}npA$K-qlk7T8V5+hRjibSf*^}b<~y}fy}0xKVu7z5nuH$!nsIbx6{sR!L+ zvBj7cFXf~Ash>zJ&c)Y$^8M{kUC@wIWVmSrRXvKt;d4J~d=xy`_NUQDVFuIslV@RL zF&B_t)L(RZIlBDkfX>kc(UuQnP?OM~KBJMg7e8bG#Xmx~NNq!pfmYpcTEu0Ca7sT0 zO}v3U7J*#a*%Xozp5jfOIyIqVJClC{g#iPp<~5W;DhD}zpwRN|&nFWq`V_`KCSr&( z(!i*5{U1>s%wqsCj%*V?Vi7bMH5@hj*P0tW=_v5BO)(4h+9zI^WX+tPH70fIc zLeZ%ZTpCLIasSUR*c!OtlucxGbF$T24Hay{D>Avo$e+mi1TYu=gneJoUeRgZU-7H% zB07OXWs$T0Cki+Y%s+mjPrn26Aux*qQB&FWZl*AZ$_omYLhp!z!EAPQXS*Dw>whgio=M)NGngp{(P%!hAH_eSBXclCY z(nnBa8ZhsV5V6Q_<->|=5UcWi|`WKp-q?JaCk@)cQgqL^A#o@5Qozp!0{Lq`ypbJSW;f4>= z3?}nqwBV56Hu%|>kF&bJ#n|z2Dts<@j4JPa?Zj1+nN(OHFN z)^7KRH+Oj5iomz7J2|HtOs(?HbXK4(FJg9laK17tDsH#Eeh!}4;Yk8Y4uzJsX3uJt z6Ru$iZo)AZ!q9<8icg0gQX^?EsAHn3?TpDsI%RN{t?{k~J8p--8L?Gg%{j{4R##4I z(JJ^AS~-j*k9m&FsdpospxtM=?WSa)!*r+wS1N!RZ!a=e{ViI&Fwj z`7GT3CiV@NIqw9Xte_r96@19biTg3C*GNvMImo{ z$eT~KH(GMe5GZY%OwZ88KN?*TNv_P2W_X(Z_D!a6e*W8Ja=n966H*i5++?zLfV%EZ zrcB0DMUY=l7Bk`n|9S%_HdRtiRd|aj6mS7{P-2Qmm)

`NHd78R71J?1aACYXYZGEZWK!fKd(@zf>68y=_L|dj-Rvm(+hvp(LhWJX&DP9q?0n ziG_nkDj1xMdDlQq1+=wv@7TreWYs;QhF`-7i6-ldXd4X-S72ncYCq$VUD%HTgWm~R zi<;7?>CkCHyWRSoY~3(V&UlVTQx~+AKLP{(6Xns`@A%Prfyc8j+^5nkrrv6*=>K%D zH}P|xy#ADhF&s6O(bnngf{>bP=Zbz6>dU9n4Q`tXjB3ExGvIZZx(oKy7o0Kg?@)td zuc!kt3!O)w-^<%^NF=!iBWrsD`BU9Xh7ijiV}uoBSWjOu*nh{n%fae1n|cke+SGdP zuQWAr%>dN!o=2lQVGZ1q6NRdjno%+I=@P_^0S4Q6$@c@EuWRPUGcg9r*Q2@)2H|-b z2C*=P;;$O)^D({kG1TQAp6!pJNYu$^(3%IjMm|?C;pEL?i|Ha(1zpbc2#hZ|HLGpk>9mA1 zbyasp1Vlw)4pK}EJ-LD`QoWaRR~{=sRS|mz6k(r_p9y6+NGg5#)C@79Jz5)MbKye1 z2ymdjbbtga<0@2?JAF>n)ge?AngOgXGKnE$8`=A?N*Z&GKi~YiK08mWg`D>}>1Ds4e;Ye@TA=~ zVzN(KLS^q_KRf4yo!2y@&Y9YbC|_SHQh~zd4%MuGrdlshX2-x3pvDk60~owKow>yI z`ll%0S-`+K1xE%foMs`)nDfpB+s27>X6felZz%X`lG0Ymd%DNb4Q@LG7`#Pgc&X5YN{jUr@(g@vUaDrO(wGet{sQ>T#?w8<)5Ol9UPvGH zJXbryiu@g)R@OEsa9+o>HOgf~VoI2hKwVyXEap_a zTH`8jK_lZ}%hqg8b0u{-KpS6WbCeurq%N@Jyp2Q~;J>y|*{n$7rj&l&!Sn4K$PYGD(|?v=xpP?M`ha z=RYyvH^Cg+tIgec<=s5n^EAr4{!q>f)YzdWOn;`?x)qNjv)I;cqdM>Mw&ed7SaWtQ zxN0G+-8Qjfx8i(i#n;>ZsK(5I7fh7fwo$}e2&&&MVo~E~J63dgvZjutXGQsAhbH&< z@GrdNP^~JI+;)c;i>Fquj)lgTYpkdjTj}jP*nQPW3>yWhrFYG%rsIV)mUJObYYIso2onO8&wnoL;tRdJkG6|T3#|I2( z%UvQwt>`!GrxBk!mr~mDtA@I+sIf%b30r!da4h`vrYlC|s)^ki$IXyp2xtD$Q12gC<9t&YSn`N#A$r9?K~v^fx++ z!~TQWVz&sJ1uhjVvEJ>0RTjp7dSng*{TH@0U&y&Lo+G44P501+Pk2rJ>KvrGQJUl#@-=9=fAu`h=4GVK+jce(%f(~+@OH?pW`qQ~!@7m3}M9OkFx>zX$SBm0+_r-`v{F9l-r z>t9AMhfzuDZVsjEjU{tt9N(-!*6b4#_L6PWV!f;^?8M8y96GV02$PH^Nt|Ih$Zq?E z*N(jEVw={ZD(Byrb@>ICo%`vWWNiMA;V|@SKBlKVL!K~$Tg!(;>(&)K%IsX!*)dBtbwljy zjS6MM4l8k63(!@im%vGV3*$d4vj5(DmbE`O;yj?(%)jb(EsZYo;J1uG6pc+T+ zsnd*n#x~zrh-UsOMQWNKW;8R2Lh~D=u;Q_^GCHEr7eLX01{62`+jlKEvujlVHgr3M zCKfP8$rTO@@5xts{>Z7O*OQoku|!f|5jA@PgX4R0-#aVpZo1b5hBwZ;yzsR&YIxS@ z*XH{9kgnzV<0k&{02ZNs>p_zX8plh6j*_J%x~w#s0xXRV`qaesmd0gzrxQm-*GD>f zzO#(oHB)Jhc^B`@D>6xEk5U!95+Pp)B6Hw6tm&4)_Dy*u>WPk`4wAa{D;Ec*EoA5w`FQx?jLC+0QQxt6V2wqFmD8mN*Z%PxxFeQJb zxE3q6vFbR?RmXS1Y2;>WTqK=5Ne69>Ce;_TFN|czbdeuyDBWTC$4OuKZ5%ZT#co3# zJCM#hD?^q&)Xr#zXl{uTiq3X^rLMqnel@zNvALYf(KzoP!vs2< zr+c6wWt=Amdz48RsJT7#))~XZ$$|E*D|+S60ZktM_#iI|7+MORbdhH9$;YjW)T$U7 zzP~7T;MbM9o_c;;0~~DP?Hk4C}hi`Fb}4^T0rY|Tqb^)J&O z#T0w6C~ow2YH?W@P-brk6)oyRd+t>@gW`>!15H(1zk z5iiQa4Y=k|mBm+Tv~_5zjtxv2kz{{yXh)=0CQVq4CpiKw9ntc3^wptT15faaPB=~F z)sUv1%2z2C!pn`XiUoK6z)`~^DrMCDMm^GrqjaSN`cmUczr1BzjWOb0wNHm_^}#B2 z?GIY*X%jVSv+dgVwt7w9xdsDecr;EmeZRj*M;wgqCaq=O1DjJOvMpvTE9cF3%WD+u z2wil&CT!r)t)M$C2G$9I*7!{=Sfx5MX*Q-c*op=`Ky%NB8dk;piI)_V5DVuSf1N&o zq|?mnVgZD|*n4o=^`GG^T7ofiBjkFM&5%C}EFBz&u*A&C7tvVLzUj_kuz zCz3&XIQNyc=mMd!7Uz7j9fs;nhbdv&|j1ImHfZ+;^ z%8h2sm7AGG_{Hl@-rjjr6}-s5p(#7z+a}+wt$VUYY!>l9ZqjKGcj|ddU@UqObvSPD zzU^$XunPPL5~>D$^QPo>TL`djdy(DAr`+(hmL`Cp;B*KGymhs%Q|qaA_csj6iGV-K zS-OF6DDErY;u`3|Lfz#3lP5kCBB!`Y9mzuf+bSLjM$)P1=_R?ZN% z&|NVK1)GiD^JbOb7WN$;9sXbuZxcAm=|JGu1ENl+e0k+#gY;6F4&cByo6=BcgvzVH zumXm0cD;4enhoKtCU$<%czBmmoG~Y%Gg}w1lkdL3oWHBtiUNK|FePJhQ+5ng#;J>o z(I=PfT;5mUF~H8)Mkdq-gm@6omTS`c7TtyZI4xidbal%GxoEamv8*15M4+?NZ@Yf> z{mlE>YH(VF_qJT2F7=f!+URO*C51ku&-_r`ymWya5f4Ro(#XQQV%%(@Mh-pG^g?~ex|YlrNZqEJ!f`u0_NWfUN>@@=no&XJeSJfSvapobSvXg@4M?hr1<`GR%z z@FK;UHDOB-8vF2sGRt6)N<5{2vPP4W_Gk9o7bVPEIWlY%zdxj;Z#oB>Ru=B={#3kk z88u|zi2G3^{}!YmDoo~3S);eq=qbG^Yjn5l3PetUOd(XU93~bA>Ris~$S+F{D`!lX zA3hT-6`$MDYRHtKX^@ta84${RM!|T$R&IhBlPC7JXP=cFIncHvFx2EOmqGdfa%>Zo zXD-%la+`+_7eGtdjnZDj&|cd$KBwXpFa-GOuPK+&&ndD3y36&u6NMqIWws4%B#{r$ z*!<*$;4!IWxqSP&1>aJnfX0xsfg9%DMY%~Nab>zjmeVd zXyYs4!Ex*7JS|@A4_>UWkAp+ISL9p?e5~sr{POQG{gw!~`d5I#sSGf7pe=Lw{^O`V zDIMk_RbPrHoHRofVQ4E~#y#6?&yI&v;%wqAB>fRQDTXI_uR5No|Al*wAYPU)pSm}+ z$6@h9JmD>jTold+ZIB-d&p=ytLZz2n-riO%LBm+nQ4IgHo}JoWubEUIGE^6K`|&kJ zxPz8BTC}G)ccZ^td4;7iqbp><2`a#(%TxCzsS^ zoPMe)H{NC)S{aDtDY-JN{`8;pwlb_qtUHyL!kfRPpepEDE#)Uz8nKD`H$9PIT3qsH zqlhEedO$8drwSTt`%BjE=oVUn_J=CQaT0Q+(N&@Bsqcic>)UKuGj#gt7|tMt!f-IP zr78zwVA!FHp0T^y^}QCC0e013{CG5d1`U4eo?mY_+rukPI!RJ5_ut72j5*Rqxoc6bE?Kek0_mZz~!8~#F2 zD7nmqnB-qqjvgM;H(dD(ua&o6xmy0Be%_y`XAf9@ai{IpvvQfjZa!mC1P1FBu{dwq+s^J5B_R>x4U=FoQy%DrK zSkRfbtJId_Q?`^ljC+nBZ9?00v7ERgB@Oqba6fkOcNZU}v{?OE#UHU}SH;$8nU~(+ z-Uju7b=#*8PN_cA&|S5*g4LHV-_T&JhczBB!j|j9i+*3<>3AvJv+IVr%_<(5+<$px z+!w(;O-Wjny45Sa7VdfPY5gA=ahtliC;fMDtNM4I7TV~0xsLkB0W+*= zq{$dcFHFX&iS@mW9rd)<$5@_j_!v(n9`!YLb4;Am#yHP5v0E==?b3yQ8rnB(NYw$o z2KVlj7&qD2)<|w`jI|Tr#29TP>NDMVfTDejh3V~Zqg~>;8OG-Lbsw@t^~7?H7TyNB TBwKh=d^wB!iGigo?l}Grj(=DJ diff --git a/compose.yml b/compose.yml index 069c3bd..7b83c11 100644 --- a/compose.yml +++ b/compose.yml @@ -15,6 +15,22 @@ services: - '4000:4000' volumes: - .:/usr/src/app + auth: + container_name: auth + build: + context: . + dockerfile: ./apps/auth/Dockerfile + target: development + command: bun dev auth + restart: always + depends_on: + - mongodb + env_file: + - ./apps/auth/.env + ports: + - '4010:4010' + volumes: + - .:/usr/src/app swap: container_name: swap build: diff --git a/libs/common/src/auth/index.ts b/libs/common/src/auth/index.ts new file mode 100644 index 0000000..3525e2d --- /dev/null +++ b/libs/common/src/auth/index.ts @@ -0,0 +1 @@ +export * from './jwt-auth.guard'; diff --git a/libs/common/src/auth/jwt-auth.guard.ts b/libs/common/src/auth/jwt-auth.guard.ts new file mode 100644 index 0000000..c130308 --- /dev/null +++ b/libs/common/src/auth/jwt-auth.guard.ts @@ -0,0 +1,69 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, + Logger, + OnModuleInit, + UnauthorizedException, +} from '@nestjs/common'; +import { type ClientGrpc } from '@nestjs/microservices'; +import { Reflector } from '@nestjs/core'; +import { catchError, map, Observable, of, tap } from 'rxjs'; +import { AUTH_SERVICE_NAME, AuthServiceClient } from '../types'; + +@Injectable() +export class JwtAuthGuard implements CanActivate, OnModuleInit { + private readonly logger = new Logger(JwtAuthGuard.name); + private authService: AuthServiceClient; + + constructor( + @Inject(AUTH_SERVICE_NAME) private readonly client: ClientGrpc, + private readonly reflector: Reflector, + ) {} + + onModuleInit() { + this.authService = + this.client.getService(AUTH_SERVICE_NAME); + } + + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const jwt = + context.switchToHttp().getRequest().cookies?.Authentication || + context.switchToHttp().getRequest().headers?.authentication; + + if (!jwt) { + return false; + } + + const roles = this.reflector.get('roles', context.getHandler()); + + return this.authService + .authenticate({ + Authentication: jwt, + }) + .pipe( + tap((res) => { + if (roles) { + for (const role of roles) { + if (!res.roles?.includes(role)) { + this.logger.error('The user does not have valid roles.'); + throw new UnauthorizedException(); + } + } + } + context.switchToHttp().getRequest().user = { + ...res, + _id: res.id, + }; + }), + map(() => true), + catchError((err) => { + this.logger.error(err); + return of(false); + }), + ); + } +} diff --git a/libs/common/src/database/index.ts b/libs/common/src/database/index.ts index aa69c4f..8cb6842 100644 --- a/libs/common/src/database/index.ts +++ b/libs/common/src/database/index.ts @@ -1,3 +1,4 @@ export * from './database.module'; export * from './abstract.repository'; export * from './abstract.schema'; +export * from './user.schema'; diff --git a/libs/common/src/database/user.schema.ts b/libs/common/src/database/user.schema.ts new file mode 100644 index 0000000..6cd87e9 --- /dev/null +++ b/libs/common/src/database/user.schema.ts @@ -0,0 +1,37 @@ +import { AbstractDocument } from '@bitsacco/common'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; + +@Schema({ versionKey: false }) +export class UserDocument extends AbstractDocument { + @Prop({ type: String, required: true }) + pin: string; + + @Prop({ type: String, required: false, unique: true }) + phone?: string; + + @Prop({ type: Boolean, required: true }) + phoneVerified: boolean; + + @Prop({ type: String, required: false, unique: true }) + npub?: string; + + @Prop({ type: String, required: false }) + name?: string; + + @Prop({ type: String, required: false }) + avatarUrl?: string; + + @Prop() + roles?: string[]; +} + +export const UserSchema = SchemaFactory.createForClass(UserDocument); + +// Ensure uniqueness only when phone is not null +UserSchema.index( + { + phone: 1, + npub: 1, + }, + { unique: true, sparse: true }, +); diff --git a/libs/common/src/decorators/auth.decorator.ts b/libs/common/src/decorators/auth.decorator.ts new file mode 100644 index 0000000..1cc1977 --- /dev/null +++ b/libs/common/src/decorators/auth.decorator.ts @@ -0,0 +1,17 @@ +import { + createParamDecorator, + ExecutionContext, + SetMetadata, +} from '@nestjs/common'; +import { UserDocument } from '../database'; + +const getCurrentUserByContext = (context: ExecutionContext): UserDocument => { + return context.switchToHttp().getRequest().user; +}; + +export const CurrentUser = createParamDecorator( + (_data: unknown, context: ExecutionContext) => + getCurrentUserByContext(context), +); + +export const Roles = (...roles: string[]) => SetMetadata('roles', roles); diff --git a/libs/common/src/decorators/index.ts b/libs/common/src/decorators/index.ts new file mode 100644 index 0000000..9795c6d --- /dev/null +++ b/libs/common/src/decorators/index.ts @@ -0,0 +1 @@ +export * from './auth.decorator'; diff --git a/libs/common/src/dto/auth.dto.ts b/libs/common/src/dto/auth.dto.ts new file mode 100644 index 0000000..7cdda22 --- /dev/null +++ b/libs/common/src/dto/auth.dto.ts @@ -0,0 +1,38 @@ +import { + IsArray, + IsNotEmpty, + IsOptional, + IsPhoneNumber, + IsString, + Validate, +} from 'class-validator'; +import { CreateUserRequest, IsStringifiedNumberConstraint } from '../types'; +import { Type } from 'class-transformer'; + +export class CreateUserRequestDto implements CreateUserRequest { + @IsPhoneNumber() + @IsOptional() + phone: string; + + @IsNotEmpty() + @IsString() + @Validate(IsStringifiedNumberConstraint) + @Type(() => String) + pin: string; + + @IsString() + @IsOptional() + npub: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + roles: string[]; +} + +export class GetUserDto { + @IsString() + @IsNotEmpty() + id: string; +} diff --git a/libs/common/src/dto/index.ts b/libs/common/src/dto/index.ts index 662a7fb..69e7e38 100644 --- a/libs/common/src/dto/index.ts +++ b/libs/common/src/dto/index.ts @@ -1,3 +1,4 @@ +export * from './auth.dto'; export * from './lib.dto'; export * from './swap.dto'; export * from './nostr.dto'; diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts index bfea254..7528335 100644 --- a/libs/common/src/index.ts +++ b/libs/common/src/index.ts @@ -1,7 +1,9 @@ +export * from './auth'; export * from './types'; export * from './logger'; export * from './utils'; export * from './constants'; +export * from './decorators'; export * from './dto'; export * from './database'; export * from './fedimint'; diff --git a/libs/common/src/types/index.ts b/libs/common/src/types/index.ts index 051ff10..21928f6 100644 --- a/libs/common/src/types/index.ts +++ b/libs/common/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './proto/auth'; export * from './proto/lib'; export * from './proto/lightning'; export * from './proto/swap'; diff --git a/libs/common/src/types/proto/auth.ts b/libs/common/src/types/proto/auth.ts new file mode 100644 index 0000000..805f77d --- /dev/null +++ b/libs/common/src/types/proto/auth.ts @@ -0,0 +1,84 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.2.7 +// protoc v3.21.12 +// source: auth.proto + +/* eslint-disable */ +import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; +import { Observable } from 'rxjs'; + +export interface CreateUserRequest { + phone: string; + pin: string; + npub: string; + roles: string[]; +} + +export interface AuthUserRequest { + Authentication: string; +} + +export interface User { + id: string; + pin: string; + /** Users phone number identifier */ + phone?: string | undefined; + /** Users phone number has been verified via SMS */ + phoneVerified: boolean; + /** Users nostr npub identifier */ + npub?: string | undefined; + /** Users name or nym */ + name?: string | undefined; + /** Users avatar url */ + avatarUrl?: string | undefined; + /** Users roles */ + roles: string[]; +} + +export interface AuthServiceClient { + createUser(request: CreateUserRequest): Observable; + + authenticate(request: AuthUserRequest): Observable; +} + +export interface AuthServiceController { + createUser( + request: CreateUserRequest, + ): Promise | Observable | User; + + authenticate( + request: AuthUserRequest, + ): Promise | Observable | User; +} + +export function AuthServiceControllerMethods() { + return function (constructor: Function) { + const grpcMethods: string[] = ['createUser', 'authenticate']; + for (const method of grpcMethods) { + const descriptor: any = Reflect.getOwnPropertyDescriptor( + constructor.prototype, + method, + ); + GrpcMethod('AuthService', method)( + constructor.prototype[method], + method, + descriptor, + ); + } + const grpcStreamMethods: string[] = []; + for (const method of grpcStreamMethods) { + const descriptor: any = Reflect.getOwnPropertyDescriptor( + constructor.prototype, + method, + ); + GrpcStreamMethod('AuthService', method)( + constructor.prototype[method], + method, + descriptor, + ); + } + }; +} + +export const AUTH_SERVICE_NAME = 'AuthService'; diff --git a/nest-cli.json b/nest-cli.json index 31b23f4..939542e 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -73,6 +73,15 @@ "compilerOptions": { "tsConfigPath": "apps/solowallet/tsconfig.app.json" } + }, + "auth": { + "type": "application", + "root": "apps/auth", + "entryFile": "main", + "sourceRoot": "apps/auth/src", + "compilerOptions": { + "tsConfigPath": "apps/auth/tsconfig.app.json" + } } }, "monorepo": true, diff --git a/package.json b/package.json index ceeea37..a0a0255 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dev": "nest start --watch", "debug": "nest start --debug --watch", "build": "nest build", + "build:auth": "bun run build auth", "build:api": "bun run build api", "build:swap": "bun run build swap", "build:nostr": "bun run build nostr", @@ -37,13 +38,16 @@ "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", "@nestjs/event-emitter": "^2.1.1", + "@nestjs/jwt": "^10.2.0", "@nestjs/microservices": "^10.4.5", "@nestjs/mongoose": "^10.1.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/sequelize": "^10.0.1", "@nestjs/swagger": "^7.4.2", "@nostr-dev-kit/ndk": "^2.10.6", "africastalking": "^0.7.0", + "bcryptjs": "^2.4.3", "cache-manager": "4.1.0", "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", @@ -56,6 +60,9 @@ "mongoose": "^8.8.0", "nestjs-pino": "^4.1.0", "nostr-tools": "^2.10.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "pino-http": "^10.3.0", "pino-pretty": "^11.3.0", "redis": "^4.7.0", @@ -68,10 +75,13 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/bcryptjs": "^2.4.6", "@types/bun": "latest", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^22.7.5", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "@types/sequelize": "^4.28.20", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", diff --git a/proto/auth.proto b/proto/auth.proto new file mode 100644 index 0000000..ba02026 --- /dev/null +++ b/proto/auth.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package auth; + +service AuthService { + rpc CreateUser (CreateUserRequest) returns (User) {} + rpc Authenticate (AuthUserRequest) returns (User) {} +} + +message CreateUserRequest { + string phone = 1; + string pin = 2; + string npub = 3; + repeated string roles = 4; +} + +message AuthUserRequest { + string Authentication = 1; +} + +message User { + string id = 1; + string pin = 2; + + reserved 3, 4, 5; + + // Users phone number identifier + optional string phone = 6; + // Users phone number has been verified via SMS + bool phone_verified = 7; + // Users nostr npub identifier + optional string npub = 8; + // Users name or nym + optional string name = 9; + // Users avatar url + optional string avatar_url = 10; + + reserved 11, 12, 13, 14; + + // Users roles + repeated string roles = 15; +}