This commit is contained in:
mobinesaghi
2025-10-21 18:48:22 +03:30
parent f50df9d6ad
commit 6e706a1af8
21 changed files with 2122 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
dist
.env
+30
View File
@@ -0,0 +1,30 @@
# Dependencies
node_modules/
# Build output
dist/
# Environment variables
.env
.env.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Testing
coverage/
.nyc_output/
+23
View File
@@ -0,0 +1,23 @@
# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install --production=false
COPY tsconfig.json ./
COPY src ./src
ENV PORT=20205
ENV DB_NAME=Identity_verification
ENV DB_USER=root
ENV DB_PASS=changeme
ENV DB_HOST=mysql
ENV DB_PORT=3306
ENV JIBIT_API_KEY=change_me
ENV JIBIT_SECRET_KEY=change_me
EXPOSE 20205
CMD ["npx", "tsx", "src/index.ts"]
+36
View File
@@ -0,0 +1,36 @@
version: '3.9'
services:
mysql:
image: mysql:8.0
container_name: iv_mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_NAME:-Identity_verification}
MYSQL_ROOT_PASSWORD: ${DB_PASS:-changeme}
ports:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
app:
build: .
container_name: iv_app
restart: unless-stopped
env_file:
- .env
environment:
PORT: ${PORT:-20205}
DB_NAME: ${DB_NAME:-Identity_verification}
DB_USER: ${DB_USER:-root}
DB_PASS: ${DB_PASS:-changeme}
DB_HOST: ${DB_HOST:-mysql}
DB_PORT: ${DB_PORT:-3306}
JIBIT_API_KEY: ${JIBIT_API_KEY:-change_me}
JIBIT_SECRET_KEY: ${JIBIT_SECRET_KEY:-change_me}
ports:
- "${PORT:-20205}:${PORT:-20205}"
depends_on:
- mysql
volumes:
mysql_data:
+1322
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "identity-verification-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts"
},
"dependencies": {
"@hono/node-server": "^1.8.0",
"@hono/swagger-ui": "^0.2.0",
"@hono/zod-openapi": "^1.1.4",
"dotenv": "^16.4.5",
"hono": "^4.0.0",
"mysql2": "^3.6.0",
"sequelize": "^6.35.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/node": "^20.11.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
}
}
+32
View File
@@ -0,0 +1,32 @@
import { Sequelize } from 'sequelize';
const DB_NAME = process.env.DB_NAME || 'Identity_verification';
const DB_USER = process.env.DB_USER || 'root';
const DB_PASS = process.env.DB_PASS || '@8@w!1o*8Aq@8P$L20';
const DB_HOST = process.env.DB_HOST || '185.97.116.24';
const DB_PORT = parseInt(process.env.DB_PORT || '3306', 10);
// اتصال به دیتابیس MySQL
export const sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASS, {
host: DB_HOST,
port: DB_PORT,
dialect: 'mysql',
logging: false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
});
// تست اتصال
export const connectDatabase = async () => {
try {
await sequelize.authenticate();
console.log('✅ Database connected');
await sequelize.sync({ alter: true });
} catch (error) {
console.error('❌ Database connection failed:', error);
}
};
+117
View File
@@ -0,0 +1,117 @@
import { Context } from 'hono';
import { User } from '../models/User.model.js';
import { Token } from '../models/Token.model.js';
import { Person } from '../models/Person.model.js';
import { Log } from '../models/Log.model.js';
import { generateTokens, refreshTokens } from '../services/jibit.service.js';
import { generateToken } from '../utils/token.js';
export const checkUser = async (c: Context) => {
try {
const authHeader = c.req.header('authorization') || '';
const providedToken = authHeader.startsWith('Bearer ') ? authHeader.substring(7) : authHeader;
const body = await c.req.json().catch(() => ({} as any));
const number = String((body as any).number || '');
const national = String((body as any).national || '');
const hostBody = (body as any).host ? String((body as any).host) : null;
if (!number || !national) {
return c.json({ matched: false, error: 'number and national are required' });
}
const userCount = await User.count();
if (userCount === 0) {
await User.create({ username: 'admin', token: 'admin-token', domain: null, host: null, isAdmin: true });
}
const requester = await User.findOne({ where: { token: providedToken } });
if (!requester) {
return c.json({ matched: false, error: 'unauthorized' }, 401);
}
let subjectUser: User = requester as any;
if (hostBody) {
if (!(requester as any).isAdmin && (requester as any).host && (requester as any).host !== hostBody) {
return c.json({ matched: false, error: 'forbidden_host' }, 403);
}
const existing = await User.findOne({ where: { host: hostBody } });
subjectUser = existing ? (existing as any) : (await User.create({ username: hostBody, host: hostBody, token: generateToken(24), isAdmin: false })) as any;
}
const cached = await Person.findOne({ where: { national, number, userId: (subjectUser as any).id } });
if (cached) {
return c.json({ matched: !!cached.matched });
}
let tokenRow = await Token.findOne();
if (!tokenRow) {
const generated = await generateTokens();
if (generated.ok) {
tokenRow = await Token.create({ accessToken: generated.accessToken!, refreshToken: generated.refreshToken! });
} else {
return c.json({ matched: false, error: generated.error || 'no Jibit tokens available', code: generated.code, status: generated.status });
}
}
let response = await loggedRequestJibit((subjectUser as any).id, tokenRow.accessToken, national, number);
if ((response as any).code === 'forbidden' || (response as any).status === 403 || (response as any).status === 401) {
const refreshed = await refreshTokens(tokenRow.refreshToken);
if (refreshed.ok) {
await Token.update({ accessToken: refreshed.accessToken!, refreshToken: refreshed.refreshToken! }, { where: { id: (tokenRow as any).id } });
response = await loggedRequestJibit((subjectUser as any).id, refreshed.accessToken!, national, number);
} else {
const generated = await generateTokens();
if (generated.ok) {
await Token.update({ accessToken: generated.accessToken!, refreshToken: generated.refreshToken! }, { where: { id: (tokenRow as any).id } });
response = await loggedRequestJibit((subjectUser as any).id, generated.accessToken!, national, number);
} else {
return c.json({ matched: false, error: generated.error || 'failed to refresh/generate token', code: generated.code, status: generated.status });
}
}
}
if (response && !(response as any).matched && ((response as any).error || (response as any).code || (response as any).status)) {
return c.json({ matched: false, error: (response as any).error || (response as any).message || 'jibit_error', code: (response as any).code, status: (response as any).status });
}
const matched = !!(response as any).matched;
try {
await Person.create({ national, number, matched, userId: (subjectUser as any).id });
} catch (e: any) {
const name = (e && e.name) || '';
if (name.includes('Unique') || name.includes('Validation')) {
const existing = await Person.findOne({ where: { national, number, userId: (subjectUser as any).id } });
return c.json({ matched: !!existing?.matched });
}
console.error('Person.create error:', e);
return c.json({ matched });
}
return c.json({ matched });
} catch (error: any) {
console.error('Error:', error);
return c.json({ matched: false, error: error?.message || 'internal_error' });
}
};
async function loggedRequestJibit(userId: number, accessToken: string, nationalCode: string, mobileNumber: string) {
const targetUrl = 'https://napi.jibit.ir/ide/v1/services/matching?' + new URLSearchParams({ nationalCode, mobileNumber }).toString();
const start = Date.now();
let status = 0;
let respBody: any = null;
const reqBody = { nationalCode, mobileNumber };
try {
const resp = await fetch(targetUrl, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` } });
status = resp.status;
try { respBody = await resp.json(); } catch {}
await Log.create({ type: 'outgoing', method: 'GET', path: '/external/jibit', headers: JSON.stringify({ Authorization: 'Bearer ***' }), body: JSON.stringify(reqBody).slice(0, 200000), response: JSON.stringify(respBody || {}).slice(0, 200000), status, durationMs: Date.now() - start, ip: null, userId, targetUrl });
if (status === 403 || status === 401) return { code: 'forbidden', status };
if (!resp.ok) return { matched: false, error: (respBody && (respBody.error || respBody.message)) || `jibit_http_${status}`, code: respBody && respBody.code, status };
return respBody || { matched: false };
} catch (e: any) {
await Log.create({ type: 'outgoing', method: 'GET', path: '/external/jibit', headers: JSON.stringify({ Authorization: 'Bearer ***' }), body: JSON.stringify(reqBody).slice(0, 200000), response: JSON.stringify({ error: e?.message || 'network_error' }), status: status || 0, durationMs: Date.now() - start, ip: null, userId, targetUrl });
return { matched: false, error: e?.message || 'network_error' };
}
}
+172
View File
@@ -0,0 +1,172 @@
import 'dotenv/config';
import { OpenAPIHono } from '@hono/zod-openapi';
import { serve } from '@hono/node-server';
import { swaggerUI } from '@hono/swagger-ui';
import { cors } from 'hono/cors';
import { registerUserRoutes } from './routes/user.routes.js';
import { connectDatabase } from './config/database.js';
import { Log } from './models/Log.model.js';
import { User } from './models/User.model.js';
const app = new OpenAPIHono();
app.use('*', cors());
app.use('*', async (c, next) => {
const start = Date.now();
const method = c.req.method;
const url = new URL(c.req.url);
const path = url.pathname;
const headersObj: Record<string, string> = {};
c.req.raw.headers.forEach((v, k) => { headersObj[k] = v; });
let requestBody: any = null;
try { requestBody = await c.req.json(); } catch {}
await next();
const durationMs = Date.now() - start;
const status = c.res.status || 200;
let responseBody: any = null;
try { responseBody = await c.res.clone().json(); } catch {}
try {
const count = await Log.count();
if (count > 85000) {
await Log.destroy({ where: {}, order: [['createdAt', 'ASC']], limit: count - 85000 } as any);
}
} catch {}
try {
let userId: number | null = null;
const auth = headersObj['authorization'] || headersObj['Authorization'];
const token = auth ? (auth.startsWith('Bearer ') ? auth.substring(7) : auth) : null;
if (token) {
const user = await User.findOne({ where: { token } });
if (user) userId = (user as any).id;
}
const ipHeader = headersObj['x-forwarded-for'] || headersObj['cf-connecting-ip'] || headersObj['x-real-ip'] || '';
await Log.create({
type: 'incoming',
method,
path,
headers: JSON.stringify(headersObj).slice(0, 100000),
body: requestBody ? JSON.stringify(requestBody).slice(0, 200000) : '',
response: responseBody ? JSON.stringify(responseBody).slice(0, 200000) : '',
status,
durationMs,
ip: ipHeader || null,
userId,
targetUrl: null
});
} catch {}
});
registerUserRoutes(app);
app.doc('/doc', {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'سامانه استعلام کد ملی',
description: 'API اعتبارسنجی کد ملی و شماره موبایل'
}
});
app.get('/swagger', swaggerUI({ url: '/doc' }));
app.get('/', (c) => {
return c.html(`<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>پنل ادمین - استعلام کد ملی</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:#0b1220;color:#e5e7eb;margin:0;padding:24px}
.card{max-width:760px;margin:0 auto;background:#0f172a;border:1px solid #1f2937;border-radius:12px;padding:20px}
h1{font-size:20px;margin:0 0 12px}
.row{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px}
.col{flex:1 1 220px;min-width:220px}
label{display:block;font-size:12px;color:#93c5fd;margin-bottom:6px}
input{width:100%;padding:10px 12px;border:1px solid #334155;border-radius:8px;background:#0b1020;color:#e5e7eb}
button{padding:10px 14px;border:1px solid #334155;border-radius:8px;background:#1e293b;color:#e5e7eb;cursor:pointer}
button.primary{background:#2563eb;border-color:#2563eb}
.bar{display:flex;gap:8px;align-items:end;margin-bottom:16px}
pre{background:#0b1020;border:1px solid #1f2937;border-radius:8px;padding:12px;overflow:auto;max-height:360px}
small{color:#9ca3af}
a{color:#93c5fd;text-decoration:none}
</style>
</head>
<body>
<div class="card">
<h1>پنل ادمین / NationalID Checker</h1>
<div class="row">
<div class="col">
<label>توکن (Authorization)</label>
<input id="token" placeholder="Bearer token" />
</div>
<div class="col">
<label>Host</label>
<input id="host" placeholder="مثال: golak" />
</div>
</div>
<div class="bar">
<button class="primary" id="saveToken">ذخیره توکن</button>
<small id="tokenStatus"></small>
<div style="flex:1"></div>
<a href="/swagger" target="_blank">Swagger</a>
</div>
<div class="row">
<div class="col">
<label>شماره موبایل</label>
<input id="number" placeholder="09xxxxxxxxx" />
</div>
<div class="col">
<label>کد ملی</label>
<input id="national" placeholder="xxxxxxxxxx" />
</div>
</div>
<div class="bar">
<button id="runQuery" class="primary">استعلام</button>
<small>نتیجه در پایین نمایش داده می‌شود</small>
</div>
<pre id="out">Ready.</pre>
</div>
<script>
const $ = (id) => document.getElementById(id);
const out = (data) => { $('out').textContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2); };
function loadToken(){ const t = localStorage.getItem('admin_token') || ''; $('token').value = t; $('tokenStatus').textContent = t ? 'توکن ست شد' : 'توکن وارد نشده'; }
$('saveToken').onclick = () => { localStorage.setItem('admin_token', $('token').value.trim()); loadToken(); };
$('runQuery').onclick = async () => {
const token = localStorage.getItem('admin_token') || '';
const number = $('number').value.trim();
const national = $('national').value.trim();
const host = $('host').value.trim();
if(!token){ out('توکن وارد نشده'); return; }
try{
const res = await fetch('/check_user', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': token },
body: JSON.stringify({ number, national, host })
});
const json = await res.json();
out(json);
}catch(e){ out(String(e)); }
};
loadToken();
</script>
</body>
</html>`);
});
const port = parseInt(process.env.PORT || '20205', 10);
connectDatabase().then(() => {
console.log(`🚀 Server: http://localhost:${port}`);
console.log(`📖 Docs: http://localhost:${port}/swagger`);
serve({ fetch: app.fetch, port });
});
+23
View File
@@ -0,0 +1,23 @@
import { DataTypes, Model } from 'sequelize';
import { sequelize } from '../config/database.js';
export class Card extends Model {
declare id: number;
declare cardNumber: string;
declare bin?: string;
declare bank?: string;
}
Card.init(
{
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
cardNumber: { type: DataTypes.STRING(16), allowNull: false, unique: true },
bin: { type: DataTypes.STRING(6), allowNull: true },
bank: { type: DataTypes.STRING(128), allowNull: true }
},
{
sequelize,
tableName: 'cards',
timestamps: true
}
);
+44
View File
@@ -0,0 +1,44 @@
import { DataTypes, Model } from 'sequelize';
import { sequelize } from '../config/database.js';
export class Log extends Model {
declare id: number;
declare type: string; // incoming | outgoing
declare method: string;
declare path: string;
declare headers: string;
declare body: string;
declare response: string;
declare status: number;
declare durationMs: number;
declare ip: string | null;
declare userId: number | null;
declare targetUrl: string | null;
}
Log.init(
{
id: { type: DataTypes.BIGINT, autoIncrement: true, primaryKey: true },
type: { type: DataTypes.STRING(16), allowNull: false, defaultValue: 'incoming' },
method: { type: DataTypes.STRING(10), allowNull: false },
path: { type: DataTypes.STRING(512), allowNull: false },
headers: { type: DataTypes.TEXT('long'), allowNull: false },
body: { type: DataTypes.TEXT('long'), allowNull: true },
response: { type: DataTypes.TEXT('long'), allowNull: true },
status: { type: DataTypes.INTEGER, allowNull: false },
durationMs: { type: DataTypes.INTEGER, allowNull: false },
ip: { type: DataTypes.STRING(64), allowNull: true },
userId: { type: DataTypes.INTEGER, allowNull: true },
targetUrl: { type: DataTypes.STRING(1024), allowNull: true }
},
{
sequelize,
tableName: 'logs',
timestamps: true,
indexes: [
{ fields: ['createdAt'] },
{ fields: ['path'] },
{ fields: ['type'] }
]
}
);
+29
View File
@@ -0,0 +1,29 @@
import { DataTypes, Model } from 'sequelize';
import { sequelize } from '../config/database.js';
export class Person extends Model {
declare id: number;
declare national: string;
declare number: string;
declare matched: boolean;
declare userId: number | null;
}
Person.init(
{
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
national: { type: DataTypes.STRING(10), allowNull: false },
number: { type: DataTypes.STRING(11), allowNull: false },
matched: { type: DataTypes.BOOLEAN, defaultValue: false },
userId: { type: DataTypes.INTEGER, allowNull: true }
},
{
sequelize,
tableName: 'persons',
timestamps: true,
indexes: [
{ unique: true, fields: ['national', 'number', 'userId'] },
{ fields: ['userId'] }
]
}
);
+63
View File
@@ -0,0 +1,63 @@
import { DataTypes, Model } from 'sequelize';
import { sequelize } from '../config/database.js';
import { User } from './User.model.js';
// مدل استعلام
export class Query extends Model {
declare id: number;
declare nationalCode: string;
declare mobileNumber: string;
declare matched: boolean;
declare userId: number;
declare source: string;
}
Query.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
nationalCode: {
type: DataTypes.STRING(10),
allowNull: false
},
mobileNumber: {
type: DataTypes.STRING(11),
allowNull: false
},
matched: {
type: DataTypes.BOOLEAN,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
source: {
type: DataTypes.STRING(50),
defaultValue: 'cache',
comment: 'cache or jibit'
}
},
{
sequelize,
tableName: 'queries',
timestamps: true,
indexes: [
{
unique: true,
fields: ['nationalCode', 'mobileNumber']
}
]
}
);
// Relations
Query.belongsTo(User, { foreignKey: 'userId' });
User.hasMany(Query, { foreignKey: 'userId' });
+21
View File
@@ -0,0 +1,21 @@
import { DataTypes, Model } from 'sequelize';
import { sequelize } from '../config/database.js';
export class Token extends Model {
declare id: number;
declare accessToken: string;
declare refreshToken: string;
}
Token.init(
{
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
accessToken: { type: DataTypes.TEXT, allowNull: false },
refreshToken: { type: DataTypes.TEXT, allowNull: false }
},
{
sequelize,
tableName: 'tokens',
timestamps: true
}
);
+34
View File
@@ -0,0 +1,34 @@
import { DataTypes, Model } from 'sequelize';
import { sequelize } from '../config/database.js';
// مدل کاربر (برای احراز هویت و محدودیت‌ها)
export class User extends Model {
declare id: number;
declare username: string;
declare domain: string | null;
declare host: string | null;
declare token: string;
declare isAdmin: boolean;
declare queryCount: number;
}
User.init(
{
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
username: { type: DataTypes.STRING(100), allowNull: false },
domain: { type: DataTypes.STRING(255), allowNull: true },
host: { type: DataTypes.STRING(255), allowNull: true },
token: { type: DataTypes.STRING(255), allowNull: false },
isAdmin: { type: DataTypes.BOOLEAN, defaultValue: false },
queryCount: { type: DataTypes.INTEGER, defaultValue: 0 }
},
{
sequelize,
tableName: 'users',
timestamps: true,
indexes: [
{ fields: ['token'] },
{ fields: ['username'] }
]
}
);
+37
View File
@@ -0,0 +1,37 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { CheckUserSchema, CheckUserResponseSchema } from '../schemas/user.schema.js';
import { checkUser } from '../controllers/user.controller.js';
// Route definition
const checkUserRoute = createRoute({
method: 'post',
path: '/check_user',
tags: ['User Verification'],
summary: 'بررسی تطابق کد ملی و شماره موبایل',
description: 'اعتبارسنجی کد ملی و شماره موبایل کاربر',
request: {
body: {
content: {
'application/json': {
schema: CheckUserSchema
}
}
}
},
responses: {
200: {
description: 'موفق',
content: {
'application/json': {
schema: CheckUserResponseSchema
}
}
}
}
});
// Register routes
export const registerUserRoutes = (app: OpenAPIHono) => {
// @ts-ignore: openapi handler generic typing mismatch
app.openapi(checkUserRoute, checkUser as any);
};
+21
View File
@@ -0,0 +1,21 @@
import { z } from '@hono/zod-openapi';
// Request Schema
export const CheckUserSchema = z.object({
number: z.string().regex(/^09\d{9}$/, 'شماره موبایل نامعتبر').openapi({
example: '09123456789',
description: 'شماره موبایل'
}),
national: z.string().length(10, 'کد ملی باید 10 رقمی باشد').openapi({
example: '0123456789',
description: 'کد ملی'
})
}).openapi('CheckUserRequest');
// Response Schema
export const CheckUserResponseSchema = z.object({
matched: z.boolean().openapi({
example: true,
description: 'وضعیت تطابق اطلاعات'
})
}).openapi('CheckUserResponse');
+40
View File
@@ -0,0 +1,40 @@
export type JibitTokenResult = { ok: boolean; accessToken?: string; refreshToken?: string; error?: string; code?: string; status?: number };
const API_KEY = process.env.JIBIT_API_KEY || '';
const SECRET_KEY = process.env.JIBIT_SECRET_KEY || '';
export async function generateTokens(): Promise<JibitTokenResult> {
if (!API_KEY || !SECRET_KEY) return { ok: false, error: 'jibit_keys_missing' };
let resp: Response;
try {
resp = await fetch('https://napi.jibit.ir/ide/v1/tokens/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey: API_KEY, secretKey: SECRET_KEY })
});
} catch (e: any) {
return { ok: false, error: e?.message || 'network_error' };
}
let data: any = null;
try { data = await resp.json(); } catch {}
if (!resp.ok) return { ok: false, error: (data && (data.error || data.message)) || `jibit_http_${resp.status}`, code: data && data.code, status: resp.status };
return { ok: true, accessToken: data?.accessToken, refreshToken: data?.refreshToken };
}
export async function refreshTokens(refreshToken: string): Promise<JibitTokenResult> {
if (!API_KEY || !SECRET_KEY) return { ok: false, error: 'jibit_keys_missing' };
let resp: Response;
try {
resp = await fetch('https://napi.jibit.ir/ide/v1/tokens/generate', {
method: 'POST',
headers: { Authorization: `Bearer ${refreshToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey: API_KEY, secretKey: SECRET_KEY })
});
} catch (e: any) {
return { ok: false, error: e?.message || 'network_error' };
}
let data: any = null;
try { data = await resp.json(); } catch {}
if (!resp.ok) return { ok: false, error: (data && (data.error || data.message)) || `jibit_http_${resp.status}`, code: data && data.code, status: resp.status };
return { ok: true, accessToken: data?.accessToken, refreshToken: data?.refreshToken };
}
+7
View File
@@ -0,0 +1,7 @@
import { randomBytes } from 'crypto';
export function generateToken(byteLength: number = 32): string {
const buf = randomBytes(byteLength);
// base64url encode
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
+20
View File
@@ -0,0 +1,20 @@
### بررسی اطلاعات کاربر
POST http://localhost:20205/check_user
Authorization: Bearer admin-token
Content-Type: application/json
{
"number": "09372281182",
"national": "1274467144",
"host": "golak"
}
###
### مشاهده مستندات
GET http://localhost:20205/
###
### مشخصات OpenAPI
GET http://localhost:20205/doc
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}