Skip to content

Commit

Permalink
refactor: account-service
Browse files Browse the repository at this point in the history
- Migrate to typescript
- Fix account removal
- Break circular dependency with tunnel-service
  • Loading branch information
fredriklindberg committed Dec 6, 2023
1 parent eda5d72 commit c54a7f7
Show file tree
Hide file tree
Showing 14 changed files with 345 additions and 108 deletions.
61 changes: 40 additions & 21 deletions src/account/account-service.js → src/account/account-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import assert from 'assert/strict';
import Storage from '../storage/index.js';
import Account from './account.js';
import { Logger } from '../logger.js';
import TunnelService from '../tunnel/tunnel-service.js';

type AccountListResult = {
cursor: string | null,
accounts: Array<Account>,
};

class AccountService {
static ACCOUNT_ID_ALPHABET = 'CDEFHJKMNPRTVWXY2345689';
static ACCOUNT_ID_LENGTH = 16;
static ACCOUNT_ID_REGEX = new RegExp(`^[${AccountService.ACCOUNT_ID_ALPHABET}]{${AccountService.ACCOUNT_ID_LENGTH}}$`);
private static ACCOUNT_ID_ALPHABET = 'CDEFHJKMNPRTVWXY2345689';
private static ACCOUNT_ID_LENGTH = 16;
private static ACCOUNT_ID_REGEX = new RegExp(`^[${AccountService.ACCOUNT_ID_ALPHABET}]{${AccountService.ACCOUNT_ID_LENGTH}}$`);

static generateId() {
static generateId(): string {
return [...Array(AccountService.ACCOUNT_ID_LENGTH)]
.map(() => {
const randomPos = Math.floor(Math.random() * AccountService.ACCOUNT_ID_ALPHABET.length);
Expand All @@ -17,7 +23,7 @@ class AccountService {
.join('');
}

static normalizeId(accountId) {
static normalizeId(accountId: string): string | undefined {
const normalized = accountId.replace(/[ -]/g, '').toUpperCase();
if (AccountService.ACCOUNT_ID_REGEX.test(normalized)) {
return normalized;
Expand All @@ -26,20 +32,26 @@ class AccountService {
}
}

static formatId(accountId) {
static formatId(accountId: string): string {
return accountId.replace(/.{1,4}(?=(.{4})+$)/g, '$&-');
}

private _db: Storage;
private logger: any;
private tunnelService: TunnelService;

constructor() {
this._db = new Storage("account");
this.logger = Logger("account-service");
this.tunnelService = new TunnelService();
}

async destroy() {
public async destroy(): Promise<void> {
await this.tunnelService.destroy();
await this._db.destroy();
}

async get(accountId) {
public async get(accountId: string): Promise<undefined | Account> {
assert(accountId != undefined);
const normalizedId = AccountService.normalizeId(accountId);
if (normalizedId == undefined) {
Expand All @@ -50,7 +62,7 @@ class AccountService {
return account;
}

async create() {
public async create(): Promise<undefined | Account> {
let maxTries = 100;
let created;
let account;
Expand All @@ -68,17 +80,17 @@ class AccountService {
return account;
}

async delete(accountId) {
async delete(accountId: string): Promise<boolean> {
assert(accountId != undefined);
const account = await this.get(accountId);
if (!(account instanceof Account)) {
return undefined;
return false;
}

const tunnels = [...account.tunnels];
try {
await Promise.all(tunnels.map((tunnelId) => {
return account.deleteTunnel(tunnelId);
await Promise.allSettled(tunnels.map((tunnelId) => {
return this.tunnelService.delete(<string>tunnelId, accountId)
}));
} catch (e) {
this.logger.error({
Expand All @@ -91,29 +103,32 @@ class AccountService {
return true;
}

async update(accountId, callback) {
async update(accountId: string, callback: (account: Account) => void): Promise<undefined | Account> {
assert(accountId != undefined);
const normalizedId = AccountService.normalizeId(accountId);
if (normalizedId == undefined) {
return undefined;
}
return this._db.update(AccountService.normalizeId(normalizedId), Account, (account) => {
return this._db.update(AccountService.normalizeId(normalizedId), Account, (account: Account) => {
callback(account);
account.updated_at = new Date().toISOString();
return true;
});
}

async list(cursor = 0, count = 10, verbose = false) {
const res = await this._db.list(cursor, count);
const data = verbose ? await this._db.read(res.data, Account) : res.data.map((id) => { return {account_id: id}; });
public async list(cursor: string | undefined, count: number = 10, verbose: boolean = false): Promise<AccountListResult> {
const res = await this._db.list(<any>cursor, count);

const data = verbose ? await this._db.read(res.data, Account) : res.data.map((id: string) => {
return {account_id: id};
});
return {
cursor: res.cursor,
accounts: data,
}
}

async disable(accountId, disabled, reason) {
async disable(accountId: string, disabled: boolean, reason: string): Promise<Account | undefined> {
assert(accountId != undefined);
const account = await this.update(accountId, (account) => {
account.status.disabled = disabled;
Expand All @@ -126,10 +141,14 @@ class AccountService {
}
});

const disconnection = [];
if (!account) {
return undefined;
}

const disconnection: Array<string> = [];
if (account.status.disabled) {
account.tunnels.forEach((tunnelId) => {
disconnection.push(account.disconnectTunnel(tunnelId));
this.tunnelService.disconnect(<string>tunnelId, accountId);
})
}
await Promise.allSettled(disconnection);
Expand Down
54 changes: 54 additions & 0 deletions src/account/account-tunnel-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Tunnel from "../tunnel/tunnel.js";
import Account from "./account.js";
import Storage from '../storage/index.js';
import AccountService from "./account-service.js";
import { TunnelConfig } from "../tunnel/tunnel-config.js";

export default class AccountTunnelService {

private storage: Storage;

constructor() {
this.storage = new Storage("account");
}

public async destroy(): Promise<void> {
await this.storage.destroy();
}

public async assignTunnel(tunnelConfig: TunnelConfig): Promise<boolean> {

const res = await this.storage.update(AccountService.normalizeId(tunnelConfig.account), Account, (account: Account) => {
if (!account.tunnels.includes(tunnelConfig.account)) {
account.tunnels.push(tunnelConfig.id);
}
account.updated_at = new Date().toISOString();
return true;
});
return res instanceof Account;
}

public async unassignTunnel(tunnelConfig: TunnelConfig): Promise<boolean> {

const res = await this.storage.update(AccountService.normalizeId(tunnelConfig.account), Account, (account: Account) => {
const pos = account.tunnels.indexOf(tunnelConfig.id);
if (pos >= 0) {
account.tunnels.splice(pos, 1);
}
account.updated_at = new Date().toISOString();
return true;
});
return res instanceof Account;
}

public async authorizedAccount(tunnel: Tunnel): Promise<Account> {
const account = await this.storage.read(AccountService.normalizeId(tunnel.account), Account);
if (!(account instanceof Account)) {
throw new Error("dangling_account");
}
if (!account.tunnels.includes(tunnel.id)) {
this.assignTunnel(tunnel.config);
}
return account;
}
}
34 changes: 0 additions & 34 deletions src/account/account.js

This file was deleted.

31 changes: 31 additions & 0 deletions src/account/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Serializable } from '../storage/serializer.js';

type AccountStatus = {
disabled: boolean,
disabled_at?: string,
disabled_reason?: string,
}

class Account implements Serializable {
public accountId: string;
public id: string;
public created_at?: string;
public updated_at?: string;
public tunnels: Array<String>;
public status: AccountStatus;

constructor(accountId: string) {
this.accountId = accountId;
this.id = accountId;
this.created_at = undefined;
this.updated_at = undefined;
this.tunnels = [];
this.status = {
disabled: false,
disabled_at: undefined,
disabled_reason: undefined,
};
}
}

export default Account;
23 changes: 10 additions & 13 deletions src/controller/admin-api-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,9 @@ class AdminApiController extends KoaController {
};

const accountProps = (account: Account) => {
const {accountId, formatted} = account.getId();
return {
account_id: accountId,
account_id_hr: formatted,
account_id: account.id,
account_id_hr: AccountService.formatId(account.id),
tunnels: account.tunnels,
status: account.status,
created_at: account.created_at,
Expand Down Expand Up @@ -179,7 +178,7 @@ class AdminApiController extends KoaController {
}
},
handler: [handleAdminAuth, handleError, async (ctx, next) => {
const account: Account = await this.accountService.get(ctx.params.account_id);
const account: Account | undefined = await this.accountService.get(ctx.params.account_id);
if (!account) {
ctx.status = 404;
ctx.body = {};
Expand Down Expand Up @@ -219,11 +218,6 @@ class AdminApiController extends KoaController {
handler: [handleAdminAuth, async (ctx, next) => {
ctx.body = {};
const res = await this.accountService.delete(ctx.params.account_id);
if (res === undefined) {
ctx.status = 404;
return;
}

if (res === false) {
ctx.status = 500;
return;
Expand Down Expand Up @@ -251,10 +245,13 @@ class AdminApiController extends KoaController {
}
},
handler: [handleAdminAuth, handleError, async (ctx, next) => {
const account = await this.accountService.disable(ctx.params.account_id, ctx.request.body.disable, ctx.request.body.reason);

ctx.status = 200;
ctx.body = accountProps(account);
const account: Account | undefined = await this.accountService.disable(ctx.params.account_id, ctx.request.body.disable, ctx.request.body.reason);
if (!account) {
ctx.status = 404;
} else {
ctx.status = 200;
ctx.body = accountProps(account);
}
}]
});

Expand Down
8 changes: 3 additions & 5 deletions src/controller/api-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,9 @@ class ApiController extends KoaController {
});

const accountProps = (account: Account) => {
const {accountId, formatted} = account.getId();
return {
account_id: accountId,
account_id_hr: formatted,
account_id: account.id,
account_id_hr: AccountService.formatId(account.id),
}
};

Expand Down Expand Up @@ -362,10 +361,9 @@ class ApiController extends KoaController {
};
return;
}
const {accountId, _} = account.getId();
ctx.status = 201;
ctx.body = {
token: Buffer.from(accountId).toString('base64'),
token: Buffer.from(account.id).toString('base64'),
};
}]
});
Expand Down
2 changes: 1 addition & 1 deletion src/storage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ class Storage {
// True if deleted
// undefined if not found
// False on storage error
async delete(key = undefined) {
async delete(key) {
if (!key) {
key = this.key;
}
Expand Down
Loading

0 comments on commit c54a7f7

Please sign in to comment.