Skip to content

Commit

Permalink
feat: Site settings migration (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheSlimvReal authored Sep 6, 2023
1 parent e760f1f commit ce9fbe9
Show file tree
Hide file tree
Showing 12 changed files with 875 additions and 108 deletions.
636 changes: 605 additions & 31 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"axios": "^1.3.4",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
"rxjs": "^7.2.0",
"sharp": "^0.32.5"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
Expand Down
8 changes: 6 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Module } from '@nestjs/common';
import { CouchdbAdminController } from './couchdb-admin/couchdb-admin.controller';
import { CouchdbAdminController } from './couchdb/couchdb-admin.controller';
import { HttpModule } from '@nestjs/axios';
import { ConfigModule } from '@nestjs/config';
import { CouchdbService } from './couchdb/couchdb.service';
import { KeycloakService } from './couchdb/keycloak.service';
import { MigrationController } from './couchdb/migration.controller';

@Module({
imports: [HttpModule, ConfigModule.forRoot({ isGlobal: true })],
controllers: [CouchdbAdminController],
controllers: [CouchdbAdminController, MigrationController],
providers: [CouchdbService, KeycloakService],
})
export class AppModule {}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { BulkUpdateDto } from './bulk-update.dto';
import { HttpService } from '@nestjs/axios';
import { catchError, firstValueFrom, map } from 'rxjs';
import { firstValueFrom, map } from 'rxjs';
import { ApiOperation } from '@nestjs/swagger';
import * as credentials from 'src/assets/credentials.json';
import { ConfigService } from '@nestjs/config';
import { CouchdbService } from './couchdb.service';
import { KeycloakService } from './keycloak.service';

