Estructura para el token de alai
cabecera automatica de bearer para todas las requests a alai
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
APP_PORT=3002
|
||||
APP_HOST="0.0.0.0"
|
||||
ALAI_PORT=3002
|
||||
ALAI_HOST="0.0.0.0"
|
||||
|
||||
ENVIORMENT=development
|
||||
|
||||
ALAI_API_URL=https://wsaccess.alaisecure.com/bssrest
|
||||
|
||||
ALAI_CERTIFICATES_DIR=./certificates/
|
||||
ALAI_USERNAME=palomaibanez
|
||||
ALAI_PASSWORD=palomaibanez1234
|
||||
ALAI_BRANDID=savefamily
|
||||
|
||||
ALAI_PACKAGE=Tarifa_250MB_100MIN_5SMS
|
||||
ALAI_SUBSCRIBER_ID="16216"
|
||||
|
||||
40
packages/sim-consumidor-alai/aplication/AlaiTokenManager.ts
Normal file
40
packages/sim-consumidor-alai/aplication/AlaiTokenManager.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { AlaiRepository } from "#infrastructure/AlaiRepository.js";
|
||||
import { JWTToken } from "sim-shared/domain/JWT.js";
|
||||
import { JWTProvider } from "sim-shared/infrastructure/HTTPClient.js";
|
||||
|
||||
|
||||
export class AlaiTokenManager implements JWTProvider<{}> {
|
||||
|
||||
isRefreshing: boolean = false;
|
||||
authToken: JWTToken<{}> | undefined;
|
||||
|
||||
private async getNewAuthToken() {
|
||||
// TODO: Si no funcionase hay que reprogramar los mensajes para ser
|
||||
// consumidos mas tarde.
|
||||
const res = await AlaiRepository.login();
|
||||
|
||||
if (res.error != undefined) {
|
||||
console.error("Error obteniendo el token de ALAI", res.error)
|
||||
}
|
||||
}
|
||||
|
||||
public tryRefreshToken(): Promise<JWTToken<{}>> {
|
||||
// En Alai no existe el concepto de refresh, se solicita otro token nuevo
|
||||
return this.getAccessToken()
|
||||
};
|
||||
|
||||
public async getAccessToken(): Promise<JWTToken<{}>> {
|
||||
// Caso 1: El token actual es valido
|
||||
if (this.authToken != undefined && !this.authToken.isExpired()) {
|
||||
return this.authToken
|
||||
} else {
|
||||
// Caso 2: El token actual no existe o ha expirado
|
||||
await this.getNewAuthToken()
|
||||
}
|
||||
|
||||
// Si después de todo no se ha generado el token es un error catastrofico
|
||||
if (this.authToken == undefined) throw new Error("Error obteniendo tokens de auth")
|
||||
|
||||
return this.authToken
|
||||
};
|
||||
}
|
||||
34
packages/sim-consumidor-alai/config/env/env.ts
vendored
34
packages/sim-consumidor-alai/config/env/env.ts
vendored
@@ -1,16 +1,18 @@
|
||||
import { loadEnvFile } from "node:process";
|
||||
import path from "node:path";
|
||||
import assert from "node:assert";
|
||||
|
||||
try {
|
||||
loadEnvFile(path.join("../../.env")) // Global
|
||||
} catch (e) {
|
||||
console.error("Error cargando el .env desde ../../.env")
|
||||
}
|
||||
|
||||
try {
|
||||
loadEnvFile(path.join("./.env")) // base
|
||||
} catch (e) {
|
||||
console.error("Error cargando el .env desde ./.env")
|
||||
}
|
||||
try {
|
||||
loadEnvFile(path.join("../../.env")) // Global
|
||||
} catch (e) {
|
||||
console.error("Error cargando el .env desde ../../.env")
|
||||
}
|
||||
|
||||
export const env = {
|
||||
ENVIRONMENT: process.env.ENVIORMENT,
|
||||
@@ -30,11 +32,23 @@ export const env = {
|
||||
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
|
||||
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
|
||||
|
||||
APP_PORT: Number(process.env.APP_PORT),
|
||||
APP_HOST: String(process.env.APP_HOST),
|
||||
ALAI_PORT: Number(process.env.APP_PORT),
|
||||
ALAI_HOST: String(process.env.APP_HOST),
|
||||
|
||||
// ESPECIFICO NOS
|
||||
NOS_BASE_URL: String(process.env.NOS_BASE_URL),
|
||||
NOS_ACCESS_TOKEN: String(process.env.NOS_ACCESS_TOKEN)
|
||||
// ESPECIFICO ALAI
|
||||
ALAI_API_URL: process.env.ALAI_API_URL,
|
||||
ALAI_CERTIFICATES_DIR: process.env.ALAI_CERTIFICATES_DIR,
|
||||
ALAI_USERNAME: process.env.ALAI_USERNAME,
|
||||
ALAI_PASSWORD: process.env.ALAI_PASSWORD,
|
||||
ALAI_BRANDID: process.env.ALAI_BRANDID,
|
||||
|
||||
ALAI_PACKAGE: process.env.ALAI_PACKAGE,
|
||||
ALAI_SUBSCRIBER_ID: process.env.ALAI_SUBSCRIBER_ID
|
||||
};
|
||||
|
||||
assert.ok(env.ALAI_SUBSCRIBER_ID != undefined, "ALAI_SUBSCRIBER_ID no definido")
|
||||
assert.ok(env.ALAI_PACKAGE != undefined, "ALAI_PACKAGE no definido")
|
||||
assert.ok(env.ALAI_USERNAME != undefined, "ALAI_USERNAME no definido")
|
||||
assert.ok(env.ALAI_PASSWORD != undefined, "ALAI_PASSWORD no definido")
|
||||
assert.ok(env.ALAI_BRANDID != undefined, "ALAI_BRANDID no definido")
|
||||
assert.ok(env.ALAI_API_URL != undefined, "ALAI_API_URL no definido")
|
||||
|
||||
11
packages/sim-consumidor-alai/config/httpClient.config.ts
Normal file
11
packages/sim-consumidor-alai/config/httpClient.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
|
||||
import { AlaiTokenManager } from "#aplication/AlaiTokenManager.js"
|
||||
import { env } from "#config/env/env.js";
|
||||
|
||||
const tokenManager = new AlaiTokenManager()
|
||||
|
||||
export const alaiHttp = new HttpClient({
|
||||
baseURL: env.ALAI_API_URL as string,
|
||||
headers: {},
|
||||
jwtManager: tokenManager
|
||||
})
|
||||
@@ -1 +1,106 @@
|
||||
export namespace AlaiAPI {
|
||||
|
||||
export type LoginResponseDTO = {
|
||||
accessToken: string,
|
||||
tokenType: string,
|
||||
refreshToken: string,
|
||||
expiresIn: string // isodate
|
||||
}
|
||||
|
||||
/**
|
||||
Hardcodeado en:
|
||||
sf-sim-connections/context/infrastructure/api/alaiService.js
|
||||
const data = {
|
||||
type: "RETAIL",
|
||||
salesChannel: "OWN_CALLCENTER",
|
||||
status: "CONFIRMED",
|
||||
packages: [{ id: "Tarifa_250MB_100MIN_5SMS" }],
|
||||
subscriber: { id: "16216" }
|
||||
};
|
||||
*/
|
||||
export type CreateOrderDTO = {
|
||||
type: "RETAIL" | string,
|
||||
salesChannel: "OWN_CALLCENTER" | string,
|
||||
status: "CONFIRMED" | string,
|
||||
packages: { id: "Tarifa_250MB_100MIN_5SMS" | string }[],
|
||||
subscriber: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
type OrderPackage = {
|
||||
id: string,
|
||||
name: string,
|
||||
packagePrices: unknown,
|
||||
packageInstance: {
|
||||
id: string,
|
||||
name: string,
|
||||
links: Link[]
|
||||
}
|
||||
}
|
||||
|
||||
type Link = {
|
||||
rel: string,
|
||||
href: string,
|
||||
hreflang: string,
|
||||
media: string,
|
||||
title: string,
|
||||
type: string,
|
||||
deprecation: string,
|
||||
profile: string,
|
||||
name: string
|
||||
}
|
||||
|
||||
export type CreateOrderResponseDTO = {
|
||||
id: string,
|
||||
name: string,
|
||||
domain: string,
|
||||
orderCode: string,
|
||||
externalID: string,
|
||||
type: string,
|
||||
status: string,
|
||||
saleStatus: string,
|
||||
distributionStatus: string,
|
||||
description: string,
|
||||
salesChannel: string,
|
||||
salesPerson: string,
|
||||
deliveryType: string,
|
||||
distributionInfo: {
|
||||
providerID: string,
|
||||
providerReference: string,
|
||||
providerTracking: string,
|
||||
cashOnDelivery: boolean,
|
||||
prepaidShipping: boolean,
|
||||
description: string,
|
||||
events: {
|
||||
status: string,
|
||||
observations: string,
|
||||
date: string | Date,
|
||||
expectedDeliveryDate: string | Date,
|
||||
completedDeliveryDate: string | Date,
|
||||
}[]
|
||||
},
|
||||
packages: OrderPackage[],
|
||||
subscription: {
|
||||
id: string,
|
||||
name: string,
|
||||
links: Link[]
|
||||
}
|
||||
subscriber: {
|
||||
id: string,
|
||||
name: string,
|
||||
links: Link[]
|
||||
}
|
||||
brand: {
|
||||
id: string,
|
||||
name: string,
|
||||
links: Link[]
|
||||
}
|
||||
pos: {
|
||||
id: string,
|
||||
name: string,
|
||||
links: Link[]
|
||||
}
|
||||
links: Link[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import { env } from "#config/env/env.js"
|
||||
|
||||
export class AlaiHttpClient {
|
||||
public client: AxiosInstance;
|
||||
|
||||
constructor(
|
||||
private baseURL: string,
|
||||
//private jwtManager: JWTProvider<any>
|
||||
) {
|
||||
this.client = axios.create({
|
||||
baseURL: baseURL
|
||||
})
|
||||
|
||||
// Interceptor para los headers fijos
|
||||
this.client.interceptors.request.use(
|
||||
async (config) => {
|
||||
// Configuracion especifica de NOS (El token simepre es el mismo?)
|
||||
const token = env.NOS_ACCESS_TOKEN;
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
config.headers.set("content-type", "application/json")
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
}
|
||||
|
||||
get post() {
|
||||
return this.client.post
|
||||
}
|
||||
|
||||
get patch() {
|
||||
return this.client.patch
|
||||
}
|
||||
|
||||
get get() {
|
||||
return this.client.get
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,23 +1,16 @@
|
||||
import { Result } from "sim-shared/domain/Result.js";
|
||||
import { NosHttpClient } from "./AlaiHttpClient.js";
|
||||
import { NosApi } from "#domain/AlaiAPI.js";
|
||||
import { AlaiAPI } from "#domain/AlaiAPI.js";
|
||||
import axios, { AxiosError, AxiosResponse } from "axios";
|
||||
import { AlaiHttpClient } from "./AlaiHttpClient.js";
|
||||
import { Result } from "sim-shared/domain/Result.js";
|
||||
import { env } from "#config/env/env.js";
|
||||
|
||||
export class NosRepository {
|
||||
export class AlaiRepository {
|
||||
constructor(
|
||||
private httpClient: NosHttpClient
|
||||
private httpClient: AlaiHttpClient
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* E => Tipo de error
|
||||
* T => Tipo de dato para cod 200
|
||||
*
|
||||
* TODO:
|
||||
* - Mejor gestion de los errores
|
||||
* - E no se aplica todavia por no hacer la transformacion del error
|
||||
*/
|
||||
private async manageNosRequest<E, T>(promise: Promise<AxiosResponse<T>>): Promise<Result<string, T>> {
|
||||
private async manageRequest<E, T>(promise: Promise<AxiosResponse<T>>): Promise<Result<string, T>> {
|
||||
try {
|
||||
const res = await promise
|
||||
return {
|
||||
@@ -37,141 +30,66 @@ export class NosRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async getLineInfo(iccid: string): Promise<Result<string, NosApi.LineData>> {
|
||||
const PATH = "/subscribers/" + iccid
|
||||
console.log("PAth", PATH)
|
||||
const lineRequest = this.httpClient.get<NosApi.LineDataResponseOK>(PATH)
|
||||
const lineResponse = await this.manageNosRequest<string, NosApi.LineDataResponseOK>(lineRequest)
|
||||
|
||||
if (lineResponse.error != undefined) {
|
||||
return lineResponse
|
||||
} else {
|
||||
return {
|
||||
data: lineResponse.data.content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* El metodo de NOS de paginar las lineas
|
||||
* maximo por pagina 100, default 25
|
||||
* no devuelve el offset ni el numero de elementos restantes
|
||||
* hay que llevar la cuenta
|
||||
*/
|
||||
public async getLinePage(args: {
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
filter?: string,
|
||||
orderBy?: string
|
||||
}): Promise<Result<string, any>> {
|
||||
const PATH = "/subscribers"
|
||||
|
||||
const LIMIT = 100
|
||||
const options = {
|
||||
limit: args.limit ?? LIMIT,
|
||||
offset: args.offset ?? 0,
|
||||
filter: args.filter,
|
||||
orderBy: args.orderBy
|
||||
}
|
||||
|
||||
const pageRequest = this.httpClient.get(PATH, {
|
||||
params: options
|
||||
})
|
||||
|
||||
const pageResponse = await this.manageNosRequest<string, any>(pageRequest)
|
||||
if (pageResponse.error != undefined) {
|
||||
return pageResponse
|
||||
} else {
|
||||
return {
|
||||
data: pageResponse.data.content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getLinesInfo(iccid: string[]) /*Promise<Result<string, NosApi.LineData>>*/ {
|
||||
throw new Error("NOS no permite buscar iccid en bulk, se puede hacer un apaño pero está en proceso")
|
||||
const PATH = "/subscribers"
|
||||
const LIMIT = 100
|
||||
|
||||
const steps = Math.ceil(iccid.length / LIMIT)
|
||||
const options = {
|
||||
limit: LIMIT,
|
||||
offset: 0,
|
||||
}
|
||||
|
||||
const req = this.httpClient.post<NosApi.LineDataResponseOK>(PATH)
|
||||
const resp = await this.manageNosRequest<string, NosApi.LineDataResponseOK>(req)
|
||||
|
||||
if (resp.error != undefined) {
|
||||
return resp
|
||||
} else {
|
||||
return {
|
||||
//@ts-expect-error
|
||||
data: resp.data.content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async activateSim(iccid: string): Promise<Result<string, NosApi.ActivationData>> {
|
||||
const PATH = '/provisioning'
|
||||
const PRODUCT_ID = 1330 // No se que es, preguntar a Ivan
|
||||
public static async login(): Promise<Result<string, AlaiAPI.LoginResponseDTO>> {
|
||||
const alaiUrl = env.ALAI_API_URL
|
||||
const endpoint = "/v1/auth/login"
|
||||
const fullUrl = alaiUrl + endpoint
|
||||
const data = {
|
||||
productSetId: PRODUCT_ID
|
||||
"username": env.ALAI_USERNAME,
|
||||
"password": env.ALAI_PASSWORD,
|
||||
"brandID": env.ALAI_BRANDID
|
||||
}
|
||||
|
||||
const req = this.httpClient.post<NosApi.ActivateResponseOK>(PATH, data)
|
||||
const resp = await this.manageNosRequest<string, NosApi.ActivateResponseOK>(req)
|
||||
|
||||
if (resp.error != undefined) {
|
||||
return resp
|
||||
} else {
|
||||
try {
|
||||
const loginRes = await axios.post<AlaiAPI.LoginResponseDTO>(fullUrl, data)
|
||||
return {
|
||||
data: resp.data.content
|
||||
data: loginRes.data
|
||||
}
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const error = e as AxiosError
|
||||
return {
|
||||
error: error.code + " : " + String(error.response?.statusText)
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
error: String(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "A bar is a service provisioning action that results in a subscriber being blocked from accessing an operator's network. The bar remains in place until the operator is sent an unbar request."
|
||||
* Se entiende que un "bar" es una suspension temporal
|
||||
* Los orders son la unidad que envuelve las peticiones para garantizar ideponencia.
|
||||
*/
|
||||
public async bar(iccid: string) {
|
||||
const PATH = `/subscribers/${iccid}/products`
|
||||
const data = {
|
||||
product: "BAR DN TOTAL",
|
||||
action: "enable"
|
||||
}
|
||||
|
||||
const req = this.httpClient.patch<NosApi.BarResponseOk>(PATH, data)
|
||||
const resp = await this.manageNosRequest<string, NosApi.BarResponseOk>(req)
|
||||
|
||||
if (resp.error != undefined) {
|
||||
return resp
|
||||
} else {
|
||||
return {
|
||||
data: resp.data.content
|
||||
}
|
||||
public async createOrder() {
|
||||
// POST
|
||||
const endpoint = "/v1/order"
|
||||
const data: AlaiAPI.CreateOrderDTO = {
|
||||
type: "RETAIL",
|
||||
salesChannel: "OWN_CALLCENTER",
|
||||
status: "CONFIRMED",
|
||||
packages: [{ id: env.ALAI_PACKAGE as string }],
|
||||
subscriber: { id: env.ALAI_SUBSCRIBER_ID as string }
|
||||
}
|
||||
const promReq = this.httpClient.post<AlaiAPI.CreateOrderResponseDTO>(endpoint, data)
|
||||
const res = await this.manageRequest(promReq)
|
||||
return res
|
||||
}
|
||||
|
||||
public async unbar(iccid: string) {
|
||||
const PATH = `/subscribers/${iccid}/products`
|
||||
const data = {
|
||||
product: "BAR DN TOTAL",
|
||||
action: "disable"
|
||||
}
|
||||
|
||||
const req = this.httpClient.patch<NosApi.BarResponseOk>(PATH, data)
|
||||
const resp = await this.manageNosRequest<string, NosApi.BarResponseOk>(req)
|
||||
|
||||
if (resp.error != undefined) {
|
||||
return resp
|
||||
} else {
|
||||
return {
|
||||
data: resp.data.content
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Ver estado del order
|
||||
*/
|
||||
public async getOrder(orderId: string) {
|
||||
const ENDPOINT = `/v1/order/${orderId}`
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public async createReserve(order: string, iccid: string) {
|
||||
const ENDPOINT = `/v1/sim/${iccid}/order/${order}`
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ const DEFAULT_DATA_JWT = {
|
||||
iss: env.OBJ_CLIENT_ID,
|
||||
aud: "https://idp.docapost.io/auth/realms/GETWAY",
|
||||
jti: Date.now().toString(),
|
||||
|
||||
}
|
||||
|
||||
function addIATHeaders(authHeaders: Object) {
|
||||
|
||||
@@ -19,7 +19,7 @@ export class HttpClient {
|
||||
|
||||
constructor(args: {
|
||||
baseURL: string,
|
||||
headers: Object,
|
||||
headers: Record<string, string>,
|
||||
jwtManager: JWTProvider<{}> // todo: asociar el tipo de token,
|
||||
jwtService?: IJWTService<any>
|
||||
}) {
|
||||
@@ -37,7 +37,7 @@ export class HttpClient {
|
||||
// token valido de forma preventiva
|
||||
const token = await this.jwtManager.getAccessToken()
|
||||
|
||||
if (token == undefined) throw new Error("No se ha obtenido el token para la peticion")
|
||||
if (token == undefined) throw new Error("No se ha obtenido el token para la petición")
|
||||
|
||||
config.headers.Authorization = `Bearer ${this.jwtManager.authToken!.rawToken}`
|
||||
console.log("request completa", config.data)
|
||||
@@ -50,8 +50,7 @@ export class HttpClient {
|
||||
this.client.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
async (error) => {
|
||||
}, async (error) => {
|
||||
// TODO: Esta parte no tiene tipos, hay que asegurar el error
|
||||
const req = error.config
|
||||
console.error("[http] Error en la respuesta ", error, error.response)
|
||||
@@ -61,8 +60,17 @@ export class HttpClient {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
get get() {
|
||||
return this.client.get
|
||||
}
|
||||
|
||||
get post() {
|
||||
return this.client.post
|
||||
}
|
||||
|
||||
get patch() {
|
||||
return this.client.patch
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user