nuxt-studio 1.0.0-alpha.0 → 1.0.0-alpha.2
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/README.md +11 -9
- package/dist/app/en-CeGQDKH5.js +7 -0
- package/dist/app/en-D0jd_r21.js +7 -0
- package/dist/app/en-XbfkDcMn.js +7 -0
- package/dist/app/fr-CbcFrmHX.js +7 -0
- package/dist/app/fr-Ct4HO5e1.js +7 -0
- package/dist/app/fr-ILHmVnG9.js +7 -0
- package/dist/app/main.d.ts +83 -33
- package/dist/app/main.js +34187 -30512
- package/dist/app/service-worker.js +29 -5
- package/dist/app/shared-B6H2zqDG.js +8 -0
- package/dist/app/{shared-D04791PA.js → shared-DLETHbvn.js} +2311 -2307
- package/dist/app/{shared-DI6F-ifc.js → shared-Dvm_mWyG.js} +3120 -3132
- package/dist/app/shared.d.ts +1 -20
- package/dist/app/shared.js +2 -4
- package/dist/module/module.d.mts +70 -27
- package/dist/module/module.json +1 -1
- package/dist/module/module.mjs +101 -58
- package/dist/module/runtime/host.dev.js +19 -12
- package/dist/module/runtime/host.js +124 -61
- package/dist/module/runtime/plugins/studio.client.js +1 -1
- package/dist/module/runtime/server/routes/admin.js +11 -2
- package/dist/module/runtime/server/routes/auth/github.get.d.ts +4 -4
- package/dist/module/runtime/server/routes/auth/github.get.js +17 -9
- package/dist/module/runtime/server/routes/auth/gitlab.get.d.ts +55 -0
- package/dist/module/runtime/server/routes/auth/gitlab.get.js +157 -0
- package/dist/module/runtime/server/routes/auth/session.delete.js +2 -1
- package/dist/module/runtime/server/routes/auth/session.get.js +4 -1
- package/dist/module/runtime/server/routes/dev/content/[...path].js +0 -11
- package/dist/module/runtime/server/routes/dev/public/[...path].d.ts +1 -0
- package/dist/module/runtime/server/routes/dev/public/[...path].js +2 -0
- package/dist/module/runtime/types/content.d.ts +6 -0
- package/dist/module/runtime/types/content.js +7 -0
- package/dist/module/runtime/utils/activation.js +16 -5
- package/dist/module/runtime/utils/collection.d.ts +10 -17
- package/dist/module/runtime/utils/collection.js +32 -92
- package/dist/module/runtime/utils/document.d.ts +15 -0
- package/dist/module/runtime/utils/document.js +288 -0
- package/dist/module/runtime/utils/media.d.ts +1 -0
- package/dist/module/runtime/utils/media.js +5 -0
- package/dist/module/runtime/utils/object.d.ts +4 -0
- package/dist/module/runtime/utils/object.js +27 -0
- package/dist/module/runtime/utils/source.d.ts +11 -0
- package/dist/module/runtime/utils/source.js +36 -0
- package/license.md +9 -0
- package/package.json +7 -6
- package/dist/app/shared-R5zYJ3Dl.js +0 -25803
- package/dist/app/utils-DI6F-ifc.js +0 -25774
- package/dist/app/utils.d.ts +0 -22
- package/dist/app/utils.js +0 -6
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { ref } from "vue";
|
|
2
2
|
import { ensure } from "./utils/ensure.js";
|
|
3
|
-
import { getCollectionByFilePath, generateIdFromFsPath,
|
|
3
|
+
import { getCollectionByFilePath, generateIdFromFsPath, generateRecordDeletion, generateRecordInsert, generateFsPathFromId, getCollectionById } from "./utils/collection.js";
|
|
4
|
+
import { normalizeDocument, isDocumentMatchingContent, generateDocumentFromContent, generateContentFromDocument, areDocumentsEqual, pickReservedKeysFromDocument, removeReservedKeysFromDocument } from "./utils/document.js";
|
|
4
5
|
import { kebabCase } from "scule";
|
|
5
|
-
import {
|
|
6
|
-
import { clearError, getAppManifest, queryCollection, queryCollectionItemSurroundings, queryCollectionNavigation, queryCollectionSearchSections } from "#imports";
|
|
6
|
+
import { clearError, getAppManifest, queryCollection, queryCollectionItemSurroundings, queryCollectionNavigation, queryCollectionSearchSections, useRuntimeConfig } from "#imports";
|
|
7
7
|
import { collections } from "#content/preview";
|
|
8
8
|
import { publicAssetsStorage } from "#build/studio-public-assets";
|
|
9
9
|
import { useHostMeta } from "./composables/useMeta.js";
|
|
10
|
+
import { generateIdFromFsPath as generateMediaIdFromFsPath } from "./utils/media.js";
|
|
11
|
+
import { getCollectionSourceById } from "./utils/source.js";
|
|
12
|
+
const serviceWorkerVersion = "v0.0.2";
|
|
10
13
|
function getSidebarWidth() {
|
|
11
14
|
let sidebarWidth = 440;
|
|
12
15
|
if (typeof window !== "undefined" && window.localStorage) {
|
|
@@ -83,7 +86,8 @@ export function useStudioHost(user, repository) {
|
|
|
83
86
|
const host = {
|
|
84
87
|
meta: {
|
|
85
88
|
dev: false,
|
|
86
|
-
components: () => meta.componentsMeta.value
|
|
89
|
+
components: () => meta.componentsMeta.value,
|
|
90
|
+
defaultLocale: useRuntimeConfig().public.studio.i18n?.defaultLocale || "en"
|
|
87
91
|
},
|
|
88
92
|
on: {
|
|
89
93
|
routeChange: (fn) => {
|
|
@@ -115,6 +119,9 @@ export function useStudioHost(user, repository) {
|
|
|
115
119
|
documentUpdate: (_fn) => {
|
|
116
120
|
},
|
|
117
121
|
mediaUpdate: (_fn) => {
|
|
122
|
+
},
|
|
123
|
+
requestDocumentEdit: (fn) => {
|
|
124
|
+
useNuxtApp().hooks.hook("studio:document:edit", fn);
|
|
118
125
|
}
|
|
119
126
|
},
|
|
120
127
|
ui: {
|
|
@@ -152,77 +159,116 @@ export function useStudioHost(user, repository) {
|
|
|
152
159
|
}
|
|
153
160
|
},
|
|
154
161
|
repository,
|
|
155
|
-
// New API
|
|
156
162
|
user: {
|
|
157
163
|
get: () => user
|
|
158
164
|
},
|
|
159
165
|
document: {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
166
|
+
db: {
|
|
167
|
+
get: async (fsPath) => {
|
|
168
|
+
const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections());
|
|
169
|
+
if (!collectionInfo) {
|
|
170
|
+
throw new Error(`Collection not found for fsPath: ${fsPath}`);
|
|
171
|
+
}
|
|
172
|
+
const id = generateIdFromFsPath(fsPath, collectionInfo);
|
|
173
|
+
const item = await useContentCollectionQuery(collectionInfo.name).where("id", "=", id).first();
|
|
174
|
+
if (!item) {
|
|
175
|
+
return void 0;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
...item,
|
|
179
|
+
fsPath
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
list: async () => {
|
|
183
|
+
const collections2 = Object.values(useContentCollections()).filter((collection) => collection.name !== "info");
|
|
184
|
+
const documentsByCollection = await Promise.all(collections2.map(async (collection) => {
|
|
185
|
+
const documents = await useContentCollectionQuery(collection.name).all();
|
|
186
|
+
return documents.map((document2) => {
|
|
187
|
+
const source = getCollectionSourceById(document2.id, collection.source);
|
|
188
|
+
const fsPath = generateFsPathFromId(document2.id, source);
|
|
189
|
+
return {
|
|
190
|
+
...document2,
|
|
191
|
+
fsPath
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}));
|
|
195
|
+
return documentsByCollection.flat();
|
|
196
|
+
},
|
|
197
|
+
create: async (fsPath, content) => {
|
|
198
|
+
const existingDocument = await host.document.db.get(fsPath);
|
|
199
|
+
if (existingDocument) {
|
|
200
|
+
throw new Error(`Cannot create document with fsPath "${fsPath}": document already exists.`);
|
|
201
|
+
}
|
|
202
|
+
const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections());
|
|
203
|
+
if (!collectionInfo) {
|
|
204
|
+
throw new Error(`Collection not found for fsPath: ${fsPath}`);
|
|
205
|
+
}
|
|
206
|
+
const id = generateIdFromFsPath(fsPath, collectionInfo);
|
|
207
|
+
const document2 = await generateDocumentFromContent(id, content);
|
|
208
|
+
const normalizedDocument = normalizeDocument(id, collectionInfo, document2);
|
|
209
|
+
await host.document.db.upsert(fsPath, normalizedDocument);
|
|
210
|
+
return {
|
|
211
|
+
...normalizedDocument,
|
|
212
|
+
fsPath
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
upsert: async (fsPath, document2) => {
|
|
216
|
+
const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections());
|
|
217
|
+
if (!collectionInfo) {
|
|
218
|
+
throw new Error(`Collection not found for fsPath: ${fsPath}`);
|
|
219
|
+
}
|
|
220
|
+
const id = generateIdFromFsPath(fsPath, collectionInfo);
|
|
221
|
+
const normalizedDocument = normalizeDocument(id, collectionInfo, document2);
|
|
222
|
+
await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordDeletion(collectionInfo, id));
|
|
223
|
+
await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordInsert(collectionInfo, normalizedDocument));
|
|
224
|
+
},
|
|
225
|
+
delete: async (fsPath) => {
|
|
226
|
+
const collection = getCollectionByFilePath(fsPath, useContentCollections());
|
|
227
|
+
if (!collection) {
|
|
228
|
+
throw new Error(`Collection not found for fsPath: ${fsPath}`);
|
|
229
|
+
}
|
|
230
|
+
const id = generateIdFromFsPath(fsPath, collection);
|
|
231
|
+
await useContentDatabaseAdapter(collection.name).exec(generateRecordDeletion(collection, id));
|
|
181
232
|
}
|
|
182
|
-
const document2 = await generateDocumentFromContent(id, content);
|
|
183
|
-
const collectionDocument = createCollectionDocument(collectionInfo, id, document2);
|
|
184
|
-
await host.document.upsert(id, collectionDocument);
|
|
185
|
-
return collectionDocument;
|
|
186
|
-
},
|
|
187
|
-
upsert: async (id, document2) => {
|
|
188
|
-
id = id.replace(/:/g, "/");
|
|
189
|
-
const collection = getCollectionInfo(id, useContentCollections()).collection;
|
|
190
|
-
const doc = createCollectionDocument(collection, id, document2);
|
|
191
|
-
await useContentDatabaseAdapter(collection.name).exec(generateRecordDeletion(collection, id));
|
|
192
|
-
await useContentDatabaseAdapter(collection.name).exec(generateRecordInsert(collection, doc));
|
|
193
233
|
},
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
234
|
+
utils: {
|
|
235
|
+
areEqual: (document1, document2) => areDocumentsEqual(document1, document2),
|
|
236
|
+
isMatchingContent: async (content, document2) => isDocumentMatchingContent(content, document2),
|
|
237
|
+
pickReservedKeys: (document2) => pickReservedKeysFromDocument(document2),
|
|
238
|
+
removeReservedKeys: (document2) => removeReservedKeysFromDocument(document2),
|
|
239
|
+
detectActives: () => {
|
|
240
|
+
const wrappers = document.querySelectorAll("[data-content-id]");
|
|
241
|
+
return Array.from(wrappers).map((wrapper) => {
|
|
242
|
+
const id = wrapper.getAttribute("data-content-id");
|
|
243
|
+
const title = id.split(/[/:]/).pop() || id;
|
|
244
|
+
const collection = getCollectionById(id, useContentCollections());
|
|
245
|
+
const source = getCollectionSourceById(id, collection.source);
|
|
246
|
+
const fsPath = generateFsPathFromId(id, source);
|
|
247
|
+
return {
|
|
248
|
+
fsPath,
|
|
249
|
+
title
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
|
198
253
|
},
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const id = wrapper.getAttribute("data-content-id");
|
|
203
|
-
const title = id.split(/[/:]/).pop() || id;
|
|
204
|
-
return {
|
|
205
|
-
id,
|
|
206
|
-
title
|
|
207
|
-
};
|
|
208
|
-
});
|
|
254
|
+
generate: {
|
|
255
|
+
documentFromContent: async (id, content) => generateDocumentFromContent(id, content),
|
|
256
|
+
contentFromDocument: async (document2) => generateContentFromDocument(document2)
|
|
209
257
|
}
|
|
210
258
|
},
|
|
211
259
|
media: {
|
|
212
|
-
get: async (
|
|
213
|
-
return await publicAssetsStorage.getItem(
|
|
214
|
-
},
|
|
215
|
-
getFileSystemPath: (id) => {
|
|
216
|
-
return id.split("/").slice(1).join("/");
|
|
260
|
+
get: async (fsPath) => {
|
|
261
|
+
return await publicAssetsStorage.getItem(generateMediaIdFromFsPath(fsPath));
|
|
217
262
|
},
|
|
218
263
|
list: async () => {
|
|
219
264
|
return await Promise.all(await publicAssetsStorage.getKeys().then((keys) => keys.map((key) => publicAssetsStorage.getItem(key))));
|
|
220
265
|
},
|
|
221
|
-
upsert: async (
|
|
222
|
-
|
|
266
|
+
upsert: async (fsPath, media) => {
|
|
267
|
+
const id = generateMediaIdFromFsPath(fsPath);
|
|
268
|
+
await publicAssetsStorage.setItem(generateMediaIdFromFsPath(fsPath), { ...media, id });
|
|
223
269
|
},
|
|
224
|
-
delete: async (
|
|
225
|
-
await publicAssetsStorage.removeItem(
|
|
270
|
+
delete: async (fsPath) => {
|
|
271
|
+
await publicAssetsStorage.removeItem(generateMediaIdFromFsPath(fsPath));
|
|
226
272
|
}
|
|
227
273
|
},
|
|
228
274
|
app: {
|
|
@@ -248,10 +294,27 @@ export function useStudioHost(user, repository) {
|
|
|
248
294
|
isMounted.value = true;
|
|
249
295
|
}).then(() => {
|
|
250
296
|
if ("serviceWorker" in navigator) {
|
|
251
|
-
navigator.serviceWorker.register(
|
|
297
|
+
navigator.serviceWorker.register(`/sw.js?${serviceWorkerVersion}`);
|
|
252
298
|
}
|
|
253
299
|
return meta.fetch();
|
|
254
300
|
});
|
|
301
|
+
document.body.addEventListener("dblclick", (event) => {
|
|
302
|
+
let element = event.target;
|
|
303
|
+
while (element) {
|
|
304
|
+
if (element.getAttribute("data-content-id")) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
element = element.parentElement;
|
|
308
|
+
}
|
|
309
|
+
if (element) {
|
|
310
|
+
const id = element.getAttribute("data-content-id");
|
|
311
|
+
const collection = getCollectionById(id, useContentCollections());
|
|
312
|
+
const source = getCollectionSourceById(id, collection.source);
|
|
313
|
+
const fsPath = generateFsPathFromId(id, source);
|
|
314
|
+
useNuxtApp().hooks.callHook("studio:document:edit", fsPath);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
host.ui.updateStyles();
|
|
255
318
|
})();
|
|
256
319
|
return host;
|
|
257
320
|
}
|
|
@@ -3,7 +3,7 @@ import { defineStudioActivationPlugin } from "../utils/activation.js";
|
|
|
3
3
|
export default defineNuxtPlugin(() => {
|
|
4
4
|
defineStudioActivationPlugin(async (user) => {
|
|
5
5
|
const config = useRuntimeConfig();
|
|
6
|
-
const host = await import("../host.
|
|
6
|
+
const host = await (config.public.studio.dev ? import("../host.dev") : import("../host")).then((m) => m.useStudioHost);
|
|
7
7
|
window.useStudioHost = () => host(user, config.public.studio.repository);
|
|
8
8
|
await import("nuxt-studio/app");
|
|
9
9
|
document.body.appendChild(document.createElement("nuxt-studio"));
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { eventHandler, sendRedirect } from "h3";
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect, setCookie } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
2
3
|
export default eventHandler((event) => {
|
|
3
|
-
|
|
4
|
+
const { redirect } = getQuery(event);
|
|
5
|
+
if (redirect) {
|
|
6
|
+
setCookie(event, "studio-redirect", String(redirect), {
|
|
7
|
+
httpOnly: true
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
const config = useRuntimeConfig(event);
|
|
11
|
+
const provider = config.public.studio?.repository?.provider || "github";
|
|
12
|
+
return sendRedirect(event, `/__nuxt_studio/auth/${provider}`);
|
|
4
13
|
return `<!DOCTYPE html>
|
|
5
14
|
<html lang="en">
|
|
6
15
|
<head>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export interface OAuthGitHubConfig {
|
|
2
2
|
/**
|
|
3
3
|
* GitHub OAuth Client ID
|
|
4
|
-
* @default process.env.
|
|
4
|
+
* @default process.env.STUDIO_GITHUB_CLIENT_ID
|
|
5
5
|
*/
|
|
6
6
|
clientId?: string;
|
|
7
7
|
/**
|
|
8
8
|
* GitHub OAuth Client Secret
|
|
9
|
-
* @default process.env.
|
|
9
|
+
* @default process.env.STUDIO_GITHUB_CLIENT_SECRET
|
|
10
10
|
*/
|
|
11
11
|
clientSecret?: string;
|
|
12
12
|
/**
|
|
@@ -44,8 +44,8 @@ export interface OAuthGitHubConfig {
|
|
|
44
44
|
authorizationParams?: Record<string, string>;
|
|
45
45
|
/**
|
|
46
46
|
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
|
|
47
|
-
*
|
|
48
|
-
* @
|
|
47
|
+
* Use `process.env.STUDIO_GITHUB_REDIRECT_URL` to overwrite the default redirect URL.
|
|
48
|
+
* @default is ${hostname}/__nuxt_studio/auth/github
|
|
49
49
|
*/
|
|
50
50
|
redirectURL?: string;
|
|
51
51
|
}
|
|
@@ -5,9 +5,11 @@ import { withQuery } from "ufo";
|
|
|
5
5
|
import { defu } from "defu";
|
|
6
6
|
import { useRuntimeConfig } from "#imports";
|
|
7
7
|
export default eventHandler(async (event) => {
|
|
8
|
-
const
|
|
8
|
+
const studioConfig = useRuntimeConfig(event).studio;
|
|
9
|
+
const config = defu(studioConfig?.auth?.github, {
|
|
9
10
|
clientId: process.env.STUDIO_GITHUB_CLIENT_ID,
|
|
10
11
|
clientSecret: process.env.STUDIO_GITHUB_CLIENT_SECRET,
|
|
12
|
+
redirectURL: process.env.STUDIO_GITHUB_REDIRECT_URL,
|
|
11
13
|
authorizationURL: "https://github.com/login/oauth/authorize",
|
|
12
14
|
tokenURL: "https://github.com/login/oauth/access_token",
|
|
13
15
|
apiURL: "https://api.github.com",
|
|
@@ -30,21 +32,21 @@ export default eventHandler(async (event) => {
|
|
|
30
32
|
});
|
|
31
33
|
}
|
|
32
34
|
const requestURL = getRequestURL(event);
|
|
33
|
-
|
|
35
|
+
config.redirectURL = config.redirectURL || `${requestURL.protocol}//${requestURL.host}${requestURL.pathname}`;
|
|
34
36
|
const state = await handleState(event);
|
|
35
37
|
if (!query.code) {
|
|
36
38
|
config.scope = config.scope || [];
|
|
37
39
|
if (config.emailRequired && !config.scope.includes("user:email")) {
|
|
38
40
|
config.scope.push("user:email");
|
|
39
41
|
}
|
|
40
|
-
if (config.emailRequired && !config.scope.includes("repo")) {
|
|
41
|
-
config.scope.push("repo");
|
|
42
|
+
if (config.emailRequired && !config.scope.includes("repo") && !config.scope.includes("public_repo")) {
|
|
43
|
+
config.scope.push(studioConfig.repository.private ? "repo" : "public_repo");
|
|
42
44
|
}
|
|
43
45
|
return sendRedirect(
|
|
44
46
|
event,
|
|
45
47
|
withQuery(config.authorizationURL, {
|
|
46
48
|
client_id: config.clientId,
|
|
47
|
-
redirect_uri: redirectURL,
|
|
49
|
+
redirect_uri: config.redirectURL,
|
|
48
50
|
scope: config.scope.join(" "),
|
|
49
51
|
state,
|
|
50
52
|
...config.authorizationParams
|
|
@@ -66,7 +68,7 @@ export default eventHandler(async (event) => {
|
|
|
66
68
|
grant_type: "authorization_code",
|
|
67
69
|
client_id: config.clientId,
|
|
68
70
|
client_secret: config.clientSecret,
|
|
69
|
-
redirect_uri: redirectURL,
|
|
71
|
+
redirect_uri: config.redirectURL,
|
|
70
72
|
code: query.code
|
|
71
73
|
}
|
|
72
74
|
});
|
|
@@ -108,14 +110,20 @@ export default eventHandler(async (event) => {
|
|
|
108
110
|
await session.update(defu({
|
|
109
111
|
user: {
|
|
110
112
|
contentUser: true,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
name: user.name,
|
|
113
|
+
providerId: user.id.toString(),
|
|
114
|
+
accessToken: token.access_token,
|
|
115
|
+
name: user.name || user.login,
|
|
114
116
|
avatar: user.avatar_url,
|
|
115
117
|
email: user.email,
|
|
116
118
|
provider: "github"
|
|
117
119
|
}
|
|
118
120
|
}, session.data));
|
|
121
|
+
const redirect = decodeURIComponent(getCookie(event, "studio-redirect") || "");
|
|
122
|
+
deleteCookie(event, "studio-redirect");
|
|
123
|
+
setCookie(event, "studio-session-check", "true", { httpOnly: false });
|
|
124
|
+
if (redirect && redirect.startsWith("/") && !redirect.startsWith("//")) {
|
|
125
|
+
return sendRedirect(event, redirect);
|
|
126
|
+
}
|
|
119
127
|
return sendRedirect(event, "/");
|
|
120
128
|
});
|
|
121
129
|
async function requestAccessToken(url, options) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface OAuthGitLabConfig {
|
|
2
|
+
/**
|
|
3
|
+
* GitLab OAuth Application ID
|
|
4
|
+
* @default process.env.STUDIO_GITLAB_APPLICATION_ID
|
|
5
|
+
*/
|
|
6
|
+
applicationId?: string;
|
|
7
|
+
/**
|
|
8
|
+
* GitLab OAuth Application Secret
|
|
9
|
+
* @default process.env.STUDIO_GITLAB_APPLICATION_SECRET
|
|
10
|
+
*/
|
|
11
|
+
applicationSecret?: string;
|
|
12
|
+
/**
|
|
13
|
+
* GitLab OAuth Scope
|
|
14
|
+
* @default []
|
|
15
|
+
* @see https://docs.gitlab.com/ee/integration/oauth_provider.html#authorized-applications
|
|
16
|
+
*/
|
|
17
|
+
scope?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Require email from user
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
emailRequired?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* GitLab instance URL
|
|
25
|
+
* @default 'https://gitlab.com'
|
|
26
|
+
*/
|
|
27
|
+
instanceUrl?: string;
|
|
28
|
+
/**
|
|
29
|
+
* GitLab OAuth Authorization URL
|
|
30
|
+
* @default '{instanceUrl}/oauth/authorize'
|
|
31
|
+
*/
|
|
32
|
+
authorizationURL?: string;
|
|
33
|
+
/**
|
|
34
|
+
* GitLab OAuth Token URL
|
|
35
|
+
* @default '{instanceUrl}/oauth/token'
|
|
36
|
+
*/
|
|
37
|
+
tokenURL?: string;
|
|
38
|
+
/**
|
|
39
|
+
* GitLab API URL
|
|
40
|
+
* @default '{instanceUrl}/api/v4'
|
|
41
|
+
*/
|
|
42
|
+
apiURL?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
45
|
+
*/
|
|
46
|
+
authorizationParams?: Record<string, string>;
|
|
47
|
+
/**
|
|
48
|
+
* Redirect URL to allow overriding for situations like prod failing to determine public hostname
|
|
49
|
+
* Use `process.env.STUDIO_GITLAB_REDIRECT_URL` to overwrite the default redirect URL.
|
|
50
|
+
* @default is ${hostname}/__nuxt_studio/auth/gitlab
|
|
51
|
+
*/
|
|
52
|
+
redirectURL?: string;
|
|
53
|
+
}
|
|
54
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
55
|
+
export default _default;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { FetchError } from "ofetch";
|
|
2
|
+
import { getRandomValues } from "uncrypto";
|
|
3
|
+
import { eventHandler, getQuery, sendRedirect, createError, getRequestURL, setCookie, deleteCookie, getCookie, useSession } from "h3";
|
|
4
|
+
import { withQuery } from "ufo";
|
|
5
|
+
import { defu } from "defu";
|
|
6
|
+
import { useRuntimeConfig } from "#imports";
|
|
7
|
+
export default eventHandler(async (event) => {
|
|
8
|
+
const studioConfig = useRuntimeConfig(event).studio;
|
|
9
|
+
const instanceUrl = studioConfig?.auth?.gitlab?.instanceUrl || "https://gitlab.com";
|
|
10
|
+
const config = defu(studioConfig?.auth?.gitlab, {
|
|
11
|
+
applicationId: process.env.STUDIO_GITLAB_APPLICATION_ID,
|
|
12
|
+
applicationSecret: process.env.STUDIO_GITLAB_APPLICATION_SECRET,
|
|
13
|
+
redirectURL: process.env.STUDIO_GITLAB_REDIRECT_URL,
|
|
14
|
+
instanceUrl,
|
|
15
|
+
authorizationURL: `${instanceUrl}/oauth/authorize`,
|
|
16
|
+
tokenURL: `${instanceUrl}/oauth/token`,
|
|
17
|
+
apiURL: `${instanceUrl}/api/v4`,
|
|
18
|
+
authorizationParams: {},
|
|
19
|
+
emailRequired: true
|
|
20
|
+
});
|
|
21
|
+
const query = getQuery(event);
|
|
22
|
+
if (query.error) {
|
|
23
|
+
throw createError({
|
|
24
|
+
statusCode: 401,
|
|
25
|
+
message: `GitLab login failed: ${query.error || "Unknown error"}`,
|
|
26
|
+
data: query
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (!config.applicationId || !config.applicationSecret) {
|
|
30
|
+
throw createError({
|
|
31
|
+
statusCode: 500,
|
|
32
|
+
message: "Missing GitLab application ID or secret",
|
|
33
|
+
data: config
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const requestURL = getRequestURL(event);
|
|
37
|
+
config.redirectURL = config.redirectURL || `${requestURL.protocol}//${requestURL.host}${requestURL.pathname}`;
|
|
38
|
+
if (!query.code) {
|
|
39
|
+
const state = await generateState(event);
|
|
40
|
+
config.scope = config.scope || [];
|
|
41
|
+
if (!config.scope.includes("api")) {
|
|
42
|
+
config.scope.push("api");
|
|
43
|
+
}
|
|
44
|
+
return sendRedirect(
|
|
45
|
+
event,
|
|
46
|
+
withQuery(config.authorizationURL, {
|
|
47
|
+
client_id: config.applicationId,
|
|
48
|
+
redirect_uri: config.redirectURL,
|
|
49
|
+
response_type: "code",
|
|
50
|
+
scope: config.scope.join(" "),
|
|
51
|
+
state,
|
|
52
|
+
...config.authorizationParams
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const storedState = getCookie(event, "studio-oauth-state");
|
|
57
|
+
if (!storedState) {
|
|
58
|
+
throw createError({
|
|
59
|
+
statusCode: 400,
|
|
60
|
+
message: "OAuth state cookie not found. Please try logging in again.",
|
|
61
|
+
data: {
|
|
62
|
+
hint: "State cookie may have expired or been cleared"
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (query.state !== storedState) {
|
|
67
|
+
throw createError({
|
|
68
|
+
statusCode: 400,
|
|
69
|
+
message: "Invalid state - OAuth state mismatch",
|
|
70
|
+
data: {
|
|
71
|
+
hint: "This may be caused by browser refresh, navigation, or expired session"
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
deleteCookie(event, "studio-oauth-state");
|
|
76
|
+
const token = await requestAccessToken(config.tokenURL, {
|
|
77
|
+
body: {
|
|
78
|
+
grant_type: "authorization_code",
|
|
79
|
+
client_id: config.applicationId,
|
|
80
|
+
client_secret: config.applicationSecret,
|
|
81
|
+
redirect_uri: config.redirectURL,
|
|
82
|
+
code: query.code
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (token.error || !token.access_token) {
|
|
86
|
+
throw createError({
|
|
87
|
+
statusCode: 500,
|
|
88
|
+
message: "Failed to get access token",
|
|
89
|
+
data: token
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const accessToken = token.access_token;
|
|
93
|
+
const user = await $fetch(`${config.apiURL}/user`, {
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${accessToken}`
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if (!user.email && config.emailRequired) {
|
|
99
|
+
throw createError({
|
|
100
|
+
statusCode: 500,
|
|
101
|
+
message: "Could not get GitLab user email",
|
|
102
|
+
data: token
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const session = await useSession(event, {
|
|
106
|
+
name: "studio-session",
|
|
107
|
+
password: useRuntimeConfig(event).studio?.auth?.sessionSecret
|
|
108
|
+
});
|
|
109
|
+
await session.update(defu({
|
|
110
|
+
user: {
|
|
111
|
+
contentUser: true,
|
|
112
|
+
providerId: user.id.toString(),
|
|
113
|
+
accessToken: token.access_token,
|
|
114
|
+
name: user.name || user.username,
|
|
115
|
+
avatar: user.avatar_url,
|
|
116
|
+
email: user.email,
|
|
117
|
+
provider: "gitlab"
|
|
118
|
+
}
|
|
119
|
+
}, session.data));
|
|
120
|
+
const redirect = decodeURIComponent(getCookie(event, "studio-redirect") || "/");
|
|
121
|
+
deleteCookie(event, "studio-redirect");
|
|
122
|
+
setCookie(event, "studio-session-check", "true", { httpOnly: false });
|
|
123
|
+
if (redirect && redirect.startsWith("/") && !redirect.startsWith("//")) {
|
|
124
|
+
return sendRedirect(event, redirect);
|
|
125
|
+
}
|
|
126
|
+
return sendRedirect(event, "/");
|
|
127
|
+
});
|
|
128
|
+
async function requestAccessToken(url, options) {
|
|
129
|
+
try {
|
|
130
|
+
return await $fetch(url, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Type": "application/json"
|
|
134
|
+
},
|
|
135
|
+
body: options.body,
|
|
136
|
+
params: options.params
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error instanceof FetchError) {
|
|
140
|
+
return error.data || { error: error.message };
|
|
141
|
+
}
|
|
142
|
+
return { error: "Unknown error" };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function generateState(event) {
|
|
146
|
+
const newState = Array.from(getRandomValues(new Uint8Array(32))).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
147
|
+
const requestURL = getRequestURL(event);
|
|
148
|
+
const isSecure = requestURL.protocol === "https:";
|
|
149
|
+
setCookie(event, "studio-oauth-state", newState, {
|
|
150
|
+
httpOnly: true,
|
|
151
|
+
secure: isSecure,
|
|
152
|
+
sameSite: "lax",
|
|
153
|
+
maxAge: 60 * 15
|
|
154
|
+
// 15 minutes
|
|
155
|
+
});
|
|
156
|
+
return newState;
|
|
157
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eventHandler, useSession } from "h3";
|
|
1
|
+
import { eventHandler, useSession, deleteCookie } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
3
|
export default eventHandler(async (event) => {
|
|
4
4
|
const session = await useSession(event, {
|
|
@@ -6,5 +6,6 @@ export default eventHandler(async (event) => {
|
|
|
6
6
|
password: useRuntimeConfig(event).studio?.auth?.sessionSecret
|
|
7
7
|
});
|
|
8
8
|
await session.clear();
|
|
9
|
+
deleteCookie(event, "studio-session-check");
|
|
9
10
|
return { loggedOut: true };
|
|
10
11
|
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { eventHandler, useSession } from "h3";
|
|
1
|
+
import { eventHandler, useSession, deleteCookie } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
3
|
export default eventHandler(async (event) => {
|
|
4
4
|
const session = await useSession(event, {
|
|
5
5
|
name: "studio-session",
|
|
6
6
|
password: useRuntimeConfig(event).studio?.auth?.sessionSecret
|
|
7
7
|
});
|
|
8
|
+
if (!session.data || Object.keys(session.data).length === 0) {
|
|
9
|
+
deleteCookie(event, "studio-session-check");
|
|
10
|
+
}
|
|
8
11
|
return {
|
|
9
12
|
...session.data,
|
|
10
13
|
id: session.id
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { createError, eventHandler, getRequestHeader, readRawBody, setResponseHeader } from "h3";
|
|
2
|
-
import { stringifyMarkdown } from "@nuxtjs/mdc/runtime";
|
|
3
|
-
import { decompressTree } from "@nuxt/content/runtime";
|
|
4
|
-
import { removeReservedKeysFromDocument } from "nuxt-studio/app/utils";
|
|
5
2
|
import { useStorage } from "#imports";
|
|
6
3
|
export default eventHandler(async (event) => {
|
|
7
4
|
const path = event.path.replace("/__nuxt_studio/dev/content/", "");
|
|
@@ -26,14 +23,6 @@ export default eventHandler(async (event) => {
|
|
|
26
23
|
} else if (getRequestHeader(event, "content-type") === "text/plain") {
|
|
27
24
|
const value = await readRawBody(event, "utf8");
|
|
28
25
|
await storage.setItem(key, value);
|
|
29
|
-
} else {
|
|
30
|
-
const value = await readRawBody(event, "utf8");
|
|
31
|
-
const json = JSON.parse(value || "{}");
|
|
32
|
-
const content = await stringifyMarkdown(
|
|
33
|
-
json.body.type === "minimark" ? decompressTree(json.body) : json.body,
|
|
34
|
-
removeReservedKeysFromDocument(json)
|
|
35
|
-
);
|
|
36
|
-
await storage.setItem(key, content);
|
|
37
26
|
}
|
|
38
27
|
return "OK";
|
|
39
28
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createError, eventHandler, getRequestHeader, readRawBody, setResponseHeader } from "h3";
|
|
2
|
+
import { withLeadingSlash } from "ufo";
|
|
2
3
|
import { useStorage } from "#imports";
|
|
3
4
|
export default eventHandler(async (event) => {
|
|
4
5
|
const path = event.path.replace("/__nuxt_studio/dev/public/", "");
|
|
@@ -23,6 +24,7 @@ export default eventHandler(async (event) => {
|
|
|
23
24
|
extension: key.split(".").pop(),
|
|
24
25
|
stem: key.split(".").join("."),
|
|
25
26
|
path: "/" + key.replace(/:/g, "/"),
|
|
27
|
+
fsPath: withLeadingSlash(key.replace(/:/g, "/")),
|
|
26
28
|
version: new Date(item.mtime || /* @__PURE__ */ new Date()).getTime()
|
|
27
29
|
};
|
|
28
30
|
}
|