@Controller('couchdb-admin')
export class CouchdbAdminController {
private keycloakPassword = this.configService.get('KEYCLOAK_ADMIN_PASSWORD');
private keycloakUrl = this.configService.get('KEYCLOAK_URL');
private domain = this.configService.get('DOMAIN');

constructor(
private http: HttpService,
private configService: ConfigService,
private couchdbService: CouchdbService,
private keycloakService: KeycloakService,
) {}

@ApiOperation({
Expand Down Expand Up @@ -50,7 +48,11 @@ export class CouchdbAdminController {
const editedOrgs: string[] = [];
for (const cred of credentials) {
const file = `/app/Config:CONFIG_ENTITY`;
const config = await this.getDataFromDB(cred.name, file, cred.password);
const config = await this.couchdbService.get(
cred.name,
file,
cred.password,
);

const configString = JSON.stringify(config);
const regex = new RegExp(searchString, 'g');
Expand All @@ -75,15 +77,19 @@ export class CouchdbAdminController {
// Update `credentials.json` using the `collect_credentials.sh` script on the server
for (const cred of credentials) {
const file = `/app/Config:CONFIG_ENTITY`;
const config = await this.getDataFromDB(cred.name, file, cred.password);
const config = await this.couchdbService.get(
cred.name,
file,
cred.password,
);

const configString = JSON.stringify(config);
const regex = new RegExp(searchString, 'g');

if (configString.match(regex)) {
const replaced = configString.replace(regex, replaceString);
editedOrgs.push(cred.name);
await this.putDataToDB(
await this.couchdbService.put(
cred.name,
file,
JSON.parse(replaced),
Expand All @@ -94,36 +100,6 @@ export class CouchdbAdminController {
return editedOrgs;
}

private getDataFromDB(org: string, path: string, password: string) {
const auth = { username: 'admin', password };
const url = `https://${org}.${this.domain}/db`;
return firstValueFrom(
this.http.get(`${url}/couchdb${path}`, { auth }).pipe(
catchError(() => this.http.get(`${url}${path}`, { auth })),
map((res) => res.data.rows ?? res.data),
),
);
}

private putDataToDB(
org: string,
path: string,
data,
password: string,
method = this.http.put,
) {
const auth = { username: 'admin', password };
const url = `https://${org}.${this.domain}/db`;
return firstValueFrom(
method.call(this.http, `${url}/couchdb${path}`, data, { auth }).pipe(
catchError(() =>
method.call(this.http, `${url}${path}`, data, { auth }),
),
map((res: any) => res.data?.docs),
),
);
}

@ApiOperation({
description: `Get statistics of how many children and users are registered.`,
})
Expand All @@ -135,27 +111,28 @@ export class CouchdbAdminController {
childrenActive: number;
users: number;
}[] = [];
const token = await this.getKeycloakToken();
const token = await this.keycloakService.getKeycloakToken();
const allUsers =
'/_users/_all_docs?startkey="org.couchdb.user:"&endkey="org.couchdb.user:\uffff"';
const allChildren =
'/app/_all_docs?startkey="Child:"&endkey="Child:\uffff"';
const activeChildren = '/app/_find';
for (const cred of credentials) {
const users = await this.getUsersFromKeycloak(cred.name, token).catch(
() => this.getDataFromDB(cred.name, allUsers, cred.password),
);
const children = await this.getDataFromDB(
const users = await this.keycloakService
.getUsersFromKeycloak(cred.name, token)
.catch(() =>
this.couchdbService.get(cred.name, allUsers, cred.password),
);
const children = await this.couchdbService.get(
cred.name,
allChildren,
cred.password,
);
const active: any = await this.putDataToDB(
const active: any = await this.couchdbService.post(
cred.name,
activeChildren,
activeChildrenFilter,
cred.password,
this.http.post,
);
stats.push({
name: cred.name,
Expand All @@ -166,33 +143,6 @@ export class CouchdbAdminController {
}
return stats;
}

private getKeycloakToken(): Promise<string> {
const body = new URLSearchParams();
body.set('username', 'admin');
body.set('password', this.keycloakPassword);
body.set('grant_type', 'password');
body.set('client_id', 'admin-cli');
return firstValueFrom(
this.http
.post<{ access_token: string }>(
`${this.keycloakUrl}/realms/master/protocol/openid-connect/token`,
body.toString(),
)
.pipe(map((res) => res.data.access_token)),
);
}

private getUsersFromKeycloak(org: string, token: string) {
return firstValueFrom(
this.http
.get<{ access_token: string }>(
`${this.keycloakUrl}/admin/realms/${org}/users?enabled=true`,
{ headers: { Authorization: `Bearer ${token}` } },
)
.pipe(map((res) => res.data)),
);
}
}

const activeChildrenFilter = {
Expand Down
18 changes: 18 additions & 0 deletions src/couchdb/couchdb.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CouchdbService } from './couchdb.service';

describe('CouchdbService', () => {
let service: CouchdbService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CouchdbService],
}).compile();

service = module.get<CouchdbService>(CouchdbService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
59 changes: 59 additions & 0 deletions src/couchdb/couchdb.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Injectable } from '@nestjs/common';
import { catchError, firstValueFrom, map } from 'rxjs';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class CouchdbService {
private domain = this.configService.get('DOMAIN');

constructor(
private http: HttpService,
private configService: ConfigService,
) {}

get(org: string, path: string, password: string) {
const auth = { username: 'admin', password };
const url = `https://${org}.${this.domain}/db`;
return firstValueFrom(
this.http.get(`${url}/couchdb${path}`, { auth }).pipe(
catchError(() => this.http.get(`${url}${path}`, { auth })),
map((res) => res.data.rows ?? res.data),
),
);
}

put(
org: string,
path: string,
data,
password: string,
headers?: any,
): Promise<any> {
const auth = { username: 'admin', password };
const url = `https://${org}.${this.domain}/db`;
return firstValueFrom(
this.http.put(`${url}/couchdb${path}`, data, { auth, headers }).pipe(
catchError(() => this.http.put(`${url}${path}`, data, { auth })),
map(({ data }) => (data?.docs ? data.docs : data)),
),
);
}

post(
org: string,
path: string,
data,
password: string,
headers?: any,
): Promise<any> {
const auth = { username: 'admin', password };
const url = `https://${org}.${this.domain}/db`;
return firstValueFrom(
this.http.post(`${url}/couchdb${path}`, data, { auth, headers }).pipe(
catchError(() => this.http.post(`${url}${path}`, data, { auth })),
map(({ data }) => (data?.docs ? data.docs : data)),
),
);
}
}
18 changes: 18 additions & 0 deletions src/couchdb/keycloak.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { KeycloakService } from './keycloak.service';

describe('KeycloakService', () => {
let service: KeycloakService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [KeycloakService],
}).compile();

service = module.get<KeycloakService>(KeycloakService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
41 changes: 41 additions & 0 deletions src/couchdb/keycloak.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { firstValueFrom, map } from 'rxjs';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class KeycloakService {
private keycloakPassword = this.configService.get('KEYCLOAK_ADMIN_PASSWORD');
private keycloakUrl = this.configService.get('KEYCLOAK_URL');
constructor(
private http: HttpService,
private configService: ConfigService,
) {}

getKeycloakToken(): Promise<string> {
const body = new URLSearchParams();
body.set('username', 'admin');
body.set('password', this.keycloakPassword);
body.set('grant_type', 'password');
body.set('client_id', 'admin-cli');
return firstValueFrom(
this.http
.post<{ access_token: string }>(
`${this.keycloakUrl}/realms/master/protocol/openid-connect/token`,
body.toString(),
)
.pipe(map((res) => res.data.access_token)),
);
}

getUsersFromKeycloak(org: string, token: string) {
return firstValueFrom(
this.http
.get<{ access_token: string }>(
`${this.keycloakUrl}/admin/realms/${org}/users?enabled=true`,
{ headers: { Authorization: `Bearer ${token}` } },
)
.pipe(map((res) => res.data)),
);
}
}
18 changes: 18 additions & 0 deletions src/couchdb/migration.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MigrationController } from './migration.controller';

describe('MigrationController', () => {
let controller: MigrationController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MigrationController],
}).compile();

controller = module.get<MigrationController>(MigrationController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
Loading

0 comments on commit ce9fbe9

Please sign in to comment.