Skip to content

Commit

Permalink
feat: add first service
Browse files Browse the repository at this point in the history
  • Loading branch information
alvarogfn committed Apr 17, 2024
1 parent f0e8981 commit 64f6be5
Show file tree
Hide file tree
Showing 25 changed files with 392 additions and 20 deletions.
3 changes: 1 addition & 2 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@octopost/backend"]
"updateInternalDependencies": "patch"
}
5 changes: 5 additions & 0 deletions .changeset/tiny-cars-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@octopost/types": patch
---

first types
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"main": "index.ts",
"type": "module",
"private": "true",
"private": true,
"engines": {
"node": "20",
"pnpm": "9"
Expand Down
10 changes: 10 additions & 0 deletions src/config/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Express from 'express';
import setupRoutes from './routes';
import setupMiddlewares from './middlewares';

const app = Express();

setupRoutes(app);
setupMiddlewares(app);

export default app;
26 changes: 26 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import dotenv from 'dotenv';

if (process.env['MODE'] === 'DEV') {
dotenv.config({
path: ['dev.env', '.env'],
});
}

if (process.env['MODE'] === 'PROD') {
dotenv.config({
path: ['prod.env', '.env'],
});
}

if (process.env['MODE'] === 'QA') {
dotenv.config({
path: ['qa.env', '.env'],
});
}

export default {
PORT: process.env['PORT'],
HOSTNAME: process.env['HOSTNAME'],
OAUTH_MASTODON_CLIENT_SECRET: process.env['OAUTH_MASTODON_CLIENT_SECRET'],
OAUTH_MASTODON_CLIENT_ID: process.env['OAUTH_MASTODON_CLIENT_ID'],
} as Record<string, string>;
8 changes: 8 additions & 0 deletions src/config/middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Express } from 'express';
import { cors } from '@/middlewares/cors';
import { bodyParser } from '@/middlewares/body-parser';

export default function setupMiddlewares(app: Express): void {
app.use(bodyParser);
app.use(cors);
}
15 changes: 15 additions & 0 deletions src/config/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Express } from 'express';
import { glob } from 'glob';
import { resolve } from 'node:path';

export default function setupRoutes(app: Express) {
const routes = glob.sync([
resolve(import.meta.dirname, '../features/**/v1/*-routes.ts'),
]);

// eslint-disable-next-line @typescript-eslint/no-floating-promises
routes.map(async (file) => {
const { router, prefix } = (await import(file)).default;
app.use(`/v1/${prefix}`, router);
});
}
75 changes: 75 additions & 0 deletions src/features/mastodon/infra/api/mastodon-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { HttpClient } from '@/shared/protocols/http-client';

interface GetAuthorizationURLParams {
responseType: string;
clientId: string;
redirectUri?: string;
instance: string;
scopes: string[];
}

interface GetTokenParams {
grantType: string;
code: string;
clientId: string;
redirectUri?: string;
instance: string;
scopes: string[];
}

interface GetTokenResponse {
access_token: string;
token_type: string;
scope: string;
created_at: number;
}

export class MastodonApi {
private readonly scopes: string[] = ['read', 'write'];

constructor(private readonly httpClient: HttpClient) {}

getAuthorizationURL = ({
responseType,
clientId,
instance,
redirectUri = 'urn:ietf:wg:oauth:2.0:oob',
scopes = this.scopes,
}: GetAuthorizationURLParams) => {
const url = new URL('/oauth/authorize', instance);
const searchParams = url.searchParams;

searchParams.append('client_id', clientId);
searchParams.append('redirect_uri', redirectUri);
searchParams.append('response_type', responseType);
searchParams.append('scope', scopes.join('+'));

return url.href;
};

getToken = async ({
grantType,
code,
clientId,
redirectUri = 'urn:ietf:wg:oauth:2.0:oob',
instance,
scopes = this.scopes,
}: GetTokenParams) => {
const form = new FormData();
const url = new URL('/oauth/token', instance);

form.append('grant_type', grantType);
form.append('code', code);
form.append('client_id', clientId);
form.append('redirect_uri', redirectUri);
form.append('scope', scopes.join(','));

const response = await this.httpClient.request<GetTokenResponse>({
method: 'post',
body: form,
url: url.href,
});

return response;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { HttpError } from '@/shared/errors/HttpError';
import {
responseErrorFactory,
responseOkFactory,
} from '@/shared/factories/responses';
import type { Controller } from '@/shared/protocols/controller';
import type { HttpRequest } from '@/shared/protocols/http';
import type { Service } from '@/shared/protocols/service';

export class MastodonController implements Controller {
constructor(private mastodonServiceFindAll: Service<unknown>) {}

findAll = async (httpRequest: HttpRequest) => {
try {
const response = await this.mastodonServiceFindAll.execute(httpRequest);
return responseOkFactory(response);
} catch (error) {
return responseErrorFactory(error as HttpError);
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MastodonAuthService } from '@/features/mastodon/services/v1/mastodon-auth-service';
import { MastodonController } from '@/features/mastodon/infra/http/controller/v1/mastodon-controller';
import { MastodonApi } from '@/features/mastodon/infra/api/mastodon-api';
import { AxiosHttpClient } from '@/shared/infra/http-client';

export function mastodonControllerFactory() {
const axiosHttpClient = new AxiosHttpClient();

const mastodonApi = new MastodonApi(axiosHttpClient);

const mastodonServiceFindAll = new MastodonAuthService(mastodonApi);

const mastodonController = new MastodonController(mastodonServiceFindAll);

return { mastodonController };
}
21 changes: 21 additions & 0 deletions src/features/mastodon/infra/http/routes/v1/mastodon-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Router } from 'express';
import { mastodonControllerFactory } from './mastodon-controller-factory';
import { redirectRouteAdapter } from '@/shared/adapters/redirect-router-adapter';

const router = Router();

const { mastodonController } = mastodonControllerFactory();

router.get('/authorize', redirectRouteAdapter(mastodonController.findAll));

router.get(
'/callback',
redirectRouteAdapter((payload) =>
Promise.resolve({ body: payload, statusCode: 301 })
)
);

export default {
router,
prefix: 'mastodon',
};
30 changes: 30 additions & 0 deletions src/features/mastodon/services/v1/mastodon-auth-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {
MastodonAuthRequestBody,
MastodonAuthRequestResponse,
} from '@octopost/types';

import type { MastodonApi } from '@/features/mastodon/infra/api/mastodon-api';
import type { HttpRequest } from '@/shared/protocols/http';
import type { ServiceRedirect } from '@/shared/protocols/service';
import env from '@/config/env';

export class MastodonAuthService implements ServiceRedirect {
constructor(private mastodonApi: MastodonApi) {}

execute(
httpRequest: HttpRequest<object, object, MastodonAuthRequestBody>
): Promise<MastodonAuthRequestResponse> {
const body = httpRequest.body!;

body.instance = 'https://mastodon.social';

const authorizationUrl = this.mastodonApi.getAuthorizationURL({
instance: body.instance,
clientId: env.OAUTH_MASTODON_CLIENT_ID,
responseType: 'code',
scopes: ['read', 'write'],
});

return Promise.resolve(authorizationUrl);
}
}
4 changes: 0 additions & 4 deletions src/index.test.ts

This file was deleted.

26 changes: 13 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import http from 'node:http';
import env from './config/env';
import app from './config/app';

// Create a local server to receive data from
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
data: 'Hello World!',
})
);
});
function startServer() {
const listener = app.listen(Number(env.PORT), env.HOSTNAME, () => {
console.log(`Server running at http://localhost:${env.PORT}`);
});

server.listen(8000, () => {
console.log('listening on port 8000');
});
listener.on('error', (err) => {
console.error(err);
process.exit(1);
});
}

