@@ -9,6 +9,10 @@ import {
99 GIST_BACKUP_FILE_NAME ,
1010 GIST_BACKUP_KEY ,
1111 SETTINGS_KEY ,
12+ TOKENS_KEY ,
13+ FILES_KEY ,
14+ COLLECTIONS_KEY ,
15+ SUBS_KEY ,
1216} from '@/constants' ;
1317import { InternalServerError , RequestInvalidError } from '@/restful/errors' ;
1418import Gist from '@/utils/gist' ;
@@ -20,36 +24,7 @@ export default function register($app) {
2024 $app . get ( '/api/utils/env' , getEnv ) ; // get runtime environment
2125 $app . get ( '/api/utils/backup' , gistBackup ) ; // gist backup actions
2226 $app . get ( '/api/utils/refresh' , refresh ) ;
23- $app . post ( '/api/jwt' , ( req , res ) => {
24- if ( ! ENV ( ) . isNode ) {
25- return failed (
26- res ,
27- new RequestInvalidError (
28- 'INVALID_ENV' ,
29- `This endpoint is only available in Node.js environment` ,
30- ) ,
31- ) ;
32- }
33- try {
34- const { payload, options } = req . body ;
35- const jwt = eval ( `require("jsonwebtoken")` ) ;
36- const secret = eval ( 'process.env.SUB_STORE_FRONTEND_BACKEND_PATH' ) ;
37- const token = jwt . sign ( payload , secret , options ) ;
38- return success ( res , {
39- token,
40- secret,
41- } ) ;
42- } catch ( e ) {
43- return failed (
44- res ,
45- new InternalServerError (
46- 'JWT_SIGN_FAILED' ,
47- `Failed to sign JWT token` ,
48- `Reason: ${ e . message ?? e } ` ,
49- ) ,
50- ) ;
51- }
52- } ) ;
27+ $app . post ( '/api/token' , signToken ) ;
5328
5429 // Storage management
5530 $app . route ( '/api/storage' )
@@ -95,9 +70,151 @@ export default function register($app) {
9570}
9671
9772function getEnv ( req , res ) {
73+ if ( req . query . share ) {
74+ env . feature . share = true ;
75+ }
9876 success ( res , env ) ;
9977}
10078
79+ async function signToken ( req , res ) {
80+ if ( ! ENV ( ) . isNode ) {
81+ return failed (
82+ res ,
83+ new RequestInvalidError (
84+ 'INVALID_ENV' ,
85+ `This endpoint is only available in Node.js environment` ,
86+ ) ,
87+ ) ;
88+ }
89+ try {
90+ const { payload, options } = req . body ;
91+ const ms = eval ( `require("ms")` ) ;
92+ let token = payload ?. token ;
93+ if ( token != null ) {
94+ if ( typeof token !== 'string' || token . length < 1 ) {
95+ return failed (
96+ res ,
97+ new RequestInvalidError (
98+ 'INVALID_CUSTOM_TOKEN' ,
99+ `Invalid custom token: ${ token } ` ,
100+ ) ,
101+ ) ;
102+ }
103+ const tokens = $ . read ( TOKENS_KEY ) || [ ] ;
104+ if ( tokens . find ( ( t ) => t . token === token ) ) {
105+ return failed (
106+ res ,
107+ new RequestInvalidError (
108+ 'DUPLICATE_TOKEN' ,
109+ `Token ${ token } already exists` ,
110+ ) ,
111+ ) ;
112+ }
113+ }
114+ const type = payload ?. type ;
115+ const name = payload ?. name ;
116+ if ( ! type || ! name )
117+ return failed (
118+ res ,
119+ new RequestInvalidError (
120+ 'INVALID_PAYLOAD' ,
121+ `payload type and name are required` ,
122+ ) ,
123+ ) ;
124+ if ( type === 'col' ) {
125+ const collections = $ . read ( COLLECTIONS_KEY ) || [ ] ;
126+ const collection = collections . find ( ( c ) => c . name === name ) ;
127+ if ( ! collection )
128+ return failed (
129+ res ,
130+ new RequestInvalidError (
131+ 'INVALID_COLLECTION' ,
132+ `collection ${ name } not found` ,
133+ ) ,
134+ ) ;
135+ } else if ( type === 'file' ) {
136+ const files = $ . read ( FILES_KEY ) || [ ] ;
137+ const file = files . find ( ( f ) => f . name === name ) ;
138+ if ( ! file )
139+ return failed (
140+ res ,
141+ new RequestInvalidError (
142+ 'INVALID_FILE' ,
143+ `file ${ name } not found` ,
144+ ) ,
145+ ) ;
146+ } else if ( type === 'sub' ) {
147+ const subs = $ . read ( SUBS_KEY ) || [ ] ;
148+ const sub = subs . find ( ( s ) => s . name === name ) ;
149+ if ( ! sub )
150+ return failed (
151+ res ,
152+ new RequestInvalidError (
153+ 'INVALID_SUB' ,
154+ `sub ${ name } not found` ,
155+ ) ,
156+ ) ;
157+ } else {
158+ return failed (
159+ res ,
160+ new RequestInvalidError (
161+ 'INVALID_TYPE' ,
162+ `type ${ name } not supported` ,
163+ ) ,
164+ ) ;
165+ }
166+ let expiresIn = options ?. expiresIn ;
167+ if ( options ?. expiresIn != null ) {
168+ expiresIn = ms ( options . expiresIn ) ;
169+ if ( expiresIn == null || isNaN ( expiresIn ) || expiresIn <= 0 ) {
170+ return failed (
171+ res ,
172+ new RequestInvalidError (
173+ 'INVALID_EXPIRES_IN' ,
174+ `Invalid expiresIn option: ${ options . expiresIn } ` ,
175+ ) ,
176+ ) ;
177+ }
178+ }
179+ const secret = eval ( 'process.env.SUB_STORE_FRONTEND_BACKEND_PATH' ) ;
180+ const nanoid = eval ( `require("nanoid")` ) ;
181+ const tokens = $ . read ( TOKENS_KEY ) || [ ] ;
182+ // const now = Date.now();
183+ // for (const key in tokens) {
184+ // const token = tokens[key];
185+ // if (token.exp != null || token.exp < now) {
186+ // delete tokens[key];
187+ // }
188+ // }
189+ if ( ! token ) {
190+ do {
191+ token = nanoid . customAlphabet ( nanoid . urlAlphabet ) ( ) ;
192+ } while ( tokens . find ( ( t ) => t . token === token ) ) ;
193+ }
194+ tokens . push ( {
195+ ...payload ,
196+ token,
197+ createdAt : Date . now ( ) ,
198+ expiresIn : expiresIn > 0 ? options ?. expiresIn : undefined ,
199+ exp : expiresIn > 0 ? Date . now ( ) + expiresIn : undefined ,
200+ } ) ;
201+
202+ $ . write ( tokens , TOKENS_KEY ) ;
203+ return success ( res , {
204+ token,
205+ secret,
206+ } ) ;
207+ } catch ( e ) {
208+ return failed (
209+ res ,
210+ new InternalServerError (
211+ 'TOKEN_SIGN_FAILED' ,
212+ `Failed to sign token` ,
213+ `Reason: ${ e . message ?? e } ` ,
214+ ) ,
215+ ) ;
216+ }
217+ }
101218async function refresh ( _ , res ) {
102219 // 1. get GitHub avatar and artifact store
103220 await updateAvatar ( ) ;
0 commit comments