tsoft-cli 2.6.15 → 3.4.1
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.
- package/CHANGELOG.md +166 -0
- package/LICENSE +21 -0
- package/README.md +86 -53
- package/locales/en.json +323 -0
- package/locales/tr.json +323 -0
- package/package.json +41 -45
- package/src/api-client.js +217 -0
- package/src/commands/login.js +94 -0
- package/src/commands/logout.js +44 -0
- package/src/commands/org-switch.js +82 -0
- package/src/commands/theme-dev.js +424 -0
- package/src/commands/theme-init.js +162 -0
- package/src/commands/theme-publish.js +12 -0
- package/src/commands/theme-push.js +197 -0
- package/src/commands/theme-section.js +317 -0
- package/src/commands/theme-submit.js +14 -0
- package/src/commands/theme.js +474 -0
- package/src/commands/whoami.js +132 -0
- package/src/config.js +19 -0
- package/src/errors/error-mapper.js +158 -0
- package/src/i18n.js +84 -0
- package/src/index.js +179 -0
- package/src/output-mode.js +45 -0
- package/src/pkce.js +34 -0
- package/src/server.js +216 -0
- package/src/storage.js +488 -0
- package/src/ui/help-renderer.js +219 -0
- package/src/ui/prompt-wrapper.js +183 -0
- package/bin/run.js +0 -5
- package/build/enums.d.ts +0 -1
- package/build/enums.js +0 -7
- package/build/enums.js.map +0 -1
- package/build/index.d.ts +0 -5
- package/build/index.js +0 -10
- package/build/index.js.map +0 -1
package/src/server.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OAuth2 callback'i yakalamak için geçici HTTP server başlatır
|
|
6
|
+
* @param {number} port - Dinlenecek port
|
|
7
|
+
* @returns {Promise<{code: string, state: string, store: string}>}
|
|
8
|
+
*/
|
|
9
|
+
export function startCallbackServer(port) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const server = http.createServer((req, res) => {
|
|
12
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
13
|
+
|
|
14
|
+
if (url.pathname === '/callback') {
|
|
15
|
+
const code = url.searchParams.get('code');
|
|
16
|
+
const state = url.searchParams.get('state');
|
|
17
|
+
const store = url.searchParams.get('store');
|
|
18
|
+
const error = url.searchParams.get('error');
|
|
19
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
20
|
+
|
|
21
|
+
if (error) {
|
|
22
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
23
|
+
res.end(errorHtml(error, errorDescription));
|
|
24
|
+
server.close();
|
|
25
|
+
reject(new Error(`OAuth2 Error: ${error} - ${errorDescription || 'No description'}`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!code || !state || !store) {
|
|
30
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
31
|
+
res.end(errorHtml('invalid_request', 'code, state veya store parametresi eksik'));
|
|
32
|
+
server.close();
|
|
33
|
+
reject(new Error('Eksik parametreler: code, state veya store bulunamadı'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
38
|
+
res.end(successHtml());
|
|
39
|
+
server.close();
|
|
40
|
+
resolve({ code, state, store });
|
|
41
|
+
} else {
|
|
42
|
+
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
43
|
+
res.end('<h1>404 - Sayfa Bulunamadı</h1>');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
server.on('error', (err) => {
|
|
48
|
+
reject(new Error(`Server başlatılamadı: ${err.message}`));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
server.listen(port, () => {
|
|
52
|
+
console.log(`✓ Callback sunucusu başlatıldı (http://localhost:${port}/callback)`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function successHtml() {
|
|
58
|
+
return `
|
|
59
|
+
<!DOCTYPE html>
|
|
60
|
+
<html lang="tr">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset="UTF-8">
|
|
63
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
64
|
+
<title>Giriş Başarılı - tsoft CLI</title>
|
|
65
|
+
<style>
|
|
66
|
+
body {
|
|
67
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
align-items: center;
|
|
71
|
+
min-height: 100vh;
|
|
72
|
+
margin: 0;
|
|
73
|
+
background: #fff;
|
|
74
|
+
}
|
|
75
|
+
.container {
|
|
76
|
+
background: #fff;
|
|
77
|
+
padding: 3rem;
|
|
78
|
+
border-radius: 1rem;
|
|
79
|
+
border: 1px solid #e5e7eb;
|
|
80
|
+
text-align: center;
|
|
81
|
+
max-width: 400px;
|
|
82
|
+
}
|
|
83
|
+
.success-icon {
|
|
84
|
+
width: 64px;
|
|
85
|
+
height: 64px;
|
|
86
|
+
margin: 0 auto 1.5rem;
|
|
87
|
+
background: #10b981;
|
|
88
|
+
border-radius: 50%;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
}
|
|
93
|
+
.success-icon svg {
|
|
94
|
+
width: 32px;
|
|
95
|
+
height: 32px;
|
|
96
|
+
}
|
|
97
|
+
h1 {
|
|
98
|
+
color: #1f2937;
|
|
99
|
+
margin: 0 0 0.75rem;
|
|
100
|
+
font-size: 1.5rem;
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
}
|
|
103
|
+
p {
|
|
104
|
+
color: #6b7280;
|
|
105
|
+
margin: 0;
|
|
106
|
+
font-size: 0.938rem;
|
|
107
|
+
line-height: 1.6;
|
|
108
|
+
}
|
|
109
|
+
.note {
|
|
110
|
+
margin-top: 2rem;
|
|
111
|
+
padding: 0.875rem 1.25rem;
|
|
112
|
+
background: #f9fafb;
|
|
113
|
+
border-radius: 0.5rem;
|
|
114
|
+
font-size: 0.875rem;
|
|
115
|
+
color: #4b5563;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<div class="container">
|
|
121
|
+
<div class="success-icon"><svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></div>
|
|
122
|
+
<h1>Giriş Başarılı!</h1>
|
|
123
|
+
<p>Yetkilendirme işlemi tamamlandı.</p>
|
|
124
|
+
<p>Artık bu pencereyi kapatabilirsiniz.</p>
|
|
125
|
+
<div class="note">
|
|
126
|
+
Terminal'e dönün ve işleme devam edin.
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<script>
|
|
130
|
+
setTimeout(() => window.close(), 3000);
|
|
131
|
+
</script>
|
|
132
|
+
</body>
|
|
133
|
+
</html>
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function errorHtml(error, description) {
|
|
138
|
+
return `
|
|
139
|
+
<!DOCTYPE html>
|
|
140
|
+
<html lang="tr">
|
|
141
|
+
<head>
|
|
142
|
+
<meta charset="UTF-8">
|
|
143
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
144
|
+
<title>Hata - tsoft CLI</title>
|
|
145
|
+
<style>
|
|
146
|
+
body {
|
|
147
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
148
|
+
display: flex;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
align-items: center;
|
|
151
|
+
min-height: 100vh;
|
|
152
|
+
margin: 0;
|
|
153
|
+
background: #fff;
|
|
154
|
+
}
|
|
155
|
+
.container {
|
|
156
|
+
background: #fff;
|
|
157
|
+
padding: 3rem;
|
|
158
|
+
border-radius: 1rem;
|
|
159
|
+
border: 1px solid #e5e7eb;
|
|
160
|
+
text-align: center;
|
|
161
|
+
max-width: 400px;
|
|
162
|
+
}
|
|
163
|
+
.error-icon {
|
|
164
|
+
width: 64px;
|
|
165
|
+
height: 64px;
|
|
166
|
+
margin: 0 auto 1.5rem;
|
|
167
|
+
background: #ef4444;
|
|
168
|
+
border-radius: 50%;
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
}
|
|
173
|
+
.error-icon svg {
|
|
174
|
+
width: 32px;
|
|
175
|
+
height: 32px;
|
|
176
|
+
}
|
|
177
|
+
h1 {
|
|
178
|
+
color: #1f2937;
|
|
179
|
+
margin: 0 0 0.75rem;
|
|
180
|
+
font-size: 1.5rem;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
}
|
|
183
|
+
p {
|
|
184
|
+
color: #6b7280;
|
|
185
|
+
margin: 0.5rem 0;
|
|
186
|
+
font-size: 0.938rem;
|
|
187
|
+
line-height: 1.6;
|
|
188
|
+
}
|
|
189
|
+
.error-code {
|
|
190
|
+
margin-top: 1.5rem;
|
|
191
|
+
padding: 0.875rem 1.25rem;
|
|
192
|
+
background: #fef2f2;
|
|
193
|
+
border-radius: 0.5rem;
|
|
194
|
+
font-size: 0.875rem;
|
|
195
|
+
color: #991b1b;
|
|
196
|
+
font-family: monospace;
|
|
197
|
+
}
|
|
198
|
+
</style>
|
|
199
|
+
</head>
|
|
200
|
+
<body>
|
|
201
|
+
<div class="container">
|
|
202
|
+
<div class="error-icon"><svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></div>
|
|
203
|
+
<h1>Yetkilendirme Hatası</h1>
|
|
204
|
+
<p>${description || 'Bir hata oluştu'}</p>
|
|
205
|
+
<div class="error-code">
|
|
206
|
+
Hata Kodu: ${error}
|
|
207
|
+
</div>
|
|
208
|
+
<p style="margin-top: 2rem; font-size: 0.875rem;">
|
|
209
|
+
Terminal'e dönün ve tekrar deneyin.
|
|
210
|
+
</p>
|
|
211
|
+
</div>
|
|
212
|
+
</body>
|
|
213
|
+
</html>
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
|
package/src/storage.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import * as config from './config.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Store domain'i dizin adına uygun formata çevirir
|
|
8
|
+
* @param {string} domain - Store domain (örn: birtan.1isim.com)
|
|
9
|
+
* @returns {string} Normalized slug (örn: birtan-1isim-com)
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeStoreDomain(domain) {
|
|
12
|
+
return domain.replace(/\./g, '-').toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Store'a özel config dizinini döner (CWD bazlı)
|
|
17
|
+
* @param {string} store - Store domain
|
|
18
|
+
* @returns {string} Config dizin path'i
|
|
19
|
+
*/
|
|
20
|
+
export function getStoreConfigDir(store) {
|
|
21
|
+
const storeSlug = normalizeStoreDomain(store);
|
|
22
|
+
return path.join(process.cwd(), '.tsoft', storeSlug);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Store'a özel config dosya path'ini döner
|
|
27
|
+
* @param {string} store - Store domain
|
|
28
|
+
* @returns {string} Config dosya path'i
|
|
29
|
+
*/
|
|
30
|
+
export function getStoreConfigFile(store) {
|
|
31
|
+
return path.join(getStoreConfigDir(store), 'config.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Store'a özel .env dosya path'ini döner
|
|
36
|
+
* @param {string} store - Store domain
|
|
37
|
+
* @returns {string} .env dosya path'i
|
|
38
|
+
*/
|
|
39
|
+
export function getStoreEnvFile(store) {
|
|
40
|
+
return path.join(getStoreConfigDir(store), '.env');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Aktif store'u ayarlar
|
|
45
|
+
* @param {string} store - Store domain
|
|
46
|
+
* @returns {Promise<void>}
|
|
47
|
+
*/
|
|
48
|
+
export async function setActiveStore(store) {
|
|
49
|
+
const activeStoreFile = path.join(process.cwd(), '.tsoft', 'active-store.txt');
|
|
50
|
+
await fs.mkdir(path.dirname(activeStoreFile), { recursive: true });
|
|
51
|
+
await fs.writeFile(activeStoreFile, store, 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Aktif store'u okur
|
|
56
|
+
* @returns {Promise<string|null>} Store domain veya null
|
|
57
|
+
*/
|
|
58
|
+
export async function getActiveStore() {
|
|
59
|
+
try {
|
|
60
|
+
const activeStoreFile = path.join(process.cwd(), '.tsoft', 'active-store.txt');
|
|
61
|
+
return (await fs.readFile(activeStoreFile, 'utf-8')).trim();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.code === 'ENOENT') {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Authorization code'u access token'a çevirir
|
|
72
|
+
* @param {string} code - Authorization code
|
|
73
|
+
* @param {string} codeVerifier - PKCE code verifier
|
|
74
|
+
* @param {string} store - Store domain (callback'ten dönen store parametresi)
|
|
75
|
+
* @param {string} state - State (CSRF protection)
|
|
76
|
+
* @returns {Promise<object>} Token bilgileri
|
|
77
|
+
*/
|
|
78
|
+
export async function exchangeToken(code, codeVerifier, store, state) {
|
|
79
|
+
const tokenUrl = config.getTokenUrl(store);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Laravel Passport application/x-www-form-urlencoded formatında bekler
|
|
83
|
+
const params = new URLSearchParams();
|
|
84
|
+
params.append('grant_type', 'authorization_code');
|
|
85
|
+
params.append('client_id', config.CLIENT_ID);
|
|
86
|
+
params.append('redirect_uri', config.REDIRECT_URI);
|
|
87
|
+
params.append('code', code);
|
|
88
|
+
params.append('code_verifier', codeVerifier);
|
|
89
|
+
params.append('state', state);
|
|
90
|
+
|
|
91
|
+
const response = await axios.post(tokenUrl, params, {
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
94
|
+
'Accept': 'application/json',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Backend response: { status, message, data: { token, user } }
|
|
99
|
+
const responseData = response.data.data;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
accessToken: responseData.token.accessToken,
|
|
103
|
+
accessTokenExpiresAt: responseData.token.accessTokenExpiresAt,
|
|
104
|
+
refreshToken: responseData.token.refreshToken,
|
|
105
|
+
refreshTokenExpiresAt: responseData.token.refreshTokenExpiresAt,
|
|
106
|
+
tokenType: 'Bearer',
|
|
107
|
+
user: responseData.user,
|
|
108
|
+
store: store,
|
|
109
|
+
};
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error.response?.data) {
|
|
112
|
+
console.error('Token exchange hatası:', error.response.data);
|
|
113
|
+
}
|
|
114
|
+
if (error.response) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Token exchange hatası: ${error.response.status} - ${
|
|
117
|
+
error.response.data.error_description ||
|
|
118
|
+
error.response.data.message ||
|
|
119
|
+
'Bilinmeyen hata'
|
|
120
|
+
}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Token exchange hatası: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Refresh token ile yeni access token alır
|
|
129
|
+
* @param {string} refreshToken - Refresh token
|
|
130
|
+
* @param {string} store - Store domain
|
|
131
|
+
* @returns {Promise<object>} Yeni token bilgileri
|
|
132
|
+
*/
|
|
133
|
+
export async function refreshAccessToken(refreshToken, store) {
|
|
134
|
+
const refreshUrl = config.getRefreshUrl(store);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// Laravel Passport application/x-www-form-urlencoded formatında bekler
|
|
138
|
+
const params = new URLSearchParams();
|
|
139
|
+
params.append('grant_type', 'refresh_token');
|
|
140
|
+
params.append('refresh_token', refreshToken);
|
|
141
|
+
params.append('client_id', config.CLIENT_ID);
|
|
142
|
+
|
|
143
|
+
const response = await axios.post(refreshUrl, params, {
|
|
144
|
+
headers: {
|
|
145
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
146
|
+
'Accept': 'application/json',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Backend response: { status, message, data: { token, user } }
|
|
151
|
+
const responseData = response.data.data;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
accessToken: responseData.token.accessToken,
|
|
155
|
+
accessTokenExpiresAt: responseData.token.accessTokenExpiresAt,
|
|
156
|
+
refreshToken: responseData.token.refreshToken,
|
|
157
|
+
refreshTokenExpiresAt: responseData.token.refreshTokenExpiresAt,
|
|
158
|
+
tokenType: 'Bearer',
|
|
159
|
+
user: responseData.user,
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error.response?.data) {
|
|
163
|
+
console.error('Token refresh hatası:', error.response.data);
|
|
164
|
+
}
|
|
165
|
+
if (error.response) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Token refresh hatası: ${error.response.status} - ${
|
|
168
|
+
error.response.data.error_description ||
|
|
169
|
+
error.response.data.message ||
|
|
170
|
+
'Bilinmeyen hata'
|
|
171
|
+
}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
throw new Error(`Token refresh hatası: ${error.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Token'ı revoke eder
|
|
180
|
+
* @param {string} token - Access veya refresh token
|
|
181
|
+
* @param {string} store - Store domain
|
|
182
|
+
* @returns {Promise<void>}
|
|
183
|
+
*/
|
|
184
|
+
export async function revokeToken(token, store) {
|
|
185
|
+
const revokeUrl = config.getRevokeUrl(store);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Laravel Passport application/x-www-form-urlencoded formatında bekler
|
|
189
|
+
const params = new URLSearchParams();
|
|
190
|
+
params.append('token', token);
|
|
191
|
+
params.append('client_id', config.CLIENT_ID);
|
|
192
|
+
|
|
193
|
+
await axios.post(revokeUrl, params, {
|
|
194
|
+
headers: {
|
|
195
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
196
|
+
'Accept': 'application/json',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// Revoke hatalarını sessizce yoksay (token zaten geçersiz olabilir)
|
|
201
|
+
console.warn('Token revoke uyarısı:', error.message);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* .env dosyası oluşturur
|
|
207
|
+
* @param {object} tokens - Token ve user bilgileri
|
|
208
|
+
* @param {string} store - Store domain
|
|
209
|
+
* @returns {Promise<void>}
|
|
210
|
+
*/
|
|
211
|
+
async function writeEnvFile(tokens, store) {
|
|
212
|
+
const envContent = `# tsoft CLI - Auto-generated credentials
|
|
213
|
+
# Store: ${store}
|
|
214
|
+
# Generated: ${new Date().toISOString()}
|
|
215
|
+
|
|
216
|
+
TSOFT_ACCESS_TOKEN="${tokens.accessToken}"
|
|
217
|
+
TSOFT_REFRESH_TOKEN="${tokens.refreshToken}"
|
|
218
|
+
TSOFT_USER_ID=${tokens.user.id}
|
|
219
|
+
TSOFT_USER_NAME="${tokens.user.name}"
|
|
220
|
+
TSOFT_USER_SURNAME="${tokens.user.surname}"
|
|
221
|
+
TSOFT_USER_EMAIL="${tokens.user.email}"
|
|
222
|
+
TSOFT_STORE_DOMAIN="${store}"
|
|
223
|
+
TSOFT_USER_VERSION="${tokens.user.version}"
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
const envFile = getStoreEnvFile(store);
|
|
227
|
+
await fs.writeFile(envFile, envContent, 'utf-8');
|
|
228
|
+
await fs.chmod(envFile, 0o600); // rw-------
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Token'ları local config dosyasına kaydeder
|
|
233
|
+
* @param {object} tokens - Token bilgileri
|
|
234
|
+
* @param {string} store - Store domain
|
|
235
|
+
* @returns {Promise<void>}
|
|
236
|
+
*/
|
|
237
|
+
export async function saveTokens(tokens, store) {
|
|
238
|
+
const configDir = getStoreConfigDir(store);
|
|
239
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
240
|
+
|
|
241
|
+
const configData = {
|
|
242
|
+
accessToken: tokens.accessToken,
|
|
243
|
+
accessTokenExpiresAt: tokens.accessTokenExpiresAt,
|
|
244
|
+
refreshToken: tokens.refreshToken,
|
|
245
|
+
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
|
|
246
|
+
tokenType: tokens.tokenType,
|
|
247
|
+
store: store,
|
|
248
|
+
user: {
|
|
249
|
+
id: tokens.user.id,
|
|
250
|
+
name: tokens.user.name,
|
|
251
|
+
surname: tokens.user.surname,
|
|
252
|
+
email: tokens.user.email,
|
|
253
|
+
hasTwoFactor: tokens.user.hasTwoFactor,
|
|
254
|
+
hasRestrictedAccess: tokens.user.hasRestrictedAccess,
|
|
255
|
+
version: tokens.user.version,
|
|
256
|
+
},
|
|
257
|
+
updatedAt: new Date().toISOString(),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const configFile = getStoreConfigFile(store);
|
|
261
|
+
await fs.writeFile(configFile, JSON.stringify(configData, null, 2), 'utf-8');
|
|
262
|
+
await fs.chmod(configFile, 0o600);
|
|
263
|
+
|
|
264
|
+
// .env dosyasını da oluştur
|
|
265
|
+
await writeEnvFile(tokens, store);
|
|
266
|
+
|
|
267
|
+
// Aktif store'u ayarla
|
|
268
|
+
await setActiveStore(store);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Local config dosyasından token'ları yükler
|
|
273
|
+
* @param {string} [store] - Store domain (opsiyonel, belirtilmezse active store kullanılır)
|
|
274
|
+
* @returns {Promise<object|null>} Token bilgileri veya null
|
|
275
|
+
*/
|
|
276
|
+
export async function loadTokens(store = null) {
|
|
277
|
+
if (!store) {
|
|
278
|
+
store = await getActiveStore();
|
|
279
|
+
if (!store) {
|
|
280
|
+
return null; // Aktif store yok
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const configFile = getStoreConfigFile(store);
|
|
286
|
+
const data = await fs.readFile(configFile, 'utf-8');
|
|
287
|
+
return JSON.parse(data);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error.code === 'ENOENT') {
|
|
290
|
+
return null; // Dosya yok
|
|
291
|
+
}
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Token'ların geçerliliğini kontrol eder
|
|
298
|
+
* @param {object} tokens - Token bilgileri
|
|
299
|
+
* @returns {boolean} Token geçerli mi?
|
|
300
|
+
*/
|
|
301
|
+
export function isTokenValid(tokens) {
|
|
302
|
+
if (!tokens || !tokens.accessToken || !tokens.accessTokenExpiresAt) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const expiresAt = new Date(tokens.accessTokenExpiresAt);
|
|
307
|
+
const now = new Date();
|
|
308
|
+
|
|
309
|
+
// 5 dakika önce expire olacaksa yenile
|
|
310
|
+
const bufferTime = 5 * 60 * 1000; // 5 dakika
|
|
311
|
+
return expiresAt.getTime() - now.getTime() > bufferTime;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Refresh token'ın geçerliliğini kontrol eder
|
|
316
|
+
* @param {object} tokens - Token bilgileri
|
|
317
|
+
* @returns {boolean} Refresh token geçerli mi?
|
|
318
|
+
*/
|
|
319
|
+
export function isRefreshTokenValid(tokens) {
|
|
320
|
+
if (!tokens || !tokens.refreshToken || !tokens.refreshTokenExpiresAt) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const expiresAt = new Date(tokens.refreshTokenExpiresAt);
|
|
325
|
+
const now = new Date();
|
|
326
|
+
|
|
327
|
+
return expiresAt.getTime() > now.getTime();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Access token geçerliliğini kontrol eder, gerekirse refresh eder
|
|
332
|
+
* @param {object} tokens - Token bilgileri
|
|
333
|
+
* @returns {Promise<object>} Güncel token bilgileri
|
|
334
|
+
* @throws {Error} RELOGIN_REQUIRED - Refresh token da expire olduysa
|
|
335
|
+
*/
|
|
336
|
+
export async function ensureValidToken(tokens) {
|
|
337
|
+
if (!tokens) {
|
|
338
|
+
throw new Error('RELOGIN_REQUIRED');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Access token geçerliyse direkt dön
|
|
342
|
+
if (isTokenValid(tokens)) {
|
|
343
|
+
return tokens;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Refresh token geçerliyse refresh et
|
|
347
|
+
if (isRefreshTokenValid(tokens)) {
|
|
348
|
+
const newTokens = await refreshAccessToken(tokens.refreshToken, tokens.store);
|
|
349
|
+
await saveTokens(newTokens, tokens.store);
|
|
350
|
+
return await loadTokens(tokens.store);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Her ikisi de geçersizse, otomatik re-login gerekiyor
|
|
354
|
+
throw new Error('RELOGIN_REQUIRED');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Store'un config dosyalarını siler (logout için)
|
|
359
|
+
* @param {string} [store] - Store domain (opsiyonel, belirtilmezse active store kullanılır)
|
|
360
|
+
* @returns {Promise<void>}
|
|
361
|
+
*/
|
|
362
|
+
export async function deleteConfig(store = null) {
|
|
363
|
+
if (!store) {
|
|
364
|
+
store = await getActiveStore();
|
|
365
|
+
if (!store) {
|
|
366
|
+
return; // Aktif store yok, bir şey yapma
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const configDir = getStoreConfigDir(store);
|
|
372
|
+
await fs.rm(configDir, { recursive: true, force: true });
|
|
373
|
+
|
|
374
|
+
// Active store dosyasını da sil
|
|
375
|
+
const activeStoreFile = path.join(process.cwd(), '.tsoft', 'active-store.txt');
|
|
376
|
+
await fs.unlink(activeStoreFile).catch(() => {}); // Hata varsa sessizce yoksay
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (error.code !== 'ENOENT') {
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Token'ın ne kadar süre geçerli olduğunu döner
|
|
386
|
+
* @param {object} tokens - Token bilgileri
|
|
387
|
+
* @returns {string} Kalan süre (human-readable)
|
|
388
|
+
*/
|
|
389
|
+
export function getTokenExpiry(tokens) {
|
|
390
|
+
if (!tokens || !tokens.accessTokenExpiresAt) {
|
|
391
|
+
return 'Bilinmiyor';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const expiresAt = new Date(tokens.accessTokenExpiresAt);
|
|
395
|
+
const now = new Date();
|
|
396
|
+
const diff = expiresAt.getTime() - now.getTime();
|
|
397
|
+
|
|
398
|
+
if (diff <= 0) {
|
|
399
|
+
return 'Süresi dolmuş';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
403
|
+
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
404
|
+
|
|
405
|
+
if (hours > 0) {
|
|
406
|
+
return `${hours} saat ${minutes} dakika`;
|
|
407
|
+
}
|
|
408
|
+
return `${minutes} dakika`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Türkçe karakterleri URL dostu hale getirir
|
|
413
|
+
* @param {string} text - Dönüştürülecek metin
|
|
414
|
+
* @returns {string} Sluglaştırılmış metin
|
|
415
|
+
*/
|
|
416
|
+
export function slugify(text) {
|
|
417
|
+
const trMap = {
|
|
418
|
+
'ç': 'c', 'Ç': 'c',
|
|
419
|
+
'ğ': 'g', 'Ğ': 'g',
|
|
420
|
+
'ı': 'i', 'İ': 'i',
|
|
421
|
+
'ö': 'o', 'Ö': 'o',
|
|
422
|
+
'ş': 's', 'Ş': 's',
|
|
423
|
+
'ü': 'u', 'Ü': 'u',
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
return text
|
|
427
|
+
.split('')
|
|
428
|
+
.map(char => trMap[char] || char)
|
|
429
|
+
.join('')
|
|
430
|
+
.toLowerCase()
|
|
431
|
+
.trim()
|
|
432
|
+
.replace(/[^\w\s-]/g, '')
|
|
433
|
+
.replace(/[\s_-]+/g, '-')
|
|
434
|
+
.replace(/^-+|-+$/g, '');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Aktif temayı ayarlar
|
|
439
|
+
* @param {string} theme - Tema adı
|
|
440
|
+
* @returns {Promise<void>}
|
|
441
|
+
*/
|
|
442
|
+
export async function setActiveTheme(theme) {
|
|
443
|
+
const activeThemeFile = path.join(process.cwd(), '.tsoft', 'active-theme.txt');
|
|
444
|
+
await fs.mkdir(path.dirname(activeThemeFile), { recursive: true });
|
|
445
|
+
await fs.writeFile(activeThemeFile, theme, 'utf-8');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Aktif temayı okur
|
|
450
|
+
* @returns {Promise<string|null>} Tema adı veya null
|
|
451
|
+
*/
|
|
452
|
+
export async function getActiveTheme() {
|
|
453
|
+
try {
|
|
454
|
+
const activeThemeFile = path.join(process.cwd(), '.tsoft', 'active-theme.txt');
|
|
455
|
+
return (await fs.readFile(activeThemeFile, 'utf-8')).trim();
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (error.code === 'ENOENT') {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Tema slug'ından UUID/folder adını bulur
|
|
466
|
+
* @param {string} themeSlug - Tema slug (örn: 'my-theme')
|
|
467
|
+
* @returns {Promise<Object>} {uuid, name, version, active}
|
|
468
|
+
*/
|
|
469
|
+
export async function getThemeUuidBySlug(themeSlug) {
|
|
470
|
+
const { createApiClient } = await import('./api-client.js');
|
|
471
|
+
const apiClient = await createApiClient();
|
|
472
|
+
const response = await apiClient.get('/theme');
|
|
473
|
+
const themes = response.data || [];
|
|
474
|
+
|
|
475
|
+
const theme = themes.find(t => slugify(t.name) === themeSlug);
|
|
476
|
+
|
|
477
|
+
if (!theme) {
|
|
478
|
+
throw new Error(`Tema bulunamadı: ${themeSlug}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
uuid: theme.theme_folder,
|
|
483
|
+
name: theme.name,
|
|
484
|
+
version: theme.version,
|
|
485
|
+
active: theme.active
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|