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.
Files changed (50) hide show
  1. package/README.md +11 -9
  2. package/dist/app/en-CeGQDKH5.js +7 -0
  3. package/dist/app/en-D0jd_r21.js +7 -0
  4. package/dist/app/en-XbfkDcMn.js +7 -0
  5. package/dist/app/fr-CbcFrmHX.js +7 -0
  6. package/dist/app/fr-Ct4HO5e1.js +7 -0
  7. package/dist/app/fr-ILHmVnG9.js +7 -0
  8. package/dist/app/main.d.ts +83 -33
  9. package/dist/app/main.js +34187 -30512
  10. package/dist/app/service-worker.js +29 -5
  11. package/dist/app/shared-B6H2zqDG.js +8 -0
  12. package/dist/app/{shared-D04791PA.js → shared-DLETHbvn.js} +2311 -2307
  13. package/dist/app/{shared-DI6F-ifc.js → shared-Dvm_mWyG.js} +3120 -3132
  14. package/dist/app/shared.d.ts +1 -20
  15. package/dist/app/shared.js +2 -4
  16. package/dist/module/module.d.mts +70 -27
  17. package/dist/module/module.json +1 -1
  18. package/dist/module/module.mjs +101 -58
  19. package/dist/module/runtime/host.dev.js +19 -12
  20. package/dist/module/runtime/host.js +124 -61
  21. package/dist/module/runtime/plugins/studio.client.js +1 -1
  22. package/dist/module/runtime/server/routes/admin.js +11 -2
  23. package/dist/module/runtime/server/routes/auth/github.get.d.ts +4 -4
  24. package/dist/module/runtime/server/routes/auth/github.get.js +17 -9
  25. package/dist/module/runtime/server/routes/auth/gitlab.get.d.ts +55 -0
  26. package/dist/module/runtime/server/routes/auth/gitlab.get.js +157 -0
  27. package/dist/module/runtime/server/routes/auth/session.delete.js +2 -1
  28. package/dist/module/runtime/server/routes/auth/session.get.js +4 -1
  29. package/dist/module/runtime/server/routes/dev/content/[...path].js +0 -11
  30. package/dist/module/runtime/server/routes/dev/public/[...path].d.ts +1 -0
  31. package/dist/module/runtime/server/routes/dev/public/[...path].js +2 -0
  32. package/dist/module/runtime/types/content.d.ts +6 -0
  33. package/dist/module/runtime/types/content.js +7 -0
  34. package/dist/module/runtime/utils/activation.js +16 -5
  35. package/dist/module/runtime/utils/collection.d.ts +10 -17
  36. package/dist/module/runtime/utils/collection.js +32 -92
  37. package/dist/module/runtime/utils/document.d.ts +15 -0
  38. package/dist/module/runtime/utils/document.js +288 -0
  39. package/dist/module/runtime/utils/media.d.ts +1 -0
  40. package/dist/module/runtime/utils/media.js +5 -0
  41. package/dist/module/runtime/utils/object.d.ts +4 -0
  42. package/dist/module/runtime/utils/object.js +27 -0
  43. package/dist/module/runtime/utils/source.d.ts +11 -0
  44. package/dist/module/runtime/utils/source.js +36 -0
  45. package/license.md +9 -0
  46. package/package.json +7 -6
  47. package/dist/app/shared-R5zYJ3Dl.js +0 -25803
  48. package/dist/app/utils-DI6F-ifc.js +0 -25774
  49. package/dist/app/utils.d.ts +0 -22
  50. 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, createCollectionDocument, generateRecordDeletion, generateRecordInsert, getCollectionInfo, normalizeDocument } from "./utils/collection.js";
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 { generateDocumentFromContent } from "nuxt-studio/app/utils";
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
- get: async (id) => {
161
- const item = await useContentCollectionQuery(id.split("/")[0]).where("id", "=", id).first();
162
- return normalizeDocument(item);
163
- },
164
- getFileSystemPath: (id) => {
165
- return getCollectionInfo(id, useContentCollections()).fsPath;
166
- },
167
- list: async () => {
168
- const collections2 = Object.keys(useContentCollections()).filter((c) => c !== "info");
169
- const contents = await Promise.all(collections2.map(async (collection) => {
170
- return await useContentCollectionQuery(collection).all();
171
- }));
172
- return contents.flat();
173
- },
174
- create: async (fsPath, content) => {
175
- const collections2 = useContentCollections();
176
- const collectionInfo = getCollectionByFilePath(fsPath, collections2);
177
- const id = generateIdFromFsPath(fsPath, collectionInfo);
178
- const existingDocument = await host.document.get(id);
179
- if (existingDocument) {
180
- throw new Error(`Cannot create document with id "${id}": document already exists.`);
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
- delete: async (id) => {
195
- id = id.replace(/:/g, "/");
196
- const collection = getCollectionInfo(id, useContentCollections()).collection;
197
- await useContentDatabaseAdapter(collection.name).exec(generateRecordDeletion(collection, id));
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
- detectActives: () => {
200
- const wrappers = document.querySelectorAll("[data-content-id]");
201
- return Array.from(wrappers).map((wrapper) => {
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 (id) => {
213
- return await publicAssetsStorage.getItem(id);
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 (id, media) => {
222
- await publicAssetsStorage.setItem(id, media);
266
+ upsert: async (fsPath, media) => {
267
+ const id = generateMediaIdFromFsPath(fsPath);
268
+ await publicAssetsStorage.setItem(generateMediaIdFromFsPath(fsPath), { ...media, id });
223
269
  },
224
- delete: async (id) => {
225
- await publicAssetsStorage.removeItem(id);
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("/sw.js");
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.js").then((m) => m.useStudioHost);
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
- return sendRedirect(event, "/__nuxt_studio/auth/github");
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.NUXT_OAUTH_GITHUB_CLIENT_ID
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.NUXT_OAUTH_GITHUB_CLIENT_SECRET
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
- * @default process.env.NUXT_OAUTH_GITHUB_REDIRECT_URL
48
- * @see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps
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 config = defu(useRuntimeConfig(event).studio?.auth?.github, {
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
- const redirectURL = `${requestURL.protocol}//${requestURL.host}${requestURL.pathname}`;
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
- githubId: user.id,
112
- githubToken: token.access_token,
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
  }
@@ -3,6 +3,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
3
3
  extension: string | undefined;
4
4
  stem: string;
5
5
  path: string;
6
+ fsPath: string;
6
7
  version: number;
7
8
  } | undefined>>;
8
9
  export default _default;
@@ -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
  }
@@ -0,0 +1,6 @@
1
+ export declare enum ContentFileExtension {
2
+ Markdown = "md",
3
+ YAML = "yaml",
4
+ YML = "yml",
5
+ JSON = "json"
6
+ }