Merge branch 'main' of git.savefamilygps.net:SaveFamily/sf-sim
This commit is contained in:
@@ -11,7 +11,7 @@ post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body:form-urlencoded {
|
body:form-urlencoded {
|
||||||
iccid: 8933201125065160380
|
iccid: 8933201125065160406
|
||||||
offer: SAVEFAMILY1
|
offer: SAVEFAMILY1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest watch",
|
"test": "vitest watch",
|
||||||
"build": "yarn workspaces foreach -A --exclude sim-consumidor-nos run build && cp .env dist/ && yarn setup:runtime",
|
"build": "yarn workspaces foreach -A --exclude sim-consumidor-nos run build && cp .env dist/ && yarn setup:runtime",
|
||||||
"setup:runtime": "mkdir -p dist/packages/node_modules && ln -sf ../sim-shared dist/packages/node_modules/sim-shared && ln -sf ../sim-consumidor-objenious dist/packages/node_modules/sim-consumidor-objenious && ln -sf ../sim-entrada-eventos dist/packages/node_modules/sim-entrada-eventos && ln -sf ../sim-objenious-cron dist/packages/node_modules/sim-objenious-cron",
|
"setup:runtime": "mkdir -p dist/packages/node_modules && ln -sf ../sim-shared dist/packages/node_modules/sim-shared && ln -sf ../sf-consumidor-objenious dist/packages/node_modules/sim-consumidor-objenious",
|
||||||
"start": "yarn setup:runtime && yarn workspaces foreach -Apiv --exclude sim-consumidor-nos run start",
|
"start": "yarn setup:runtime && yarn workspaces foreach -Apiv --exclude sim-consumidor-nos run start",
|
||||||
"typecheck": "npx tsc --noEmit",
|
"typecheck": "npx tsc --noEmit",
|
||||||
"dev": "yarn workspaces foreach -Apiv --exclude sim-consumidor-nos run dev ",
|
"dev": "yarn workspaces foreach -Apiv --exclude sim-consumidor-nos run dev ",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// PEM ?
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
* Está demasiado acoplado a objenious, hay que sacar un servicio jwt general para
|
* Está demasiado acoplado a objenious, hay que sacar un servicio jwt general para
|
||||||
@@ -10,7 +8,9 @@ import { env } from "#config/env/index.js";
|
|||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
JWTToken
|
JWTToken,
|
||||||
|
JWTHeader,
|
||||||
|
IJWTService
|
||||||
} from "sim-shared/domain/JWT.js"
|
} from "sim-shared/domain/JWT.js"
|
||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
|
|
||||||
@@ -32,15 +32,6 @@ type TokensRequestResponse = {
|
|||||||
"scope": string
|
"scope": string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthHeaders = {
|
|
||||||
content_type: string,
|
|
||||||
sub: string,
|
|
||||||
iss: string,
|
|
||||||
aud: string,
|
|
||||||
jti: string,
|
|
||||||
iat: number,
|
|
||||||
exp: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
|
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
|
||||||
|
|
||||||
@@ -64,7 +55,7 @@ const DEFAULT_HEADERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addIATHeaders(authHeaders: Object) {
|
function addIATHeaders(authHeaders: Object) {
|
||||||
const headers = <AuthHeaders>{
|
const headers = <JWTHeader>{
|
||||||
...authHeaders,
|
...authHeaders,
|
||||||
sub: env.OBJ_CLIENT_ID,
|
sub: env.OBJ_CLIENT_ID,
|
||||||
iss: env.OBJ_CLIENT_ID,
|
iss: env.OBJ_CLIENT_ID,
|
||||||
@@ -76,6 +67,7 @@ function addIATHeaders(authHeaders: Object) {
|
|||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ObjeniousTokenBody = any
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* El servicio gestiona un par de tokens auth - refresh para las
|
* El servicio gestiona un par de tokens auth - refresh para las
|
||||||
@@ -85,10 +77,10 @@ function addIATHeaders(authHeaders: Object) {
|
|||||||
* Debe tener un cliente HTTP propio para que no le afecten los
|
* Debe tener un cliente HTTP propio para que no le afecten los
|
||||||
* interceptores, sino puede haber bucles de refresco de token
|
* interceptores, sino puede haber bucles de refresco de token
|
||||||
*/
|
*/
|
||||||
export class JWTService {
|
export class JWTService implements IJWTService<ObjeniousTokenBody> {
|
||||||
public isRefreshing: boolean = false;
|
public isRefreshing: boolean = false;
|
||||||
public authToken: JWTToken<{}> | undefined
|
public authToken: JWTToken<ObjeniousTokenBody> | undefined;
|
||||||
private refreshToken?: JWTToken<{}>
|
private refreshToken?: JWTToken<ObjeniousTokenBody> | undefined;
|
||||||
|
|
||||||
constructor(args?: {
|
constructor(args?: {
|
||||||
token?: string // si se partiese de un token existente,
|
token?: string // si se partiese de un token existente,
|
||||||
@@ -122,7 +114,7 @@ export class JWTService {
|
|||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNewTokens() {
|
public async getNewAuthToken() {
|
||||||
const bodyWithtoken = {
|
const bodyWithtoken = {
|
||||||
...DEFAULT_BODY,
|
...DEFAULT_BODY,
|
||||||
client_assertion: this.buildJwtBody()
|
client_assertion: this.buildJwtBody()
|
||||||
@@ -139,8 +131,8 @@ export class JWTService {
|
|||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
res = (await req).data as TokensRequestResponse;
|
res = (await req).data as TokensRequestResponse;
|
||||||
this.authToken = new JWTToken(res.access_token)
|
this.authToken = new JWTToken<ObjeniousTokenBody>(res.access_token)
|
||||||
this.refreshToken = new JWTToken(res.refresh_token)
|
this.refreshToken = new JWTToken<ObjeniousTokenBody>(res.refresh_token)
|
||||||
return this.authToken
|
return this.authToken
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorString = "No se ha podido conseguir el token de acceso de OBJENIOUS"
|
const errorString = "No se ha podido conseguir el token de acceso de OBJENIOUS"
|
||||||
@@ -163,7 +155,7 @@ export class JWTService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Caso 3: Ningún token es valido
|
// Caso 3: Ningún token es valido
|
||||||
await this.getNewTokens()
|
await this.getNewAuthToken()
|
||||||
|
|
||||||
if (this.authToken == undefined) throw new Error("Error obteniendo tokens de auth")
|
if (this.authToken == undefined) throw new Error("Error obteniendo tokens de auth")
|
||||||
|
|
||||||
@@ -190,8 +182,8 @@ export class JWTService {
|
|||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
res = (await req).data as TokensRequestResponse;
|
res = (await req).data as TokensRequestResponse;
|
||||||
this.authToken = new JWTToken(res.access_token)
|
this.authToken = new JWTToken<ObjeniousTokenBody>(res.access_token)
|
||||||
this.refreshToken = new JWTToken(res.refresh_token)
|
this.refreshToken = new JWTToken<ObjeniousTokenBody>(res.refresh_token)
|
||||||
return this.authToken
|
return this.authToken
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorString = "No se ha podido conseguir el token de acceso de OBJENIOUS"
|
const errorString = "No se ha podido conseguir el token de acceso de OBJENIOUS"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ActionData, ActivationData } from "#domain/DTOs/objeniousapi.js"
|
|||||||
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
|
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
|
||||||
import { AxiosError } from "axios"
|
import { AxiosError } from "axios"
|
||||||
import { Result } from "sim-shared/domain/Result.js"
|
import { Result } from "sim-shared/domain/Result.js"
|
||||||
import { ObjeniousOperation, IOperationsRepository as OperationsRepositoryPort } from "#domain/operationsRepository.port.js"
|
import { ObjeniousOperation, IOperationsRepository as OperationsRepositoryPort } from "sim-shared/domain/operationsRepository.port.js"
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// - Pasar a un archivo de DTOs
|
// - Pasar a un archivo de DTOs
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { OperationsRepository } from "#adapters/OperationRepository.js"
|
import { OperationsRepository } from "sim-shared/infrastructure/OperationRepository.js"
|
||||||
import { startRMQClient } from "#config/eventBus.config.js"
|
import { startRMQClient } from "#config/eventBus.config.js"
|
||||||
import { httpInstance } from "#config/httpClient.config.js"
|
import { httpInstance } from "#config/httpClient.config.js"
|
||||||
import { pgPool } from "#config/postgreConfig.js"
|
import { pgPool } from "#config/postgreConfig.js"
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"cors": "*",
|
"cors": "*",
|
||||||
"dotenv": "*",
|
"dotenv": "*",
|
||||||
"express": "*",
|
"express": "*",
|
||||||
|
"sim-consumidor-objenious": "sim-consumidor-objenious:*",
|
||||||
"sim-shared": "sim-shared:*",
|
"sim-shared": "sim-shared:*",
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
|
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
|
||||||
import { JWTService } from "sim-consumidor-objenious/aplication/JWT.service.js"
|
|
||||||
import { env } from "./env/index.js"
|
import { env } from "./env/index.js"
|
||||||
|
import { JWTService } from "packages/sim-consumidor-objenious/aplication/JWT.service.js"
|
||||||
|
|
||||||
const OBJ_BASE_URL = env.OBJ_BASE_URL
|
const OBJ_BASE_URL = env.OBJ_BASE_URL
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
import { pgPool } from "./config/postgreConfig.js"
|
import { pgPool } from "./config/postgreConfig.js"
|
||||||
import { PgClient } from "sim-shared/infrastructure/PgClient.js"
|
import { PgClient } from "sim-shared/infrastructure/PgClient.js"
|
||||||
import { OperationsRepository } from "../sim-consumidor-objenious/infrastructure/OperationRepository.js"
|
|
||||||
import { httpInstance } from "./config/httpClient.config.js"
|
import { httpInstance } from "./config/httpClient.config.js"
|
||||||
import { CheckObjeniousRequests } from "./tasks/check_objenious_request.js"
|
import { CheckObjeniousRequests } from "./tasks/check_objenious_request.js"
|
||||||
|
import { OperationsRepository } from "sim-shared/infrastructure/OperationRepository.js"
|
||||||
|
|
||||||
async function startCron() {
|
async function startCron() {
|
||||||
const commonSettings = {
|
const commonSettings = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IOperationsRepository, Objenious, ObjeniousOperation, ObjeniousOperationChange, StatusEnum } from "sim-consumidor-objenious/domain/operationsRepository.port.js"
|
import { IOperationsRepository, Objenious, ObjeniousOperation, ObjeniousOperationChange, StatusEnum } from "sim-shared/domain/operationsRepository.port.js";
|
||||||
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
|
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
|
||||||
|
|
||||||
export class CheckObjeniousRequests {
|
export class CheckObjeniousRequests {
|
||||||
|
|||||||
@@ -1,11 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Herramientas para gestionar todos los trabajos con JWT, los tipos
|
||||||
|
* definidos aquí deben de ser sufcientemente generales para usarse
|
||||||
|
* en todos los proyectos.
|
||||||
|
*
|
||||||
|
* Las cabeceras de un token y la firma son standard para todos, pero
|
||||||
|
* el body puede variar con contenido nuevo.
|
||||||
|
*/
|
||||||
|
|
||||||
import { sign } from "node:crypto"
|
import { sign } from "node:crypto"
|
||||||
|
|
||||||
export type JWTHeader = {
|
export interface IJWTService<T> {
|
||||||
alg: string,
|
/* Obtener un token de auth sin comprobar nada */
|
||||||
typ: string,
|
getNewAuthToken: () => Promise<JWTToken<T>>,
|
||||||
kid: string
|
/* Obtener un token valido -> sino refrescar -> sin pillar un token nuevo */
|
||||||
|
getAccessToken: () => Promise<JWTToken<T>>,
|
||||||
|
/* Intenta refrescar el token actual */
|
||||||
|
tryRefreshToken: () => Promise<JWTToken<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type JWTHeader = {
|
||||||
|
/* algoritmo de firma */
|
||||||
|
alg: string,
|
||||||
|
/* tipo de token */
|
||||||
|
typ: string,
|
||||||
|
/* key ID */
|
||||||
|
kid?: string
|
||||||
|
/* content type */
|
||||||
|
cty?: string
|
||||||
|
/**/
|
||||||
|
iss?: string,
|
||||||
|
sub?: string,
|
||||||
|
aud?: string,
|
||||||
|
jti?: string,
|
||||||
|
iat?: number,
|
||||||
|
exp?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Define los campos basicos del payload de un token, en <T> se
|
||||||
|
* añaden los campos espeficos de la comunicacion
|
||||||
|
* */
|
||||||
export type JWTPayload<T> = {
|
export type JWTPayload<T> = {
|
||||||
/** (Issuer) Quién emitió el token */
|
/** (Issuer) Quién emitió el token */
|
||||||
iss?: string;
|
iss?: string;
|
||||||
@@ -45,29 +79,33 @@ export type JWT<T> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// todo pasar a la clase JWT
|
// todo pasar a la clase JWT
|
||||||
function signJWT(args: {
|
function signJWT(args: SignatureOptions) {
|
||||||
algorythm: "sha256" | string,
|
|
||||||
data: string,
|
|
||||||
privateKey: string
|
|
||||||
}) {
|
|
||||||
const signature = sign(
|
const signature = sign(
|
||||||
args.algorythm,
|
args.algorythm,
|
||||||
Buffer.from(args.data),
|
Buffer.from(args.data),
|
||||||
args.privateKey
|
args.privateKey
|
||||||
)
|
)
|
||||||
return signature
|
return signature.toString("base64url")
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SignatureOptions = {
|
||||||
|
algorythm: "sha256" | string,
|
||||||
|
data: string,
|
||||||
|
privateKey: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JWTToken<T> {
|
export class JWTToken<T> {
|
||||||
|
|
||||||
public rawToken: string
|
public rawToken: string
|
||||||
private decodedPayload: JWTPayload<T> | undefined
|
private decodedPayload: JWTPayload<T> | undefined
|
||||||
|
private signatureFunc = signJWT
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
token: string
|
token: string,
|
||||||
|
externalSignatureFunc?: (args: SignatureOptions) => string
|
||||||
) {
|
) {
|
||||||
this.rawToken = token
|
this.rawToken = token
|
||||||
this.decodedPayload = this.decodePayload()
|
this.decodedPayload = this.decodePayload()
|
||||||
|
if (externalSignatureFunc != undefined) this.signatureFunc = externalSignatureFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromParts<T>(args: {
|
public static fromParts<T>(args: {
|
||||||
@@ -76,10 +114,13 @@ export class JWTToken<T> {
|
|||||||
sigantureData?: {
|
sigantureData?: {
|
||||||
privateKey: string,
|
privateKey: string,
|
||||||
algorythm: string
|
algorythm: string
|
||||||
}
|
},
|
||||||
|
signatureFunc?: (args: SignatureOptions) => string
|
||||||
}) {
|
}) {
|
||||||
const strHeader = JSON.stringify(args.header)
|
const strHeader = JSON.stringify(args.header)
|
||||||
const base64Header = Buffer.from(strHeader).toString("base64url")
|
const base64Header = Buffer.from(strHeader).toString("base64url")
|
||||||
|
const signatureFunc = args.signatureFunc ?? signJWT
|
||||||
|
|
||||||
let token = base64Header
|
let token = base64Header
|
||||||
|
|
||||||
if (args.payload != undefined) {
|
if (args.payload != undefined) {
|
||||||
@@ -89,11 +130,11 @@ export class JWTToken<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.sigantureData != undefined) {
|
if (args.sigantureData != undefined) {
|
||||||
const base64signature = signJWT({
|
const base64signature = signatureFunc({
|
||||||
algorythm: args.sigantureData.algorythm,
|
algorythm: args.sigantureData.algorythm,
|
||||||
privateKey: args.sigantureData.privateKey,
|
privateKey: args.sigantureData.privateKey,
|
||||||
data: token
|
data: token,
|
||||||
}).toString("base64url")
|
})
|
||||||
token += ("." + base64signature)
|
token += ("." + base64signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,4 +157,14 @@ export class JWTToken<T> {
|
|||||||
if (expirationDate * 1000 <= now.getTime()) return true
|
if (expirationDate * 1000 <= now.getTime()) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid() {
|
||||||
|
throw new Error("No implementado")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public verifySignature() {
|
||||||
|
throw new Error("No implementado")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import axios, { AxiosInstance } from "axios"
|
import axios, { AxiosInstance } from "axios"
|
||||||
import { JWTToken } from "../domain/JWT.js"
|
import { IJWTService, JWTToken } from "../domain/JWT.js"
|
||||||
|
|
||||||
|
// Cambiar por IJWRGeneralService
|
||||||
|
|
||||||
export type JWTProvider<T> = {
|
export type JWTProvider<T> = {
|
||||||
/** El servidor está solicitando un token nuevo o refrescando el actual*/
|
/** El servidor está solicitando un token nuevo o refrescando el actual*/
|
||||||
@@ -13,11 +15,13 @@ export class HttpClient {
|
|||||||
|
|
||||||
public client: AxiosInstance
|
public client: AxiosInstance
|
||||||
private jwtManager: JWTProvider<{}>
|
private jwtManager: JWTProvider<{}>
|
||||||
|
private jwtService: IJWTService<any> | undefined;
|
||||||
|
|
||||||
constructor(args: {
|
constructor(args: {
|
||||||
baseURL: string,
|
baseURL: string,
|
||||||
headers: Object,
|
headers: Object,
|
||||||
jwtManager: JWTProvider<{}> // todo: asociar el tipo de token
|
jwtManager: JWTProvider<{}> // todo: asociar el tipo de token,
|
||||||
|
jwtService?: IJWTService<any>
|
||||||
}) {
|
}) {
|
||||||
this.client = axios.create({
|
this.client = axios.create({
|
||||||
...args
|
...args
|
||||||
@@ -25,6 +29,7 @@ export class HttpClient {
|
|||||||
|
|
||||||
this.jwtManager = args.jwtManager
|
this.jwtManager = args.jwtManager
|
||||||
|
|
||||||
|
if (args.jwtService != undefined) this.jwtService = args.jwtService
|
||||||
|
|
||||||
this.client.interceptors.request.use(
|
this.client.interceptors.request.use(
|
||||||
async (config) => {
|
async (config) => {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { IOperationsRepository, ObjeniousOperation, ObjeniousOperationChange } from "#domain/operationsRepository.port.js";
|
import { IOperationsRepository, ObjeniousOperation, ObjeniousOperationChange } from "sim-shared/domain/operationsRepository.port.js";
|
||||||
import { Result } from "sim-shared/domain/Result.js";
|
import { Result } from "sim-shared/domain/Result.js";
|
||||||
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
|
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class OperationsRepository implements IOperationsRepository {
|
export class OperationsRepository implements IOperationsRepository {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"../../packages/sim-shared/**/*.ts"
|
"../../packages/sim-shared/**/*.ts",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2769,7 +2769,7 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"sim-consumidor-objenious@workspace:packages/sim-consumidor-objenious":
|
"sim-consumidor-objenious@sim-consumidor-objenious:*, sim-consumidor-objenious@workspace:packages/sim-consumidor-objenious":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "sim-consumidor-objenious@workspace:packages/sim-consumidor-objenious"
|
resolution: "sim-consumidor-objenious@workspace:packages/sim-consumidor-objenious"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2784,6 +2784,7 @@ __metadata:
|
|||||||
dotenv: "npm:*"
|
dotenv: "npm:*"
|
||||||
express: "npm:*"
|
express: "npm:*"
|
||||||
prettier: "npm:*"
|
prettier: "npm:*"
|
||||||
|
sim-consumidor-objenious: "sim-consumidor-objenious:*"
|
||||||
sim-shared: "sim-shared:*"
|
sim-shared: "sim-shared:*"
|
||||||
supertest: "npm:*"
|
supertest: "npm:*"
|
||||||
tsc-alias: "npm:^1.8.16"
|
tsc-alias: "npm:^1.8.16"
|
||||||
|
|||||||
Reference in New Issue
Block a user