server.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import express, { Request, Response } from 'express';
  2. import axios from 'axios';
  3. import crypto from 'crypto';
  4. import dotenv from 'dotenv';
  5. // Load environment variables from .env.server
  6. dotenv.config({ path: 'local_server/.env.server' });
  7. const app = express();
  8. const PORT = 3001;
  9. app.use(express.json());
  10. // Store for caching access tokens and jsapi tickets
  11. const tokenCache: {
  12. accessToken: string | null;
  13. accessTokenExpiry: number;
  14. jsapiTicket: string | null;
  15. jsapiTicketExpiry: number;
  16. } = {
  17. accessToken: null,
  18. accessTokenExpiry: 0,
  19. jsapiTicket: null,
  20. jsapiTicketExpiry: 0,
  21. };
  22. // Type definitions
  23. type AccessTokenResponse = {
  24. accessToken: string;
  25. expireIn: number;
  26. };
  27. type JsapiTicketResponse = {
  28. jsapiTicket: string;
  29. expireIn: number;
  30. };
  31. /**
  32. * Get access token from DingTalk API
  33. */
  34. async function getAccessToken(): Promise<{ accessToken: string; expiresIn: number }> {
  35. // Check if we have a cached token that's still valid
  36. if (tokenCache.accessToken && Date.now() < tokenCache.accessTokenExpiry) {
  37. console.log('Using cached access token');
  38. return { accessToken: tokenCache.accessToken, expiresIn: 7200 };
  39. }
  40. const appKey = process.env.DINGTALK_APPKEY || '';
  41. const appSecret = process.env.DINGTALK_APPSECRET || '';
  42. if (!appKey || !appSecret) {
  43. throw new Error('DINGTALK_APPKEY and DINGTALK_APPSECRET must be set in environment variables');
  44. }
  45. // Request access token from DingTalk API
  46. const response = await axios.post('https://api.dingtalk.com/v1.0/oauth2/accessToken', {
  47. appKey,
  48. appSecret,
  49. });
  50. const data: AccessTokenResponse = response.data;
  51. // Cache the token with expiry time (current time + expireIn - 300 seconds for safety)
  52. tokenCache.accessToken = data.accessToken;
  53. tokenCache.accessTokenExpiry = Date.now() + (data.expireIn - 300) * 1000;
  54. console.log('Got new access token from DingTalk API');
  55. return { accessToken: data.accessToken, expiresIn: data.expireIn };
  56. }
  57. /**
  58. * Get jsapi ticket from DingTalk API
  59. */
  60. async function getJsapiTicket(accessToken: string): Promise<{ jsapiTicket: string; expiresIn: number }> {
  61. // Check if we have a cached ticket that's still valid
  62. if (tokenCache.jsapiTicket && Date.now() < tokenCache.jsapiTicketExpiry) {
  63. console.log('Using cached jsapi ticket');
  64. return { jsapiTicket: tokenCache.jsapiTicket, expiresIn: 7200 };
  65. }
  66. // Request jsapi ticket from DingTalk API
  67. const response = await axios.post(
  68. 'https://api.dingtalk.com/v1.0/oauth2/jsapiTickets',
  69. {},
  70. {
  71. headers: {
  72. 'x-acs-dingtalk-access-token': accessToken,
  73. },
  74. }
  75. );
  76. const data: JsapiTicketResponse = response.data;
  77. // Cache the ticket with expiry time (current time + expireIn - 300 seconds for safety)
  78. tokenCache.jsapiTicket = data.jsapiTicket;
  79. tokenCache.jsapiTicketExpiry = Date.now() + (data.expireIn - 300) * 1000;
  80. console.log('Got new jsapi ticket from DingTalk API');
  81. return { jsapiTicket: data.jsapiTicket, expiresIn: data.expireIn };
  82. }
  83. /**
  84. * Calculate signature for dd.config
  85. */
  86. function calculateSignature(jsapiTicket: string, nonceStr: string, timeStamp: number, url: string): string {
  87. try {
  88. // Create the string to sign using SHA-1 (as per DingTalk's expected algorithm)
  89. const plain = `jsapi_ticket=${jsapiTicket}&noncestr=${nonceStr}&timestamp=${timeStamp}&url=${decodeUrl(url)}`;
  90. const sha1 = crypto.createHash('sha1');
  91. sha1.update(plain, 'utf8');
  92. return sha1.digest('hex');
  93. } catch (error) {
  94. console.error('Error in calculateSignature function:', error);
  95. throw error;
  96. }
  97. }
  98. /**
  99. * Because iOS passes URL that is encoded, but Android passes the original URL.
  100. * So we need to decode the parameters as a regular URL decode
  101. */
  102. function decodeUrl(urlString: string): string {
  103. try {
  104. const parsedUrl = new URL(urlString);
  105. let urlBuffer = `${parsedUrl.protocol}//`;
  106. if (parsedUrl.host) {
  107. urlBuffer += parsedUrl.host;
  108. }
  109. if (parsedUrl.pathname) {
  110. urlBuffer += parsedUrl.pathname;
  111. }
  112. if (parsedUrl.search) {
  113. urlBuffer += `?${decodeURIComponent(parsedUrl.search.substring(1))}`;
  114. }
  115. return urlBuffer;
  116. } catch (error) {
  117. console.error('Error in decodeUrl function:', error);
  118. throw error;
  119. }
  120. }
  121. // Endpoint to get DingTalk access token
  122. app.get('/api/accessToken', async (req: Request, res: Response) => {
  123. try {
  124. const { accessToken } = await getAccessToken();
  125. res.json({
  126. accessToken,
  127. expiresAt: new Date(tokenCache.accessTokenExpiry).toISOString(),
  128. expiryIn: Math.floor((tokenCache.accessTokenExpiry - Date.now()) / 1000)
  129. });
  130. } catch (error) {
  131. console.error('Error getting access token:', error);
  132. res.status(500).json({
  133. error: error instanceof Error ? error.message : 'Unknown error occurred',
  134. timestamp: Date.now()
  135. });
  136. }
  137. });
  138. // Endpoint to get DingTalk jsapi ticket
  139. app.get('/api/jsapiTicket', async (req: Request, res: Response) => {
  140. try {
  141. const { accessToken } = await getAccessToken();
  142. const { jsapiTicket } = await getJsapiTicket(accessToken);
  143. res.json({
  144. jsapiTicket,
  145. expiresAt: new Date(tokenCache.jsapiTicketExpiry).toISOString(),
  146. expiryIn: Math.floor((tokenCache.jsapiTicketExpiry - Date.now()) / 1000)
  147. });
  148. } catch (error) {
  149. console.error('Error getting jsapi ticket:', error);
  150. res.status(500).json({
  151. error: error instanceof Error ? error.message : 'Unknown error occurred',
  152. timestamp: Date.now()
  153. });
  154. }
  155. });
  156. // Endpoint to get permission config for DingTalk
  157. app.get('/api/configPermission', async (req: Request, res: Response) => {
  158. try {
  159. const url = req.query.url as string;
  160. if (!url) {
  161. return res.status(400).json({ error: 'URL parameter is required' });
  162. }
  163. // Get agentId and corpId from environment variables
  164. const agentId = process.env.DINGTALK_AGENT_ID || '';
  165. const corpId = process.env.DINGTALK_CORP_ID || '';
  166. if (!agentId || !corpId) {
  167. return res.status(400).json({ error: 'DINGTALK_AGENT_ID and DINGTALK_CORP_ID must be set in environment variables' });
  168. }
  169. const { accessToken } = await getAccessToken();
  170. const { jsapiTicket } = await getJsapiTicket(accessToken);
  171. // Generate timestamp and nonce string
  172. const timeStamp = Date.now();
  173. const nonceStr = Math.random().toString(36).substr(2, 15);
  174. // Calculate signature
  175. const signature = calculateSignature(jsapiTicket, nonceStr, timeStamp, url);
  176. res.json({
  177. agentId,
  178. corpId,
  179. timeStamp,
  180. nonceStr,
  181. signature,
  182. jsApiList: ['DingdocsScript.base.readWriteAll'],
  183. url
  184. });
  185. } catch (error) {
  186. console.error('Error getting config permission:', error);
  187. res.status(500).json({
  188. error: error instanceof Error ? error.message : 'Unknown error occurred',
  189. timestamp: Date.now()
  190. });
  191. }
  192. });
  193. // Start the server
  194. app.listen(PORT, () => {
  195. console.log(`DingTalk API server is running on http://localhost:${PORT}`);
  196. console.log('Available endpoints:');
  197. console.log(` GET http://localhost:${PORT}/api/accessToken - Get access token`);
  198. console.log(` GET http://localhost:${PORT}/api/jsapiTicket - Get JSAPI ticket`);
  199. console.log(` GET http://localhost:${PORT}/api/configPermission?url=<url> - Get config permission`);
  200. console.log('');
  201. console.log('Make sure to set the following environment variables:');
  202. console.log(' DINGTALK_APPKEY - Your DingTalk app key');
  203. console.log(' DINGTALK_APPSECRET - Your DingTalk app secret');
  204. console.log(' DINGTALK_AGENT_ID - Your DingTalk agent ID');
  205. console.log(' DINGTALK_CORP_ID - Your DingTalk corp ID');
  206. });
  207. // Handle graceful shutdown
  208. process.on('SIGINT', () => {
  209. console.log('\nShutting down server...');
  210. process.exit(0);
  211. });