| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- import express, { Request, Response } from 'express';
- import axios from 'axios';
- import crypto from 'crypto';
- import dotenv from 'dotenv';
- // Load environment variables from .env.server
- dotenv.config({ path: 'local_server/.env.server' });
- const app = express();
- const PORT = 3001;
- app.use(express.json());
- // Store for caching access tokens and jsapi tickets
- const tokenCache: {
- accessToken: string | null;
- accessTokenExpiry: number;
- jsapiTicket: string | null;
- jsapiTicketExpiry: number;
- } = {
- accessToken: null,
- accessTokenExpiry: 0,
- jsapiTicket: null,
- jsapiTicketExpiry: 0,
- };
- // Type definitions
- type AccessTokenResponse = {
- accessToken: string;
- expireIn: number;
- };
- type JsapiTicketResponse = {
- jsapiTicket: string;
- expireIn: number;
- };
- /**
- * Get access token from DingTalk API
- */
- async function getAccessToken(): Promise<{ accessToken: string; expiresIn: number }> {
- // Check if we have a cached token that's still valid
- if (tokenCache.accessToken && Date.now() < tokenCache.accessTokenExpiry) {
- console.log('Using cached access token');
- return { accessToken: tokenCache.accessToken, expiresIn: 7200 };
- }
- const appKey = process.env.DINGTALK_APPKEY || '';
- const appSecret = process.env.DINGTALK_APPSECRET || '';
- if (!appKey || !appSecret) {
- throw new Error('DINGTALK_APPKEY and DINGTALK_APPSECRET must be set in environment variables');
- }
- // Request access token from DingTalk API
- const response = await axios.post('https://api.dingtalk.com/v1.0/oauth2/accessToken', {
- appKey,
- appSecret,
- });
- const data: AccessTokenResponse = response.data;
- // Cache the token with expiry time (current time + expireIn - 300 seconds for safety)
- tokenCache.accessToken = data.accessToken;
- tokenCache.accessTokenExpiry = Date.now() + (data.expireIn - 300) * 1000;
- console.log('Got new access token from DingTalk API');
- return { accessToken: data.accessToken, expiresIn: data.expireIn };
- }
- /**
- * Get jsapi ticket from DingTalk API
- */
- async function getJsapiTicket(accessToken: string): Promise<{ jsapiTicket: string; expiresIn: number }> {
- // Check if we have a cached ticket that's still valid
- if (tokenCache.jsapiTicket && Date.now() < tokenCache.jsapiTicketExpiry) {
- console.log('Using cached jsapi ticket');
- return { jsapiTicket: tokenCache.jsapiTicket, expiresIn: 7200 };
- }
- // Request jsapi ticket from DingTalk API
- const response = await axios.post(
- 'https://api.dingtalk.com/v1.0/oauth2/jsapiTickets',
- {},
- {
- headers: {
- 'x-acs-dingtalk-access-token': accessToken,
- },
- }
- );
- const data: JsapiTicketResponse = response.data;
- // Cache the ticket with expiry time (current time + expireIn - 300 seconds for safety)
- tokenCache.jsapiTicket = data.jsapiTicket;
- tokenCache.jsapiTicketExpiry = Date.now() + (data.expireIn - 300) * 1000;
- console.log('Got new jsapi ticket from DingTalk API');
- return { jsapiTicket: data.jsapiTicket, expiresIn: data.expireIn };
- }
- /**
- * Calculate signature for dd.config
- */
- function calculateSignature(jsapiTicket: string, nonceStr: string, timeStamp: number, url: string): string {
- try {
- // Create the string to sign using SHA-1 (as per DingTalk's expected algorithm)
- const plain = `jsapi_ticket=${jsapiTicket}&noncestr=${nonceStr}×tamp=${timeStamp}&url=${decodeUrl(url)}`;
- const sha1 = crypto.createHash('sha1');
- sha1.update(plain, 'utf8');
- return sha1.digest('hex');
- } catch (error) {
- console.error('Error in calculateSignature function:', error);
- throw error;
- }
- }
- /**
- * Because iOS passes URL that is encoded, but Android passes the original URL.
- * So we need to decode the parameters as a regular URL decode
- */
- function decodeUrl(urlString: string): string {
- try {
- const parsedUrl = new URL(urlString);
- let urlBuffer = `${parsedUrl.protocol}//`;
- if (parsedUrl.host) {
- urlBuffer += parsedUrl.host;
- }
- if (parsedUrl.pathname) {
- urlBuffer += parsedUrl.pathname;
- }
- if (parsedUrl.search) {
- urlBuffer += `?${decodeURIComponent(parsedUrl.search.substring(1))}`;
- }
- return urlBuffer;
- } catch (error) {
- console.error('Error in decodeUrl function:', error);
- throw error;
- }
- }
- // Endpoint to get DingTalk access token
- app.get('/api/accessToken', async (req: Request, res: Response) => {
- try {
- const { accessToken } = await getAccessToken();
- res.json({
- accessToken,
- expiresAt: new Date(tokenCache.accessTokenExpiry).toISOString(),
- expiryIn: Math.floor((tokenCache.accessTokenExpiry - Date.now()) / 1000)
- });
- } catch (error) {
- console.error('Error getting access token:', error);
- res.status(500).json({
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- timestamp: Date.now()
- });
- }
- });
- // Endpoint to get DingTalk jsapi ticket
- app.get('/api/jsapiTicket', async (req: Request, res: Response) => {
- try {
- const { accessToken } = await getAccessToken();
- const { jsapiTicket } = await getJsapiTicket(accessToken);
- res.json({
- jsapiTicket,
- expiresAt: new Date(tokenCache.jsapiTicketExpiry).toISOString(),
- expiryIn: Math.floor((tokenCache.jsapiTicketExpiry - Date.now()) / 1000)
- });
- } catch (error) {
- console.error('Error getting jsapi ticket:', error);
- res.status(500).json({
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- timestamp: Date.now()
- });
- }
- });
- // Endpoint to get permission config for DingTalk
- app.get('/api/configPermission', async (req: Request, res: Response) => {
- try {
- const url = req.query.url as string;
- if (!url) {
- return res.status(400).json({ error: 'URL parameter is required' });
- }
- // Get agentId and corpId from environment variables
- const agentId = process.env.DINGTALK_AGENT_ID || '';
- const corpId = process.env.DINGTALK_CORP_ID || '';
- if (!agentId || !corpId) {
- return res.status(400).json({ error: 'DINGTALK_AGENT_ID and DINGTALK_CORP_ID must be set in environment variables' });
- }
- const { accessToken } = await getAccessToken();
- const { jsapiTicket } = await getJsapiTicket(accessToken);
- // Generate timestamp and nonce string
- const timeStamp = Date.now();
- const nonceStr = Math.random().toString(36).substr(2, 15);
- // Calculate signature
- const signature = calculateSignature(jsapiTicket, nonceStr, timeStamp, url);
- res.json({
- agentId,
- corpId,
- timeStamp,
- nonceStr,
- signature,
- jsApiList: ['DingdocsScript.base.readWriteAll'],
- url
- });
- } catch (error) {
- console.error('Error getting config permission:', error);
- res.status(500).json({
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- timestamp: Date.now()
- });
- }
- });
- // Start the server
- app.listen(PORT, () => {
- console.log(`DingTalk API server is running on http://localhost:${PORT}`);
- console.log('Available endpoints:');
- console.log(` GET http://localhost:${PORT}/api/accessToken - Get access token`);
- console.log(` GET http://localhost:${PORT}/api/jsapiTicket - Get JSAPI ticket`);
- console.log(` GET http://localhost:${PORT}/api/configPermission?url=<url> - Get config permission`);
- console.log('');
- console.log('Make sure to set the following environment variables:');
- console.log(' DINGTALK_APPKEY - Your DingTalk app key');
- console.log(' DINGTALK_APPSECRET - Your DingTalk app secret');
- console.log(' DINGTALK_AGENT_ID - Your DingTalk agent ID');
- console.log(' DINGTALK_CORP_ID - Your DingTalk corp ID');
- });
- // Handle graceful shutdown
- process.on('SIGINT', () => {
- console.log('\nShutting down server...');
- process.exit(0);
- });
|