@tradejs/app 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,216 @@
1
+ import bcrypt from 'bcryptjs';
2
+ import { NextResponse } from 'next/server';
3
+ import {
4
+ getUserSettings,
5
+ updateUserRecord,
6
+ type UserRecord,
7
+ type UserSettings,
8
+ } from '@tradejs/infra/userSettings';
9
+ import { getCurrentUserName } from '@app/lib/currentUser';
10
+
11
+ export const dynamic = 'force-dynamic';
12
+
13
+ type UpdateBody =
14
+ | {
15
+ section: 'bybit';
16
+ data?: {
17
+ apiKey?: string;
18
+ apiSecret?: string;
19
+ };
20
+ }
21
+ | {
22
+ section: 'token';
23
+ data?: {
24
+ token?: string;
25
+ };
26
+ }
27
+ | {
28
+ section: 'coinalyze';
29
+ data?: {
30
+ apiKey?: string;
31
+ };
32
+ }
33
+ | {
34
+ section: 'openai';
35
+ data?: {
36
+ apiKey?: string;
37
+ apiEndpoint?: string;
38
+ };
39
+ }
40
+ | {
41
+ section: 'telegram';
42
+ data?: {
43
+ botToken?: string;
44
+ chatId?: string;
45
+ };
46
+ }
47
+ | {
48
+ section: 'password';
49
+ data?: {
50
+ password?: string;
51
+ confirmPassword?: string;
52
+ };
53
+ };
54
+
55
+ const cleanText = (value: unknown): string =>
56
+ typeof value === 'string' ? value.trim() : '';
57
+
58
+ const cleanOptionalText = (value: unknown): string | undefined => {
59
+ if (typeof value !== 'string') {
60
+ return undefined;
61
+ }
62
+
63
+ const trimmed = value.trim();
64
+ return trimmed ? trimmed : undefined;
65
+ };
66
+
67
+ const maskSecret = (value: string) => {
68
+ const trimmed = cleanText(value);
69
+ if (!trimmed) {
70
+ return '';
71
+ }
72
+
73
+ return `${'*'.repeat(12)}${trimmed.slice(-4) || trimmed}`;
74
+ };
75
+
76
+ const toResponse = (settings: UserSettings) => ({
77
+ userName: settings.userName,
78
+ settings: {
79
+ bybit: {
80
+ apiKey: maskSecret(settings.BYBIT_API_KEY),
81
+ apiSecret: maskSecret(settings.BYBIT_API_SECRET),
82
+ },
83
+ token: maskSecret(settings.token),
84
+ coinalyze: {
85
+ apiKey: maskSecret(settings.COINALYZE_API_KEY),
86
+ },
87
+ openai: {
88
+ apiKey: maskSecret(settings.OPENAI_API_KEY),
89
+ apiEndpoint: settings.OPENAI_API_ENDPOINT,
90
+ },
91
+ telegram: {
92
+ botToken: maskSecret(settings.TG_BOT_TOKEN),
93
+ chatId: settings.TG_CHAT_ID,
94
+ },
95
+ },
96
+ });
97
+
98
+ const hasKeys = (patch: Partial<UserRecord>) => Object.keys(patch).length > 0;
99
+
100
+ export const GET = async () => {
101
+ const userName = await getCurrentUserName();
102
+ if (!userName) {
103
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
104
+ }
105
+
106
+ const settings = await getUserSettings(userName);
107
+ return NextResponse.json(toResponse(settings));
108
+ };
109
+
110
+ export const PATCH = async (request: Request) => {
111
+ const userName = await getCurrentUserName();
112
+ if (!userName) {
113
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
114
+ }
115
+
116
+ const body = (await request.json()) as UpdateBody | null;
117
+ if (!body || typeof body !== 'object' || !('section' in body)) {
118
+ return NextResponse.json({ error: 'Invalid payload' }, { status: 400 });
119
+ }
120
+
121
+ if (body.section === 'password') {
122
+ const password = String(body.data?.password || '');
123
+ const confirmPassword = String(body.data?.confirmPassword || '');
124
+
125
+ if (!password) {
126
+ return NextResponse.json(
127
+ { error: 'Password is required' },
128
+ { status: 400 },
129
+ );
130
+ }
131
+
132
+ if (password !== confirmPassword) {
133
+ return NextResponse.json(
134
+ { error: 'Password confirmation does not match' },
135
+ { status: 400 },
136
+ );
137
+ }
138
+
139
+ const passwordHash = await bcrypt.hash(password, 10);
140
+ await updateUserRecord(userName, { passwordHash });
141
+ const settings = await getUserSettings(userName);
142
+ return NextResponse.json(toResponse(settings));
143
+ }
144
+
145
+ if (body.section === 'bybit') {
146
+ const patch: Partial<UserRecord> = {};
147
+ const apiKey = cleanOptionalText(body.data?.apiKey);
148
+ const apiSecret = cleanOptionalText(body.data?.apiSecret);
149
+
150
+ if (apiKey) {
151
+ patch.BYBIT_API_KEY = apiKey;
152
+ }
153
+
154
+ if (apiSecret) {
155
+ patch.BYBIT_API_SECRET = apiSecret;
156
+ }
157
+
158
+ if (hasKeys(patch)) {
159
+ await updateUserRecord(userName, patch);
160
+ }
161
+ }
162
+
163
+ if (body.section === 'token') {
164
+ const token = cleanOptionalText(body.data?.token);
165
+
166
+ if (token) {
167
+ await updateUserRecord(userName, { token });
168
+ }
169
+ }
170
+
171
+ if (body.section === 'coinalyze') {
172
+ const apiKey = cleanOptionalText(body.data?.apiKey);
173
+
174
+ if (apiKey) {
175
+ await updateUserRecord(userName, { COINALYZE_API_KEY: apiKey });
176
+ }
177
+ }
178
+
179
+ if (body.section === 'openai') {
180
+ const patch: Partial<UserRecord> = {};
181
+ const apiKey = cleanOptionalText(body.data?.apiKey);
182
+ const apiEndpoint = cleanOptionalText(body.data?.apiEndpoint);
183
+
184
+ if (apiKey) {
185
+ patch.OPENAI_API_KEY = apiKey;
186
+ }
187
+
188
+ if (apiEndpoint) {
189
+ patch.OPENAI_API_ENDPOINT = apiEndpoint;
190
+ }
191
+
192
+ if (hasKeys(patch)) {
193
+ await updateUserRecord(userName, patch);
194
+ }
195
+ }
196
+
197
+ if (body.section === 'telegram') {
198
+ const patch: Partial<UserRecord> = {};
199
+ const botToken = cleanOptionalText(body.data?.botToken);
200
+
201
+ if (botToken) {
202
+ patch.TG_BOT_TOKEN = botToken;
203
+ }
204
+
205
+ if (body.data && 'chatId' in body.data) {
206
+ patch.TG_CHAT_ID = cleanText(body.data.chatId);
207
+ }
208
+
209
+ if (hasKeys(patch)) {
210
+ await updateUserRecord(userName, patch);
211
+ }
212
+ }
213
+
214
+ const settings = await getUserSettings(userName);
215
+ return NextResponse.json(toResponse(settings));
216
+ };