startServer();
3 changes: 3 additions & 0 deletions src/middlewares/body-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { json } from 'express';

export const bodyParser = json();
8 changes: 8 additions & 0 deletions src/middlewares/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Request, Response, NextFunction } from 'express';

export const cors = (req: Request, res: Response, next: NextFunction): void => {
res.set('access-control-allow-origin', '*');
res.set('access-control-allow-headers', '*');
res.set('access-control-allow-methods', '*');
next();
};
23 changes: 23 additions & 0 deletions src/shared/adapters/redirect-router-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import type { RequestHandler } from 'express';
import type { HttpRequest, HttpResponse } from '../protocols/http';

type Method = (httpRequest: HttpRequest) => Promise<HttpResponse>;

export function redirectRouteAdapter(method: Method): RequestHandler {
return async (req, res) => {
const httpRequest: HttpRequest = {
body: req.body as object,
pathParams: req.params as Record<string, string | number>,
queryParams: req.query as Record<string, string | number>,
};

const httpResponse = await method(httpRequest);

if (httpResponse.statusCode !== 200) {
return res.status(httpResponse.statusCode).json(httpResponse.body);
}

res.status(httpResponse.statusCode).redirect(httpResponse.body);
};
}
19 changes: 19 additions & 0 deletions src/shared/adapters/router-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import type { RequestHandler } from 'express';
import type { HttpRequest, HttpResponse } from '../protocols/http';

type Method = (httpRequest: HttpRequest) => Promise<HttpResponse>;

export function routeAdapter(method: Method): RequestHandler {
return async (req, res) => {
const httpRequest: HttpRequest = {
body: req.body as object,
pathParams: req.params as Record<string, string | number>,
queryParams: req.query as Record<string, string | number>,
};

const httpResponse = await method(httpRequest);

res.status(httpResponse.statusCode).json(httpResponse.body);
};
}
8 changes: 8 additions & 0 deletions src/shared/errors/HttpError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class HttpError extends Error {
constructor(
public readonly code: number,
message: string
) {
super(message);
}
}
12 changes: 12 additions & 0 deletions src/shared/factories/responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { HttpError } from '../errors/HttpError';
import type { HttpResponse } from '../protocols/http';

export const responseErrorFactory = (error: HttpError): HttpResponse => ({
statusCode: 500,
body: { statusCode: 500, message: error.message },
});

export const responseOkFactory = (body: unknown): HttpResponse => ({
statusCode: 200,
body,
});
30 changes: 30 additions & 0 deletions src/shared/infra/http-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import axios from 'axios';
import type { AxiosError } from 'axios';
import type {
HttpClient,
HttpClientRequest,
HttpClientResponse,
} from '../protocols/http-client';

export class AxiosHttpClient implements HttpClient {
async request(data: HttpClientRequest): Promise<HttpClientResponse> {
try {
const response = await axios({
url: data.url,
method: data.method,
data: data.body,
headers: data.headers,
});

return {
statusCode: response.status,
body: response.data,
};
} catch (error) {
return {
statusCode: (error as AxiosError).response!.status,
body: (error as AxiosError).response!.data,
};
}
}
}
1 change: 1 addition & 0 deletions src/shared/protocols/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface Controller {}
Loading

0 comments on commit 64f6be5

Please sign in to comment.