diff --git a/.content-collections/cache/content-collection-config.mjs b/.content-collections/cache/content-collection-config.mjs deleted file mode 100644 index b929b80..0000000 --- a/.content-collections/cache/content-collection-config.mjs +++ /dev/null @@ -1,394 +0,0 @@ -// content-collections.ts -import { defineCollection, defineConfig } from "@content-collections/core"; -import { compileMDX } from "@content-collections/mdx"; -import rehypeAutolinkHeadings from "rehype-autolink-headings"; -import rehypePrettyCode from "rehype-pretty-code"; -import rehypeSlug from "rehype-slug"; -import { codeImport } from "remark-code-import"; -import remarkGfm from "remark-gfm"; -import { createHighlighter } from "shiki"; -import { visit as visit3 } from "unist-util-visit"; - -// lib/rehype-component.ts -import fs from "fs"; -import path from "path"; -import { u } from "unist-builder"; -import { visit } from "unist-util-visit"; - -// __registry__/index.tsx -var Index = {}; - -// components/ui/registry/registry-styles.ts -var styles = [ - { - name: "default", - label: "Default" - } -]; - -// lib/rehype-component.ts -function rehypeComponent() { - return async (tree) => { - visit(tree, (node) => { - const { value: srcPath } = getNodeAttributeByName(node, "src") || {}; - if (node.name === "ComponentSource") { - const name = getNodeAttributeByName(node, "name")?.value; - const fileName = getNodeAttributeByName(node, "fileName")?.value; - if (!name && !srcPath) { - return null; - } - try { - for (const style of styles) { - let src; - if (srcPath) { - src = srcPath; - } else { - const component = Index[style.name][name]; - src = fileName ? component.files.find((file) => { - return file.endsWith(`${fileName}.tsx`) || file.endsWith(`${fileName}.ts`); - }) || component.files[0] : component.files[0]; - } - const filePath = path.join(process.cwd(), src); - let source = fs.readFileSync(filePath, "utf8"); - source = source.replaceAll( - `@/registry/${style.name}/`, - "@/components/" - ); - source = source.replaceAll("export default", "export"); - node.children?.push( - u("element", { - tagName: "pre", - properties: { - __src__: src - }, - attributes: [ - { - name: "styleName", - type: "mdxJsxAttribute", - value: style.name - } - ], - children: [ - u("element", { - tagName: "code", - properties: { - className: ["language-tsx"] - }, - data: { - meta: `event="copy_source_code"` - }, - children: [ - { - type: "text", - value: source - } - ] - }) - ] - }) - ); - } - } catch (error) { - console.error(error); - } - } - if (node.name === "ComponentPreview" || node.name === "BlockPreview") { - const name = getNodeAttributeByName(node, "name")?.value; - if (!name) { - return null; - } - try { - for (const style of styles) { - const component = Index[style.name][name]; - const src = component.files[0]; - const filePath = path.join(process.cwd(), src); - let source = fs.readFileSync(filePath, "utf8"); - source = source.replaceAll( - `@/registry/${style.name}/`, - "@/components/" - ); - source = source.replaceAll("export default", "export"); - node.children?.push( - u("element", { - tagName: "pre", - properties: { - __src__: src - }, - children: [ - u("element", { - tagName: "code", - properties: { - className: ["language-tsx"] - }, - data: { - meta: `event="copy_usage_code"` - }, - children: [ - { - type: "text", - value: source - } - ] - }) - ] - }) - ); - } - } catch (error) { - console.error(error); - } - } - }); - }; -} -function getNodeAttributeByName(node, name) { - return node.attributes?.find((attribute) => attribute.name === name); -} - -// lib/rehype-npm-command.ts -import { visit as visit2 } from "unist-util-visit"; -function rehypeNpmCommand() { - return (tree) => { - visit2(tree, (node) => { - if (node.type !== "element" || node?.tagName !== "pre") { - return; - } - if (node.properties?.["__rawString__"]?.startsWith("npm install")) { - const npmCommand = node.properties?.["__rawString__"]; - node.properties["__npmCommand__"] = npmCommand; - node.properties["__yarnCommand__"] = npmCommand.replace( - "npm install", - "yarn add" - ); - node.properties["__pnpmCommand__"] = npmCommand.replace( - "npm install", - "pnpm add" - ); - node.properties["__bunCommand__"] = npmCommand.replace( - "npm install", - "bun add" - ); - } - if (node.properties?.["__rawString__"]?.startsWith("npx create-")) { - const npmCommand = node.properties?.["__rawString__"]; - node.properties["__npmCommand__"] = npmCommand; - node.properties["__yarnCommand__"] = npmCommand.replace( - "npx create-", - "yarn create " - ); - node.properties["__pnpmCommand__"] = npmCommand.replace( - "npx create-", - "pnpm create " - ); - node.properties["__bunCommand__"] = npmCommand.replace( - "npx", - "bunx --bun" - ); - } - if (node.properties?.["__rawString__"]?.startsWith("npx") && !node.properties?.["__rawString__"]?.startsWith("npx create-")) { - const npmCommand = node.properties?.["__rawString__"]; - node.properties["__npmCommand__"] = npmCommand; - node.properties["__yarnCommand__"] = npmCommand; - node.properties["__pnpmCommand__"] = npmCommand.replace( - "npx", - "pnpm dlx" - ); - node.properties["__bunCommand__"] = npmCommand.replace( - "npx", - "bunx --bun" - ); - } - if (node.properties?.["__rawString__"]?.startsWith("npm create")) { - const npmCommand = node.properties?.["__rawString__"]; - node.properties["__npmCommand__"] = npmCommand; - node.properties["__yarnCommand__"] = npmCommand.replace( - "npm create", - "yarn create" - ); - node.properties["__pnpmCommand__"] = npmCommand.replace( - "npm create", - "pnpm create" - ); - node.properties["__bunCommand__"] = npmCommand.replace( - "npm create", - "bun create" - ); - } - }); - }; -} - -// content-collections.ts -var prettyCodeOptions = { - theme: "github-dark", - getHighlighter: (options) => createHighlighter({ - ...options - }), - onVisitLine(node) { - if (node.children.length === 0) { - node.children = [{ type: "text", value: " " }]; - } - }, - onVisitHighlightedLine(node) { - if (!node.properties.className) { - node.properties.className = []; - } - node.properties.className.push("line--highlighted"); - }, - onVisitHighlightedChars(node) { - if (!node.properties.className) { - node.properties.className = []; - } - node.properties.className = ["word--highlighted"]; - } -}; -var showcase = defineCollection({ - name: "Showcase", - directory: "content/showcase", - include: "**/*.mdx", - schema: (z) => ({ - title: z.string(), - description: z.string(), - image: z.string(), - href: z.string(), - affiliation: z.string(), - featured: z.boolean().optional().default(false) - }), - transform: async (document, context) => { - const body = await compileMDX(context, document, { - remarkPlugins: [codeImport, remarkGfm] - }); - return { - ...document, - slug: `/showcase/${document._meta.path}`, - slugAsParams: document._meta.path, - body: { - raw: document.content, - code: body - } - }; - } -}); -var pages = defineCollection({ - name: "Page", - directory: "content/pages", - include: "**/*.mdx", - schema: (z) => ({ - title: z.string(), - description: z.string() - }), - transform: async (document, context) => { - const body = await compileMDX(context, document, { - remarkPlugins: [codeImport, remarkGfm] - }); - return { - ...document, - slug: `/${document._meta.path}`, - slugAsParams: document._meta.path, - body: { - raw: document.content, - code: body - } - }; - } -}); -var documents = defineCollection({ - name: "Doc", - directory: "content", - include: "**/*.mdx", - schema: (z) => ({ - title: z.string(), - description: z.string(), - published: z.boolean().default(true), - date: z.string().optional(), - links: z.object({ - doc: z.string().optional(), - api: z.string().optional() - }).optional(), - featured: z.boolean().optional().default(false), - component: z.boolean().optional().default(false), - toc: z.boolean().optional().default(true), - image: z.string().optional() - }), - transform: async (document, context) => { - const body = await compileMDX(context, document, { - remarkPlugins: [codeImport, remarkGfm], - rehypePlugins: [ - rehypeSlug, - rehypeComponent, - () => (tree) => { - visit3(tree, (node) => { - if (node?.type === "element" && node?.tagName === "pre") { - const [codeEl] = node.children; - if (codeEl.tagName !== "code") { - return; - } - if (codeEl.data?.meta) { - const regex = /event="([^"]*)"/; - const match = codeEl.data?.meta.match(regex); - if (match) { - node.__event__ = match ? match[1] : null; - codeEl.data.meta = codeEl.data.meta.replace(regex, ""); - } - } - node.__rawString__ = codeEl.children?.[0].value; - node.__src__ = node.properties?.__src__; - node.__style__ = node.properties?.__style__; - } - }); - }, - [rehypePrettyCode, prettyCodeOptions], - () => (tree) => { - visit3(tree, (node) => { - if (node?.type === "element" && node?.tagName === "figure") { - if (!("data-rehype-pretty-code-figure" in node.properties)) { - return; - } - const preElement = node.children.at(-1); - if (preElement.tagName !== "pre") { - return; - } - preElement.properties["__withMeta__"] = node.children.at(0).tagName === "div"; - preElement.properties["__rawString__"] = node.__rawString__; - if (node.__src__) { - preElement.properties["__src__"] = node.__src__; - } - if (node.__event__) { - preElement.properties["__event__"] = node.__event__; - } - if (node.__style__) { - preElement.properties["__style__"] = node.__style__; - } - } - }); - }, - rehypeNpmCommand, - [ - rehypeAutolinkHeadings, - { - properties: { - className: ["subheading-anchor"], - ariaLabel: "Link to section" - } - } - ] - ] - }); - return { - ...document, - image: `${process.env.NEXT_PUBLIC_APP_URL}/og?title=${encodeURI(document.title)}`, - slug: `/${document._meta.path}`, - slugAsParams: document._meta.path.split("/").slice(1).join("/"), - body: { - raw: document.content, - code: body - } - }; - } -}); -var content_collections_default = defineConfig({ - collections: [documents, pages, showcase] -}); -export { - content_collections_default as default -}; diff --git a/.content-collections/cache/doc/docs/36c920da13347438bad62a209abb00dc33ec1ab4a63f4d06cff0d778ce08cd63.cache b/.content-collections/cache/doc/docs/36c920da13347438bad62a209abb00dc33ec1ab4a63f4d06cff0d778ce08cd63.cache deleted file mode 100644 index e9cee9e..0000000 --- a/.content-collections/cache/doc/docs/36c920da13347438bad62a209abb00dc33ec1ab4a63f4d06cff0d778ce08cd63.cache +++ /dev/null @@ -1 +0,0 @@ -"var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var f=(i,n)=>()=>(n||i((n={exports:{}}).exports,n),n.exports),y=(i,n)=>{for(var o in n)l(i,o,{get:n[o],enumerable:!0})},r=(i,n,o,a)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let t of m(n))!b.call(i,t)&&t!==o&&l(i,t,{get:()=>n[t],enumerable:!(a=p(n,t))||a.enumerable});return i};var k=(i,n,o)=>(o=i!=null?u(g(i)):{},r(n||!i||!i.__esModule?l(o,\"default\",{value:i,enumerable:!0}):o,i)),w=i=>r(l({},\"__esModule\",{value:!0}),i);var s=f((I,c)=>{c.exports=_jsx_runtime});var x={};y(x,{default:()=>h});var e=k(s());function d(i){let n={a:\"a\",h2:\"h2\",h3:\"h3\",li:\"li\",p:\"p\",span:\"span\",strong:\"strong\",ul:\"ul\",...i.components};return(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(n.p,{children:\"Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\"}),`\n`,(0,e.jsxs)(n.h2,{id:\"why-lib-ui\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#why-lib-ui\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Why Lib UI?\"]}),`\n`,(0,e.jsx)(n.p,{children:\"In today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\"}),`\n`,(0,e.jsx)(n.p,{children:\"Lib UI bridges this gap by offering:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Full-stack Components\"}),\": Get both UI and logic in a single package\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Ready-to-use Solutions\"}),\": Implement complex features like authentication and payments with one command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Customizable Building Blocks\"}),\": Maintain full control while leveraging pre-built functionality\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Developer Experience\"}),\": Focus on building your product, not wrestling with documentation\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h3,{id:\"real-world-examples\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#real-world-examples\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Real-world Examples\"]}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Authentication\"}),\": Implement secure user authentication with both UI components and backend logic using a single command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Payment Integration\"}),\": Add Stripe payments to your app without spending hours reading documentation\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Admin Dashboards\"}),\": Deploy fully functional admin interfaces that connect directly to your data\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"our-mission\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#our-mission\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Our Mission\"]}),`\n`,(0,e.jsx)(n.p,{children:\"We believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F680} Ship products faster\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F6E0}\\uFE0F Reduce boilerplate code\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u26A1 Focus on core business logic\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F3A8} Maintain design flexibility\"}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"inspiration\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#inspiration\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Inspiration\"]}),`\n`,(0,e.jsx)(n.p,{children:\"This project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://ui.shadcn.com/\",children:\"shadcn/ui\"}),\" - For pioneering component architecture\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://magicui.design/\",children:\"MagicUI\"}),\" - For innovative design patterns\"]}),`\n`]})]})}function h(i={}){let{wrapper:n}=i.components||{};return n?(0,e.jsx)(n,{...i,children:(0,e.jsx)(d,{...i})}):d(i)}return w(x);})();\n;return Component;" \ No newline at end of file diff --git a/.content-collections/cache/doc/docs_cli/06eb5a14822b1ac9891f9460e471a550903d80e0f006a6896cebb95a748dc444.cache b/.content-collections/cache/doc/docs_cli/06eb5a14822b1ac9891f9460e471a550903d80e0f006a6896cebb95a748dc444.cache deleted file mode 100644 index e9cee9e..0000000 --- a/.content-collections/cache/doc/docs_cli/06eb5a14822b1ac9891f9460e471a550903d80e0f006a6896cebb95a748dc444.cache +++ /dev/null @@ -1 +0,0 @@ -"var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var f=(i,n)=>()=>(n||i((n={exports:{}}).exports,n),n.exports),y=(i,n)=>{for(var o in n)l(i,o,{get:n[o],enumerable:!0})},r=(i,n,o,a)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let t of m(n))!b.call(i,t)&&t!==o&&l(i,t,{get:()=>n[t],enumerable:!(a=p(n,t))||a.enumerable});return i};var k=(i,n,o)=>(o=i!=null?u(g(i)):{},r(n||!i||!i.__esModule?l(o,\"default\",{value:i,enumerable:!0}):o,i)),w=i=>r(l({},\"__esModule\",{value:!0}),i);var s=f((I,c)=>{c.exports=_jsx_runtime});var x={};y(x,{default:()=>h});var e=k(s());function d(i){let n={a:\"a\",h2:\"h2\",h3:\"h3\",li:\"li\",p:\"p\",span:\"span\",strong:\"strong\",ul:\"ul\",...i.components};return(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(n.p,{children:\"Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\"}),`\n`,(0,e.jsxs)(n.h2,{id:\"why-lib-ui\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#why-lib-ui\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Why Lib UI?\"]}),`\n`,(0,e.jsx)(n.p,{children:\"In today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\"}),`\n`,(0,e.jsx)(n.p,{children:\"Lib UI bridges this gap by offering:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Full-stack Components\"}),\": Get both UI and logic in a single package\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Ready-to-use Solutions\"}),\": Implement complex features like authentication and payments with one command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Customizable Building Blocks\"}),\": Maintain full control while leveraging pre-built functionality\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Developer Experience\"}),\": Focus on building your product, not wrestling with documentation\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h3,{id:\"real-world-examples\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#real-world-examples\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Real-world Examples\"]}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Authentication\"}),\": Implement secure user authentication with both UI components and backend logic using a single command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Payment Integration\"}),\": Add Stripe payments to your app without spending hours reading documentation\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Admin Dashboards\"}),\": Deploy fully functional admin interfaces that connect directly to your data\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"our-mission\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#our-mission\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Our Mission\"]}),`\n`,(0,e.jsx)(n.p,{children:\"We believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F680} Ship products faster\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F6E0}\\uFE0F Reduce boilerplate code\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u26A1 Focus on core business logic\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F3A8} Maintain design flexibility\"}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"inspiration\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#inspiration\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Inspiration\"]}),`\n`,(0,e.jsx)(n.p,{children:\"This project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://ui.shadcn.com/\",children:\"shadcn/ui\"}),\" - For pioneering component architecture\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://magicui.design/\",children:\"MagicUI\"}),\" - For innovative design patterns\"]}),`\n`]})]})}function h(i={}){let{wrapper:n}=i.components||{};return n?(0,e.jsx)(n,{...i,children:(0,e.jsx)(d,{...i})}):d(i)}return w(x);})();\n;return Component;" \ No newline at end of file diff --git a/.content-collections/cache/doc/docs_installation/afdc9fa72addad13f3d0cec80a80146922446204bfda4c3dd05301e0dd25220c.cache b/.content-collections/cache/doc/docs_installation/afdc9fa72addad13f3d0cec80a80146922446204bfda4c3dd05301e0dd25220c.cache deleted file mode 100644 index 091782b..0000000 --- a/.content-collections/cache/doc/docs_installation/afdc9fa72addad13f3d0cec80a80146922446204bfda4c3dd05301e0dd25220c.cache +++ /dev/null @@ -1 +0,0 @@ -"var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var y=Object.getPrototypeOf,_=Object.prototype.hasOwnProperty;var x=(a,n)=>()=>(n||a((n={exports:{}}).exports,n),n.exports),b=(a,n)=>{for(var t in n)l(a,t,{get:n[t],enumerable:!0})},o=(a,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let i of m(n))!_.call(a,i)&&i!==t&&l(a,i,{get:()=>n[i],enumerable:!(r=g(n,i))||r.enumerable});return a};var E=(a,n,t)=>(t=a!=null?u(y(a)):{},o(n||!a||!a.__esModule?l(t,\"default\",{value:a,enumerable:!0}):t,a)),f=a=>o(l({},\"__esModule\",{value:!0}),a);var c=x((j,d)=>{d.exports=_jsx_runtime});var F={};b(F,{default:()=>p});var e=E(c());function s(a){let n={a:\"a\",code:\"code\",figure:\"figure\",h3:\"h3\",p:\"p\",pre:\"pre\",span:\"span\",strong:\"strong\",...a.components},{Callout:t,Steps:r}=n;return t||h(\"Callout\",!0),r||h(\"Steps\",!0),(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(t,{children:(0,e.jsx)(n.p,{children:(0,e.jsx)(n.strong,{children:\"We are currently only available on nextjs, stay tuned for further updates.\"})})}),`\n`,(0,e.jsxs)(r,{children:[(0,e.jsxs)(n.h3,{id:\"create-project\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#create-project\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Create project\"]}),(0,e.jsxs)(n.p,{children:[\"Run the \",(0,e.jsx)(n.code,{children:\"init\"}),\" command to create a new Next.js project or to setup an existing one:\"]}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"bash\",\"data-theme\":\"github-dark\",__rawString__:`npx libui-next init\n`,__npmCommand__:`npx libui-next init\n`,__yarnCommand__:`npx libui-next init\n`,__pnpmCommand__:`pnpm dlx libui-next init\n`,__bunCommand__:`bunx --bun libui-next init\n`,children:(0,e.jsx)(n.code,{\"data-language\":\"bash\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},children:(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\"npx\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" libui-next\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" init\"})]})})})}),(0,e.jsxs)(n.h3,{id:\"start-coding\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#start-coding\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Start coding\"]}),(0,e.jsx)(n.p,{children:\"You can now start adding full stack components to your project!\"}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"bash\",\"data-theme\":\"github-dark\",__rawString__:`npx libui-next add authjs\n`,__npmCommand__:`npx libui-next add authjs\n`,__yarnCommand__:`npx libui-next add authjs\n`,__pnpmCommand__:`pnpm dlx libui-next add authjs\n`,__bunCommand__:`bunx --bun libui-next add authjs\n`,children:(0,e.jsx)(n.code,{\"data-language\":\"bash\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},children:(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\"npx\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" libui-next\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" add\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" authjs\"})]})})})}),(0,e.jsxs)(n.p,{children:[\"This will add all the configuration of \",(0,e.jsx)(n.code,{children:\"Auth.js\"}),\" to your project.\"]}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"tsx\",\"data-theme\":\"github-dark\",__rawString__:`import { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUp() {\n return (\n
\n \n
\n )\n}\n`,children:(0,e.jsxs)(n.code,{\"data-line-numbers\":\"\",\"data-language\":\"tsx\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},\"data-line-numbers-max-digits\":\"1\",children:[(0,e.jsxs)(n.span,{className:\"line--highlighted\",\"data-line\":\"\",\"data-highlighted-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"import\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" { SignUpForm } \"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"from\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" '@/components/auth/sign-up-form'\"})]}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:\" \"}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"export\"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" default\"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" function\"}),(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\" SignUp\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\"() {\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" return\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" (\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" <\"}),(0,e.jsx)(n.span,{style:{color:\"#85E89D\"},children:\"div\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\">\"})]}),`\n`,(0,e.jsxs)(n.span,{className:\"line--highlighted\",\"data-line\":\"\",\"data-highlighted-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" <\"}),(0,e.jsx)(n.span,{style:{color:\"#79B8FF\"},children:\"SignUpForm\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" />\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" \"})]}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" )\"})}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\"}\"})})]})})})]})]})}function p(a={}){let{wrapper:n}=a.components||{};return n?(0,e.jsx)(n,{...a,children:(0,e.jsx)(s,{...a})}):s(a)}function h(a,n){throw new Error(\"Expected \"+(n?\"component\":\"object\")+\" `\"+a+\"` to be defined: you likely forgot to import, pass, or provide it.\")}return f(F);})();\n;return Component;" \ No newline at end of file diff --git a/.content-collections/cache/mapping.json b/.content-collections/cache/mapping.json deleted file mode 100644 index c7612ab..0000000 --- a/.content-collections/cache/mapping.json +++ /dev/null @@ -1 +0,0 @@ -{"Doc":{"docs":["36c920da13347438bad62a209abb00dc33ec1ab4a63f4d06cff0d778ce08cd63"],"docs/cli":["06eb5a14822b1ac9891f9460e471a550903d80e0f006a6896cebb95a748dc444"],"docs/installation":["afdc9fa72addad13f3d0cec80a80146922446204bfda4c3dd05301e0dd25220c"]}} \ No newline at end of file diff --git a/.content-collections/generated/allDocs.js b/.content-collections/generated/allDocs.js deleted file mode 100644 index 3d818fc..0000000 --- a/.content-collections/generated/allDocs.js +++ /dev/null @@ -1,72 +0,0 @@ - -export default [ - { - "content": "Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\n\n## Why Lib UI?\n\nIn today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\n\nLib UI bridges this gap by offering:\n\n- **Full-stack Components**: Get both UI and logic in a single package\n- **Ready-to-use Solutions**: Implement complex features like authentication and payments with one command\n- **Customizable Building Blocks**: Maintain full control while leveraging pre-built functionality\n- **Developer Experience**: Focus on building your product, not wrestling with documentation\n\n### Real-world Examples\n\n- **Authentication**: Implement secure user authentication with both UI components and backend logic using a single command\n- **Payment Integration**: Add Stripe payments to your app without spending hours reading documentation\n- **Admin Dashboards**: Deploy fully functional admin interfaces that connect directly to your data\n\n## Our Mission\n\nWe believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\n\n- 🚀 Ship products faster\n- 🛠️ Reduce boilerplate code\n- ⚡ Focus on core business logic\n- 🎨 Maintain design flexibility\n\n## Inspiration\n\nThis project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\n\n- [shadcn/ui](https://ui.shadcn.com/) - For pioneering component architecture\n- [MagicUI](https://magicui.design/) - For innovative design patterns", - "title": "CLI", - "description": "Build full-stack applications faster with pre-built, customizable components", - "published": true, - "featured": false, - "component": false, - "toc": true, - "_meta": { - "filePath": "docs/cli.mdx", - "fileName": "cli.mdx", - "directory": "docs", - "extension": "mdx", - "path": "docs/cli" - }, - "image": "http://localhost:3000/og?title=CLI", - "slug": "/docs/cli", - "slugAsParams": "cli", - "body": { - "raw": "Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\n\n## Why Lib UI?\n\nIn today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\n\nLib UI bridges this gap by offering:\n\n- **Full-stack Components**: Get both UI and logic in a single package\n- **Ready-to-use Solutions**: Implement complex features like authentication and payments with one command\n- **Customizable Building Blocks**: Maintain full control while leveraging pre-built functionality\n- **Developer Experience**: Focus on building your product, not wrestling with documentation\n\n### Real-world Examples\n\n- **Authentication**: Implement secure user authentication with both UI components and backend logic using a single command\n- **Payment Integration**: Add Stripe payments to your app without spending hours reading documentation\n- **Admin Dashboards**: Deploy fully functional admin interfaces that connect directly to your data\n\n## Our Mission\n\nWe believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\n\n- 🚀 Ship products faster\n- 🛠️ Reduce boilerplate code\n- ⚡ Focus on core business logic\n- 🎨 Maintain design flexibility\n\n## Inspiration\n\nThis project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\n\n- [shadcn/ui](https://ui.shadcn.com/) - For pioneering component architecture\n- [MagicUI](https://magicui.design/) - For innovative design patterns", - "code": "var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var f=(i,n)=>()=>(n||i((n={exports:{}}).exports,n),n.exports),y=(i,n)=>{for(var o in n)l(i,o,{get:n[o],enumerable:!0})},r=(i,n,o,a)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let t of m(n))!b.call(i,t)&&t!==o&&l(i,t,{get:()=>n[t],enumerable:!(a=p(n,t))||a.enumerable});return i};var k=(i,n,o)=>(o=i!=null?u(g(i)):{},r(n||!i||!i.__esModule?l(o,\"default\",{value:i,enumerable:!0}):o,i)),w=i=>r(l({},\"__esModule\",{value:!0}),i);var s=f((I,c)=>{c.exports=_jsx_runtime});var x={};y(x,{default:()=>h});var e=k(s());function d(i){let n={a:\"a\",h2:\"h2\",h3:\"h3\",li:\"li\",p:\"p\",span:\"span\",strong:\"strong\",ul:\"ul\",...i.components};return(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(n.p,{children:\"Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\"}),`\n`,(0,e.jsxs)(n.h2,{id:\"why-lib-ui\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#why-lib-ui\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Why Lib UI?\"]}),`\n`,(0,e.jsx)(n.p,{children:\"In today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\"}),`\n`,(0,e.jsx)(n.p,{children:\"Lib UI bridges this gap by offering:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Full-stack Components\"}),\": Get both UI and logic in a single package\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Ready-to-use Solutions\"}),\": Implement complex features like authentication and payments with one command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Customizable Building Blocks\"}),\": Maintain full control while leveraging pre-built functionality\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Developer Experience\"}),\": Focus on building your product, not wrestling with documentation\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h3,{id:\"real-world-examples\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#real-world-examples\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Real-world Examples\"]}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Authentication\"}),\": Implement secure user authentication with both UI components and backend logic using a single command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Payment Integration\"}),\": Add Stripe payments to your app without spending hours reading documentation\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Admin Dashboards\"}),\": Deploy fully functional admin interfaces that connect directly to your data\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"our-mission\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#our-mission\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Our Mission\"]}),`\n`,(0,e.jsx)(n.p,{children:\"We believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F680} Ship products faster\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F6E0}\\uFE0F Reduce boilerplate code\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u26A1 Focus on core business logic\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F3A8} Maintain design flexibility\"}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"inspiration\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#inspiration\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Inspiration\"]}),`\n`,(0,e.jsx)(n.p,{children:\"This project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://ui.shadcn.com/\",children:\"shadcn/ui\"}),\" - For pioneering component architecture\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://magicui.design/\",children:\"MagicUI\"}),\" - For innovative design patterns\"]}),`\n`]})]})}function h(i={}){let{wrapper:n}=i.components||{};return n?(0,e.jsx)(n,{...i,children:(0,e.jsx)(d,{...i})}):d(i)}return w(x);})();\n;return Component;" - } - }, - { - "content": "Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\n\n## Why Lib UI?\n\nIn today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\n\nLib UI bridges this gap by offering:\n\n- **Full-stack Components**: Get both UI and logic in a single package\n- **Ready-to-use Solutions**: Implement complex features like authentication and payments with one command\n- **Customizable Building Blocks**: Maintain full control while leveraging pre-built functionality\n- **Developer Experience**: Focus on building your product, not wrestling with documentation\n\n### Real-world Examples\n\n- **Authentication**: Implement secure user authentication with both UI components and backend logic using a single command\n- **Payment Integration**: Add Stripe payments to your app without spending hours reading documentation\n- **Admin Dashboards**: Deploy fully functional admin interfaces that connect directly to your data\n\n## Our Mission\n\nWe believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\n\n- 🚀 Ship products faster\n- 🛠️ Reduce boilerplate code\n- ⚡ Focus on core business logic\n- 🎨 Maintain design flexibility\n\n## Inspiration\n\nThis project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\n\n- [shadcn/ui](https://ui.shadcn.com/) - For pioneering component architecture\n- [MagicUI](https://magicui.design/) - For innovative design patterns", - "title": "Introduction", - "description": "Build full-stack applications faster with pre-built, customizable components", - "published": true, - "featured": false, - "component": false, - "toc": true, - "_meta": { - "filePath": "docs/index.mdx", - "fileName": "index.mdx", - "directory": "docs", - "extension": "mdx", - "path": "docs" - }, - "image": "http://localhost:3000/og?title=Introduction", - "slug": "/docs", - "slugAsParams": "", - "body": { - "raw": "Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\n\n## Why Lib UI?\n\nIn today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\n\nLib UI bridges this gap by offering:\n\n- **Full-stack Components**: Get both UI and logic in a single package\n- **Ready-to-use Solutions**: Implement complex features like authentication and payments with one command\n- **Customizable Building Blocks**: Maintain full control while leveraging pre-built functionality\n- **Developer Experience**: Focus on building your product, not wrestling with documentation\n\n### Real-world Examples\n\n- **Authentication**: Implement secure user authentication with both UI components and backend logic using a single command\n- **Payment Integration**: Add Stripe payments to your app without spending hours reading documentation\n- **Admin Dashboards**: Deploy fully functional admin interfaces that connect directly to your data\n\n## Our Mission\n\nWe believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\n\n- 🚀 Ship products faster\n- 🛠️ Reduce boilerplate code\n- ⚡ Focus on core business logic\n- 🎨 Maintain design flexibility\n\n## Inspiration\n\nThis project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\n\n- [shadcn/ui](https://ui.shadcn.com/) - For pioneering component architecture\n- [MagicUI](https://magicui.design/) - For innovative design patterns", - "code": "var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var f=(i,n)=>()=>(n||i((n={exports:{}}).exports,n),n.exports),y=(i,n)=>{for(var o in n)l(i,o,{get:n[o],enumerable:!0})},r=(i,n,o,a)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let t of m(n))!b.call(i,t)&&t!==o&&l(i,t,{get:()=>n[t],enumerable:!(a=p(n,t))||a.enumerable});return i};var k=(i,n,o)=>(o=i!=null?u(g(i)):{},r(n||!i||!i.__esModule?l(o,\"default\",{value:i,enumerable:!0}):o,i)),w=i=>r(l({},\"__esModule\",{value:!0}),i);var s=f((I,c)=>{c.exports=_jsx_runtime});var x={};y(x,{default:()=>h});var e=k(s());function d(i){let n={a:\"a\",h2:\"h2\",h3:\"h3\",li:\"li\",p:\"p\",span:\"span\",strong:\"strong\",ul:\"ul\",...i.components};return(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(n.p,{children:\"Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\"}),`\n`,(0,e.jsxs)(n.h2,{id:\"why-lib-ui\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#why-lib-ui\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Why Lib UI?\"]}),`\n`,(0,e.jsx)(n.p,{children:\"In today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\"}),`\n`,(0,e.jsx)(n.p,{children:\"Lib UI bridges this gap by offering:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Full-stack Components\"}),\": Get both UI and logic in a single package\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Ready-to-use Solutions\"}),\": Implement complex features like authentication and payments with one command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Customizable Building Blocks\"}),\": Maintain full control while leveraging pre-built functionality\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Developer Experience\"}),\": Focus on building your product, not wrestling with documentation\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h3,{id:\"real-world-examples\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#real-world-examples\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Real-world Examples\"]}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Authentication\"}),\": Implement secure user authentication with both UI components and backend logic using a single command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Payment Integration\"}),\": Add Stripe payments to your app without spending hours reading documentation\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Admin Dashboards\"}),\": Deploy fully functional admin interfaces that connect directly to your data\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"our-mission\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#our-mission\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Our Mission\"]}),`\n`,(0,e.jsx)(n.p,{children:\"We believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F680} Ship products faster\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F6E0}\\uFE0F Reduce boilerplate code\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u26A1 Focus on core business logic\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F3A8} Maintain design flexibility\"}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"inspiration\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#inspiration\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Inspiration\"]}),`\n`,(0,e.jsx)(n.p,{children:\"This project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://ui.shadcn.com/\",children:\"shadcn/ui\"}),\" - For pioneering component architecture\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://magicui.design/\",children:\"MagicUI\"}),\" - For innovative design patterns\"]}),`\n`]})]})}function h(i={}){let{wrapper:n}=i.components||{};return n?(0,e.jsx)(n,{...i,children:(0,e.jsx)(d,{...i})}):d(i)}return w(x);})();\n;return Component;" - } - }, - { - "content": "\n\n**We are currently only available on nextjs, stay tuned for further updates.**\n\n\n\n\n\n### Create project\n\nRun the `init` command to create a new Next.js project or to setup an existing one:\n\n```bash\nnpx libui-next init\n```\n\n### Start coding\n\nYou can now start adding full stack components to your project!\n\n```bash\nnpx libui-next add authjs\n```\n\nThis will add all the configuration of `Auth.js` to your project.\n\n```tsx {1,6} showLineNumbers\nimport { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUp() {\n return (\n
\n \n
\n )\n}\n```\n\n
", - "title": "Installation (example)", - "description": "Install and configure Next.js.", - "published": true, - "featured": false, - "component": false, - "toc": true, - "_meta": { - "filePath": "docs/installation.mdx", - "fileName": "installation.mdx", - "directory": "docs", - "extension": "mdx", - "path": "docs/installation" - }, - "image": "http://localhost:3000/og?title=Installation%20(example)", - "slug": "/docs/installation", - "slugAsParams": "installation", - "body": { - "raw": "\n\n**We are currently only available on nextjs, stay tuned for further updates.**\n\n\n\n\n\n### Create project\n\nRun the `init` command to create a new Next.js project or to setup an existing one:\n\n```bash\nnpx libui-next init\n```\n\n### Start coding\n\nYou can now start adding full stack components to your project!\n\n```bash\nnpx libui-next add authjs\n```\n\nThis will add all the configuration of `Auth.js` to your project.\n\n```tsx {1,6} showLineNumbers\nimport { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUp() {\n return (\n
\n \n
\n )\n}\n```\n\n
", - "code": "var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var y=Object.getPrototypeOf,_=Object.prototype.hasOwnProperty;var x=(a,n)=>()=>(n||a((n={exports:{}}).exports,n),n.exports),b=(a,n)=>{for(var t in n)l(a,t,{get:n[t],enumerable:!0})},o=(a,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let i of m(n))!_.call(a,i)&&i!==t&&l(a,i,{get:()=>n[i],enumerable:!(r=g(n,i))||r.enumerable});return a};var E=(a,n,t)=>(t=a!=null?u(y(a)):{},o(n||!a||!a.__esModule?l(t,\"default\",{value:a,enumerable:!0}):t,a)),f=a=>o(l({},\"__esModule\",{value:!0}),a);var c=x((j,d)=>{d.exports=_jsx_runtime});var F={};b(F,{default:()=>p});var e=E(c());function s(a){let n={a:\"a\",code:\"code\",figure:\"figure\",h3:\"h3\",p:\"p\",pre:\"pre\",span:\"span\",strong:\"strong\",...a.components},{Callout:t,Steps:r}=n;return t||h(\"Callout\",!0),r||h(\"Steps\",!0),(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(t,{children:(0,e.jsx)(n.p,{children:(0,e.jsx)(n.strong,{children:\"We are currently only available on nextjs, stay tuned for further updates.\"})})}),`\n`,(0,e.jsxs)(r,{children:[(0,e.jsxs)(n.h3,{id:\"create-project\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#create-project\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Create project\"]}),(0,e.jsxs)(n.p,{children:[\"Run the \",(0,e.jsx)(n.code,{children:\"init\"}),\" command to create a new Next.js project or to setup an existing one:\"]}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"bash\",\"data-theme\":\"github-dark\",__rawString__:`npx libui-next init\n`,__npmCommand__:`npx libui-next init\n`,__yarnCommand__:`npx libui-next init\n`,__pnpmCommand__:`pnpm dlx libui-next init\n`,__bunCommand__:`bunx --bun libui-next init\n`,children:(0,e.jsx)(n.code,{\"data-language\":\"bash\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},children:(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\"npx\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" libui-next\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" init\"})]})})})}),(0,e.jsxs)(n.h3,{id:\"start-coding\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#start-coding\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Start coding\"]}),(0,e.jsx)(n.p,{children:\"You can now start adding full stack components to your project!\"}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"bash\",\"data-theme\":\"github-dark\",__rawString__:`npx libui-next add authjs\n`,__npmCommand__:`npx libui-next add authjs\n`,__yarnCommand__:`npx libui-next add authjs\n`,__pnpmCommand__:`pnpm dlx libui-next add authjs\n`,__bunCommand__:`bunx --bun libui-next add authjs\n`,children:(0,e.jsx)(n.code,{\"data-language\":\"bash\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},children:(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\"npx\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" libui-next\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" add\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" authjs\"})]})})})}),(0,e.jsxs)(n.p,{children:[\"This will add all the configuration of \",(0,e.jsx)(n.code,{children:\"Auth.js\"}),\" to your project.\"]}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"tsx\",\"data-theme\":\"github-dark\",__rawString__:`import { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUp() {\n return (\n
\n \n
\n )\n}\n`,children:(0,e.jsxs)(n.code,{\"data-line-numbers\":\"\",\"data-language\":\"tsx\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},\"data-line-numbers-max-digits\":\"1\",children:[(0,e.jsxs)(n.span,{className:\"line--highlighted\",\"data-line\":\"\",\"data-highlighted-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"import\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" { SignUpForm } \"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"from\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" '@/components/auth/sign-up-form'\"})]}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:\" \"}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"export\"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" default\"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" function\"}),(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\" SignUp\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\"() {\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" return\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" (\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" <\"}),(0,e.jsx)(n.span,{style:{color:\"#85E89D\"},children:\"div\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\">\"})]}),`\n`,(0,e.jsxs)(n.span,{className:\"line--highlighted\",\"data-line\":\"\",\"data-highlighted-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" <\"}),(0,e.jsx)(n.span,{style:{color:\"#79B8FF\"},children:\"SignUpForm\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" />\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" \"})]}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" )\"})}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\"}\"})})]})})})]})]})}function p(a={}){let{wrapper:n}=a.components||{};return n?(0,e.jsx)(n,{...a,children:(0,e.jsx)(s,{...a})}):s(a)}function h(a,n){throw new Error(\"Expected \"+(n?\"component\":\"object\")+\" `\"+a+\"` to be defined: you likely forgot to import, pass, or provide it.\")}return f(F);})();\n;return Component;" - } - } -] \ No newline at end of file diff --git a/.content-collections/generated/allPages.js b/.content-collections/generated/allPages.js deleted file mode 100644 index 4b90925..0000000 --- a/.content-collections/generated/allPages.js +++ /dev/null @@ -1,2 +0,0 @@ - -export default [] \ No newline at end of file diff --git a/.content-collections/generated/allShowcases.js b/.content-collections/generated/allShowcases.js deleted file mode 100644 index 4b90925..0000000 --- a/.content-collections/generated/allShowcases.js +++ /dev/null @@ -1,2 +0,0 @@ - -export default [] \ No newline at end of file diff --git a/.content-collections/generated/index.d.ts b/.content-collections/generated/index.d.ts deleted file mode 100644 index 9c6e024..0000000 --- a/.content-collections/generated/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import configuration from "../../content-collections.ts"; -import { GetTypeByName } from "@content-collections/core"; - -export type Doc = GetTypeByName; -export declare const allDocs: Array; - -export type Page = GetTypeByName; -export declare const allPages: Array; - -export type Showcase = GetTypeByName; -export declare const allShowcases: Array; - -export {}; diff --git a/.content-collections/generated/index.js b/.content-collections/generated/index.js deleted file mode 100644 index bdae159..0000000 --- a/.content-collections/generated/index.js +++ /dev/null @@ -1,7 +0,0 @@ -// generated by content-collections at Wed Jan 08 2025 13:32:25 GMT-0500 (Eastern Standard Time) - -import allDocs from "./allDocs.js"; -import allPages from "./allPages.js"; -import allShowcases from "./allShowcases.js"; - -export { allDocs, allPages, allShowcases }; diff --git a/.gitignore b/.gitignore index 674c0aa..94de876 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ !.env.example # dependencies -/node_modules +**/node_modules /.pnp .pnp.* .yarn/* @@ -21,10 +21,12 @@ # production /build +**/dist/ # misc .DS_Store *.pem +.content-collections/ # debug npm-debug.log* diff --git a/core/auth/actions/admin.ts b/core/auth/actions/admin.ts new file mode 100644 index 0000000..7c2011d --- /dev/null +++ b/core/auth/actions/admin.ts @@ -0,0 +1,14 @@ +"use server"; + +import { currentRole } from "@/lib/auth"; +import { UserRole } from "@prisma/client"; + +export const admin = async () => { + const role = await currentRole(); + + if (role === UserRole.ADMIN) { + return { success: "Allowed Server Action!" }; + } + + return { error: "Forbidden Server Action!" } +}; \ No newline at end of file diff --git a/core/auth/actions/login.ts b/core/auth/actions/login.ts new file mode 100644 index 0000000..919438b --- /dev/null +++ b/core/auth/actions/login.ts @@ -0,0 +1,102 @@ +"use server"; + +import * as z from "zod"; +import * as bcrypt from "bcrypt"; + +import { db } from "@/lib/db"; +import { signIn } from "@/auth"; +import { LoginSchema } from "@/schemas"; +import { getUserByEmail } from "@/data/user"; +import { getTwoFactorTokenByEmail } from "@/data/two-factor-token"; +import { sendVerificationEmail, sendTwoFactorTokenEmail } from "@/lib/mail"; +import { DEFAULT_LOGIN_REDIRECT } from "../routes"; +import { + generateVerificationToken, + generateTwoFactorToken, +} from "../lib/token"; +import { getTwoFactorConfirmationByUserId } from "@/data/two-factor-confirmation"; + +export const login = async ( + values: z.infer, + callbackUrl?: string | null, + twoFactorEnabled?: boolean +) => { + const validatedFields = LoginSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid fields!" }; + } + + const { email, password, code } = validatedFields.data; + + const existingUser = await getUserByEmail(email); + + if (!existingUser || !existingUser.email || !existingUser.password) { + return { error: "Email does not exist!" }; + } + + const passwordsMatch = await bcrypt.compare(password, existingUser.password); + + if (!passwordsMatch) { + return { error: "Invalid credentials!" }; + } + + if (!existingUser.emailVerified) { + const verificationToken = await generateVerificationToken( + existingUser.email + ); + await sendVerificationEmail( + verificationToken.email, + verificationToken.token + ); + return { success: "Confirmation email sent!" }; + } + + // Only proceed with 2FA if enabled via prop + if (twoFactorEnabled) { + if (code) { + const twoFactorToken = await getTwoFactorTokenByEmail(existingUser.email); + + if (!twoFactorToken || twoFactorToken.token !== code) { + return { error: "Invalid 2FA code!" }; + } + + const hasExpired = new Date(twoFactorToken.expires) < new Date(); + + if (hasExpired) { + return { error: "2FA code has expired!" }; + } + + await db.twoFactorToken.delete({ + where: { id: twoFactorToken.id }, + }); + + await db.twoFactorConfirmation.upsert({ + where: { + userId: existingUser.id, + }, + update: {}, + create: { + userId: existingUser.id, + }, + }); + } else { + const twoFactorToken = await generateTwoFactorToken(existingUser.email); + await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token); + return { twoFactor: true }; + } + } + + try { + await signIn("credentials", { + email, + password, + redirect: false, + }); + + return { success: "Logged in successfully!" }; + } catch (error) { + console.error("Login error:", error); + return { error: "Something went wrong!" }; + } +}; diff --git a/core/auth/actions/logout.ts b/core/auth/actions/logout.ts new file mode 100644 index 0000000..252bc7f --- /dev/null +++ b/core/auth/actions/logout.ts @@ -0,0 +1,7 @@ +"use server"; + +import { signOut } from "@/auth"; + +export const logout = async () => { + await signOut(); +}; \ No newline at end of file diff --git a/core/auth/actions/new-password.ts b/core/auth/actions/new-password.ts new file mode 100644 index 0000000..5509b60 --- /dev/null +++ b/core/auth/actions/new-password.ts @@ -0,0 +1,57 @@ +"use server"; + +import * as z from "zod"; +import bcrypt from "bcryptjs"; + +import { NewPasswordSchema } from "@/schemas"; +import { getPasswordResetTokenByToken } from "@/data/password-reset-token"; +import { getUserByEmail } from "@/data/user"; +import { db } from "@/lib/db"; + +export const newPassword = async ( + values: z.infer , + token?: string | null, +) => { + if (!token) { + return { error: "Missing token!" }; + } + + const validatedFields = NewPasswordSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid fields!" }; + } + + const { password } = validatedFields.data; + + const existingToken = await getPasswordResetTokenByToken(token); + + if (!existingToken) { + return { error: "Invalid token!" }; + } + + const hasExpired = new Date(existingToken.expires) < new Date(); + + if (hasExpired) { + return { error: "Token has expired!" }; + } + + const existingUser = await getUserByEmail(existingToken.email); + + if (!existingUser) { + return { error: "Email does not exist!" } + } + + const hashedPassword = await bcrypt.hash(password, 10); + + await db.user.update({ + where: { id: existingUser.id }, + data: { password: hashedPassword }, + }); + + await db.passwordResetToken.delete({ + where: { id: existingToken.id } + }); + + return { success: "Password updated!" }; +}; \ No newline at end of file diff --git a/core/auth/actions/new-verification.ts b/core/auth/actions/new-verification.ts new file mode 100644 index 0000000..2b6b21e --- /dev/null +++ b/core/auth/actions/new-verification.ts @@ -0,0 +1,38 @@ +"use server"; + +import { db } from "@/lib/db"; +import { getUserByEmail } from "@/data/user"; +import { getVerificationTokenByToken } from "../data/verification-token"; + +export const newVerification = async (token: string) => { + const existingToken = await getVerificationTokenByToken(token); + + if (!existingToken) { + return { error: "Token does not exist!" }; + } + + const hasExpired = new Date(existingToken.expires) < new Date(); + + if (hasExpired) { + return { error: "Token has expired!" }; + } + + const existingUser = await getUserByEmail(existingToken.email); + + if (!existingUser) { + return { error: "Email does not exist!" }; + } + + await db.user.update({ + where: { id: existingUser.id }, + data: { + emailVerified: new Date(), + email: existingToken.email, + } + }); + await db.verificationToken.delete({ + where: { id: existingToken.id } + }); + + return { success: "Email verified!" }; +}; \ No newline at end of file diff --git a/core/auth/actions/register.ts b/core/auth/actions/register.ts new file mode 100644 index 0000000..3c55c76 --- /dev/null +++ b/core/auth/actions/register.ts @@ -0,0 +1,60 @@ +"use server"; + +import * as z from "zod"; +import bcrypt from "bcryptjs"; + +import { db } from "@/lib/db"; +import { RegisterSchema } from "@/schemas"; +import { getUserByEmail } from "@/data/user"; +import { sendVerificationEmail } from "@/lib/mail"; +import { generateVerificationToken } from "../lib/token"; +import { signIn } from "@/auth"; + +export const register = async (values: z.infer) => { + const validatedFields = RegisterSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid fields!" }; + } + + const { email, password, name } = validatedFields.data; + const hashedPassword = await bcrypt.hash(password, 10); + + const existingUser = await getUserByEmail(email); + + if (existingUser) { + return { error: "Email already in use!" }; + } + + await db.user.create({ + data: { + name, + email, + password: hashedPassword, + }, + }); + + const verificationToken = await generateVerificationToken(email); + await sendVerificationEmail( + verificationToken.email, + verificationToken.token, + ); + + try { + const signInResult = await signIn("credentials", { + email, + password, + redirect: false, + }); + + console.log(password) + + if (signInResult?.error) { + return { error: "Something went wrong!" }; + } + } catch (error) { + return { error: "Something went wrong!" }; + } + + return { success: "Confirmation email sent!" }; +}; \ No newline at end of file diff --git a/core/auth/actions/reset.ts b/core/auth/actions/reset.ts new file mode 100644 index 0000000..c84fd66 --- /dev/null +++ b/core/auth/actions/reset.ts @@ -0,0 +1,32 @@ +"use server"; + +import * as z from "zod"; + +import { ResetSchema } from "@/schemas"; +import { getUserByEmail } from "@/data/user"; +import { sendPasswordResetEmail } from "@/lib/mail"; +import { generatePasswordResetToken } from "../lib/token"; + +export const reset = async (values: z.infer) => { + const validatedFields = ResetSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid emaiL!" }; + } + + const { email } = validatedFields.data; + + const existingUser = await getUserByEmail(email); + + if (!existingUser) { + return { error: "Email not found!" }; + } + + const passwordResetToken = await generatePasswordResetToken(email); + await sendPasswordResetEmail( + passwordResetToken.email, + passwordResetToken.token, + ); + + return { success: "Reset email sent!" }; +} \ No newline at end of file diff --git a/core/auth/actions/settings.ts b/core/auth/actions/settings.ts new file mode 100644 index 0000000..f7373e0 --- /dev/null +++ b/core/auth/actions/settings.ts @@ -0,0 +1,89 @@ +"use server"; + +import * as z from "zod"; +import bcrypt from "bcryptjs"; + +import { update } from "@/auth"; +import { db } from "@/lib/db"; +import { SettingsSchema } from "@/schemas"; +import { getUserByEmail, getUserById } from "@/data/user"; +import { currentUser } from "@/lib/auth"; +import { generateVerificationToken } from "@/lib/token"; +import { sendVerificationEmail } from "@/lib/mail"; + +export const settings = async ( + values: z.infer + ) => { + const user = await currentUser(); + + if (!user) { + return { error: "Unauthorized" } + } + + const dbUser = await getUserById(user.id); + + if (!dbUser) { + return { error: "Unauthorized" } + } + + if (user.isOAuth) { + values.email = undefined; + values.password = undefined; + values.newPassword = undefined; + values.isTwoFactorEnabled = undefined; + } + + if (values.email && values.email !== user.email) { + const existingUser = await getUserByEmail(values.email); + + if (existingUser && existingUser.id !== user.id) { + return { error: "Email already in use!" } + } + + const verificationToken = await generateVerificationToken( + values.email + ); + await sendVerificationEmail( + verificationToken.email, + verificationToken.token, + ); + + return { success: "Verification email sent!" }; + } + + if (values.password && values.newPassword && dbUser.password) { + const passwordsMatch = await bcrypt.compare( + values.password, + dbUser.password, + ); + + if (!passwordsMatch) { + return { error: "Incorrect password!" }; + } + + const hashedPassword = await bcrypt.hash( + values.newPassword, + 10, + ); + values.password = hashedPassword; + values.newPassword = undefined; + } + + const updatedUser = await db.user.update({ + where: { id: dbUser.id }, + data: { + ...values, + } + }); + + update({ + user: { + name: updatedUser.name, + email: updatedUser.email, + isTwoFactorEnabled: updatedUser.isTwoFactorEnabled, + role: updatedUser.role, + } + }); + + return { success: "Settings Updated!" } + } \ No newline at end of file diff --git a/core/auth/app/api/admin/route.ts b/core/auth/app/api/admin/route.ts new file mode 100644 index 0000000..7a460e9 --- /dev/null +++ b/core/auth/app/api/admin/route.ts @@ -0,0 +1,13 @@ +import { currentRole } from "@/lib/auth"; +import { UserRole } from "@prisma/client"; +import { NextResponse } from "next/server"; + +export async function GET() { + const role = await currentRole(); + + if (role === UserRole.ADMIN) { + return new NextResponse(null, { status: 200 }); + } + + return new NextResponse(null, { status: 403 }); +} diff --git a/core/auth/app/api/auth/[...nextauth]/route.ts b/core/auth/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..5acc419 --- /dev/null +++ b/core/auth/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1 @@ +export { GET, POST } from "@/auth"; \ No newline at end of file diff --git a/core/auth/auth.config.ts b/core/auth/auth.config.ts new file mode 100644 index 0000000..dc443a4 --- /dev/null +++ b/core/auth/auth.config.ts @@ -0,0 +1,39 @@ +import bcrypt from 'bcryptjs'; +import type { NextAuthConfig } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import Github from 'next-auth/providers/github'; +import Google from 'next-auth/providers/google'; + +import { LoginSchema } from '@/schemas'; +import { getUserByEmail } from '@/data/user'; + +export default { + providers: [ + Google({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), + Github({ + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }), + Credentials({ + async authorize(credentials) { + const validatedFields = LoginSchema.safeParse(credentials); + + if (validatedFields.success) { + const { email, password } = validatedFields.data; + + const user = await getUserByEmail(email); + if (!user || !user.password) return null; + + const passwordsMatch = await bcrypt.compare(password, user.password); + + if (passwordsMatch) return user; + } + + return null; + }, + }), + ], +} satisfies NextAuthConfig; diff --git a/core/auth/auth.ts b/core/auth/auth.ts new file mode 100644 index 0000000..b3a11bc --- /dev/null +++ b/core/auth/auth.ts @@ -0,0 +1,80 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import NextAuth from "next-auth"; +import { UserRole } from "@prisma/client"; +import { PrismaAdapter } from "@auth/prisma-adapter"; + +import { db } from "@/lib/db"; +import authConfig from "@/auth.config"; +import { getUserById } from "@/data/user"; +import { getTwoFactorConfirmationByUserId } from "@/data/two-factor-confirmation"; +import { getAccountByUserId } from "./data/account"; + +export const { + handlers: { GET, POST }, + auth, + signIn, + signOut, + update, +} = NextAuth({ + pages: { + signIn: "/auth/login", + error: "/auth/error", + }, + events: { + async linkAccount({ user }: any) { + await db.user.update({ + where: { id: user.id }, + data: { emailVerified: new Date() }, + }); + }, + }, + callbacks: { + async signIn({ account }: any) { + + if (account?.provider !== "credentials") return true; + + return true; + }, + async session({ token, session }: any) { + if (token.sub && session.user) { + session.user.id = token.sub; + } + + if (token.role && session.user) { + session.user.role = token.role as UserRole; + } + + if (session.user) { + session.user.isTwoFactorEnabled = token.isTwoFactorEnabled as boolean; + } + + if (session.user) { + session.user.name = token.name; + session.user.email = token.email; + session.user.isOAuth = token.isOAuth as boolean; + } + + return session; + }, + async jwt({ token }: any) { + if (!token.sub) return token; + + const existingUser = await getUserById(token.sub); + + if (!existingUser) return token; + + const existingAccount = await getAccountByUserId(existingUser.id); + + token.isOAuth = !!existingAccount; + token.name = existingUser.name; + token.email = existingUser.email; + token.role = existingUser.role; + token.isTwoFactorEnabled = existingUser.isTwoFactorEnabled; + + return token; + }, + }, + adapter: PrismaAdapter(db), + session: { strategy: "jwt" }, + ...authConfig, +}); diff --git a/core/auth/components/auth/auth.tsx b/core/auth/components/auth/auth.tsx new file mode 100644 index 0000000..4eb0969 --- /dev/null +++ b/core/auth/components/auth/auth.tsx @@ -0,0 +1,601 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +"use client"; + +import * as z from "zod"; +import { useCallback, useEffect, useState, useTransition } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { + LoginSchema, + NewPasswordSchema, + RegisterSchema, + ResetSchema, +} from "@/schemas"; +import { Input } from "@/components/ui/input"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Button } from "@/components/ui/button"; +import { register } from "@/actions/register"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useSearchParams } from "next/navigation"; +import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; +import { signIn } from "@/auth"; +import { FaGithub, FaGoogle, FaUser } from "react-icons/fa"; +import { login } from "@/actions/login"; +import Link from "next/link"; +import { reset } from "@/actions/reset"; +import { newVerification } from "@/actions/new-verification"; +import { BeatLoader } from "react-spinners"; +import { newPassword } from "@/actions/new-password"; +import { useCurrentUser } from "@/hooks/use-current-user"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { logout } from "@/actions/logout"; +import { toast } from "sonner"; +import { useSession } from "next-auth/react"; + +export const SignUp = ({ + google, + github, +}: { + google?: boolean; + github?: boolean; +}) => { + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(RegisterSchema), + defaultValues: { + email: "", + password: "", + name: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + register(values).then((data) => { + if (data.error) { + setError(data.error); + toast.error(data.error); + } + if (data.success) { + form.reset(); + setSuccess(data.success); + toast.success(data.success); + } + }); + }); + }; + + return ( + + + Create an account + Enter your email to access. + + +
+ +
+ ( + + Name + + + + + )} + /> + ( + + Email + + + + + )} + /> + ( + + Password + + + + + )} + /> +
+ +
+ +
+ {(google || github) && ( +
+
+ +
+
+ + Or continue with + +
+
+ )} + {(google || github) && ( + + + + )} +
+ ); +}; + +export const Login = ({ + google, + github, + twoFactor, +}: { + google?: boolean; + github?: boolean; + twoFactor?: boolean; +}) => { + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get("callbackUrl"); + const session = useSession(); + + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + const [showTwoFactor, setShowTwoFactor] = useState(false); + + const form = useForm>({ + resolver: zodResolver(LoginSchema), + defaultValues: { + email: "", + password: "", + code: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + login(values, callbackUrl, twoFactor) + .then((data) => { + console.log("Login response:", data); + if (data?.error) { + setError(data.error); + toast.error(data.error); + } + + if (data?.twoFactor) { + setShowTwoFactor(true); + toast.success("2FA Code sent to your email!"); + } + + if (data?.success) { + form.reset(); + setSuccess(data.success); + toast.success(data.success); + window.location.href = callbackUrl || DEFAULT_LOGIN_REDIRECT; + } + }) + .catch((error) => { + console.error("Login error:", error); + toast.error("Something went wrong with login"); + }); + }); + }; + + return ( + + + + {showTwoFactor ? "Two-Factor Authentication" : "Login"} + + + {showTwoFactor + ? "Enter the code sent to your email" + : "Enter your information to login."} + + + +
+ +
+ {showTwoFactor ? ( + ( + + 2FA Code + + + + + + )} + /> + ) : ( + <> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + + )} + /> + + )} +
+ +
+ +
+ {(google || github) && ( +
+
+ +
+
+ + Or continue with + +
+
+ )} + {(google || github) && ( + + + + )} +
+ ); +}; + +export const OAuth = ({ + google, + github, +}: { + google?: boolean; + github?: boolean; +}) => { + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get("callbackUrl"); + + const onClick = (provider: "google" | "github") => { + signIn(provider, { + callbackUrl: callbackUrl || DEFAULT_LOGIN_REDIRECT, + }); + }; + return ( +
+ {google && ( + + )} + {github && ( + + )} +
+ ); +}; + +export const VerifyEmail = () => { + const [error, setError] = useState(); + const [success, setSuccess] = useState(); + + const searchParams = useSearchParams(); + + const token = searchParams.get("token"); + + const onSubmit = useCallback(() => { + if (success || error) return; + + if (!token) { + setError("Missing token!"); + return; + } + + newVerification(token) + .then((data) => { + setSuccess(data.success); + setError(data.error); + }) + .catch(() => { + setError("Something went wrong!"); + }); + }, [token, success, error]); + + useEffect(() => { + onSubmit(); + }, [onSubmit]); + + return ( + + + Verify your email + + This page is used to verify your email address. + + + +
+ {!success && !error && } + {success && toast.success(success)} + {error && toast.error(error)} +
+
+ ); +}; + +export const ResetPassword = () => { + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(ResetSchema), + defaultValues: { + email: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + reset(values).then((data) => { + toast.error(data?.error); + toast.success(data?.success); + }); + }); + }; + + return ( + + + Forgot your password? + + Enter your email to reset your password. + + + +
+ +
+ ( + + Email + + + + + + )} + /> +
+ +
+ +
+
+ ); +}; + +export const NewPassword = () => { + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(NewPasswordSchema), + defaultValues: { + password: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + newPassword(values, token).then((data) => { + toast.error(data?.error); + toast.success(data?.success); + }); + }); + }; + + return ( + + + Enter a new password + + This page is used to enter a new password. + + + +
+ +
+ ( + + Password + + + + + + )} + /> +
+ + +
+ +
+ ); +}; + +export const UserButton = () => { + const user = useCurrentUser(); + + const onClick = () => { + logout(); + }; + + return ( + + + + + + + + + + + + + + ); +}; diff --git a/core/auth/components/auth/logout-button.tsx b/core/auth/components/auth/logout-button.tsx new file mode 100644 index 0000000..8d7b4d9 --- /dev/null +++ b/core/auth/components/auth/logout-button.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { logout } from "@/actions/logout"; + +interface LogoutButtonProps { + children?: React.ReactNode; +} + +export const LogoutButton = ({ children }: LogoutButtonProps) => { + const onClick = () => { + logout(); + }; + + return ( + + {children} + + ); +}; diff --git a/core/auth/components/auth/user-button.tsx b/core/auth/components/auth/user-button.tsx new file mode 100644 index 0000000..4c0cf02 --- /dev/null +++ b/core/auth/components/auth/user-button.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { FaUser } from "react-icons/fa"; +import { ExitIcon } from "@radix-ui/react-icons" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Avatar, + AvatarImage, + AvatarFallback, +} from "@/components/ui/avatar"; +import { useCurrentUser } from "@/hooks/use-current-user"; +import { LogoutButton } from "@/components/auth/logout-button"; + +export const UserButton = () => { + const user = useCurrentUser(); + + return ( + + + + + + + + + + + + + + Logout + + + + + ); +}; diff --git a/core/auth/components/ui/avatar.tsx b/core/auth/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/core/auth/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/core/auth/components/ui/examples/auth-example.tsx b/core/auth/components/ui/examples/auth-example.tsx new file mode 100644 index 0000000..568743f --- /dev/null +++ b/core/auth/components/ui/examples/auth-example.tsx @@ -0,0 +1,182 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +"use client"; + +import { Button } from "@/components/ui/button"; +import { signIn, signOut, useSession } from "next-auth/react"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Icons } from "@/components/ui/icons/icons"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import axios from "axios"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +export function Auth() { + const router = useRouter(); + const [error, setError] = useState(null); + const session = useSession(); + + const formSchema = z.object({ + name: z.string().min(1, { message: "Name is required" }), + email: z.string().email(), + password: z.string().min(1, { message: "Password is required" }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + email: "", + password: "", + }, + }); + + const handleGitHubSignIn = () => { + signIn("github", { callbackUrl: "/" }); + toast.success("Successfully signed in with GitHub!"); + }; + + const handleGoogleSignIn = () => { + signIn("google", { callbackUrl: "/" }); + }; + + const onSubmit = async (values: z.infer) => { + if (session.status === "authenticated") { + toast.error("You are already logged in!"); + return; + } + try { + event?.preventDefault(); + + await axios.post("/api/auth/signup", { + email: values.email, + password: values.password, + name: values.name, + }); + + const result = await signIn("credentials", { + email: values.email, + password: values.password, + redirect: false, + }); + + if (result?.error) { + throw new Error(result.error); + } + + form.reset(); + toast.success("Successfully created account!"); + router.refresh(); + } catch (error) { + const errorMessage = axios.isAxiosError(error) + ? error.response?.data || error.message + : "An error occurred"; + setError(errorMessage); + toast.error(errorMessage); + } + }; + + return ( + + + Create an account + Enter your email to access. + + +
+ +
+ ( + + Name + + + + + + )} + /> + ( + + Email + + + + + + )} + /> +
+ ( + + Password + + + + + + )} + /> + + + +
+
+
+ +
+
+ + Or continue with + +
+
+ +
+ + +
+
+ {/* if logged in card should show persons name and saying hello */} +
+ ); +} diff --git a/core/auth/data/account.ts b/core/auth/data/account.ts new file mode 100644 index 0000000..49427b3 --- /dev/null +++ b/core/auth/data/account.ts @@ -0,0 +1,13 @@ +import { db } from "@/lib/db"; + +export const getAccountByUserId = async (userId: string) => { + try { + const account = await db.account.findFirst({ + where: { userId } + }); + + return account; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/password-reset-token.ts b/core/auth/data/password-reset-token.ts new file mode 100644 index 0000000..677c730 --- /dev/null +++ b/core/auth/data/password-reset-token.ts @@ -0,0 +1,25 @@ +import { db } from "@/lib/db"; + +export const getPasswordResetTokenByToken = async (token: string) => { + try { + const passwordResetToken = await db.passwordResetToken.findUnique({ + where: { token } + }); + + return passwordResetToken; + } catch { + return null; + } +}; + +export const getPasswordResetTokenByEmail = async (email: string) => { + try { + const passwordResetToken = await db.passwordResetToken.findFirst({ + where: { email } + }); + + return passwordResetToken; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/two-factor-confirmation.ts b/core/auth/data/two-factor-confirmation.ts new file mode 100644 index 0000000..e3eb4f6 --- /dev/null +++ b/core/auth/data/two-factor-confirmation.ts @@ -0,0 +1,15 @@ +import { db } from "@/lib/db"; + +export const getTwoFactorConfirmationByUserId = async ( + userId: string +) => { + try { + const twoFactorConfirmation = await db.twoFactorConfirmation.findUnique({ + where: { userId } + }); + + return twoFactorConfirmation; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/two-factor-token.ts b/core/auth/data/two-factor-token.ts new file mode 100644 index 0000000..48f9e00 --- /dev/null +++ b/core/auth/data/two-factor-token.ts @@ -0,0 +1,25 @@ +import { db } from "@/lib/db"; + +export const getTwoFactorTokenByToken = async (token: string) => { + try { + const twoFactorToken = await db.twoFactorToken.findUnique({ + where: { token } + }); + + return twoFactorToken; + } catch { + return null; + } +}; + +export const getTwoFactorTokenByEmail = async (email: string) => { + try { + const twoFactorToken = await db.twoFactorToken.findFirst({ + where: { email } + }); + + return twoFactorToken; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/user.ts b/core/auth/data/user.ts new file mode 100644 index 0000000..0710961 --- /dev/null +++ b/core/auth/data/user.ts @@ -0,0 +1,21 @@ +import { db } from "@/lib/db"; + +export const getUserByEmail = async (email: string) => { + try { + const user = await db.user.findUnique({ where: { email } }); + + return user; + } catch { + return null; + } +}; + +export const getUserById = async (id: string) => { + try { + const user = await db.user.findUnique({ where: { id } }); + + return user; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/verification-token.ts b/core/auth/data/verification-token.ts new file mode 100644 index 0000000..2db9ad2 --- /dev/null +++ b/core/auth/data/verification-token.ts @@ -0,0 +1,29 @@ +import { db } from "@/lib/db"; + +export const getVerificationTokenByToken = async ( + token: string +) => { + try { + const verificationToken = await db.verificationToken.findUnique({ + where: { token } + }); + + return verificationToken; + } catch { + return null; + } +} + +export const getVerificationTokenByEmail = async ( + email: string +) => { + try { + const verificationToken = await db.verificationToken.findFirst({ + where: { email } + }); + + return verificationToken; + } catch { + return null; + } +} \ No newline at end of file diff --git a/core/auth/hooks/use-current-role.ts b/core/auth/hooks/use-current-role.ts new file mode 100644 index 0000000..dd485c1 --- /dev/null +++ b/core/auth/hooks/use-current-role.ts @@ -0,0 +1,7 @@ +import { useSession } from "next-auth/react"; + +export const useCurrentRole = () => { + const session = useSession(); + + return session.data?.user?.role ; +}; \ No newline at end of file diff --git a/core/auth/hooks/use-current-user.ts b/core/auth/hooks/use-current-user.ts new file mode 100644 index 0000000..7a328da --- /dev/null +++ b/core/auth/hooks/use-current-user.ts @@ -0,0 +1,7 @@ +import { useSession } from "next-auth/react"; + +export const useCurrentUser = () => { + const session = useSession(); + + return session.data?.user; +}; \ No newline at end of file diff --git a/core/auth/lib/auth.ts b/core/auth/lib/auth.ts new file mode 100644 index 0000000..3c95273 --- /dev/null +++ b/core/auth/lib/auth.ts @@ -0,0 +1,13 @@ +import { auth } from "@/auth"; + +export const currentUser = async () => { + const session = await auth(); + + return session?.user; +}; + +export const currentRole = async () => { + const session = await auth(); + + return session?.user?.role; +}; \ No newline at end of file diff --git a/core/auth/lib/mail.ts b/core/auth/lib/mail.ts new file mode 100644 index 0000000..80431e5 --- /dev/null +++ b/core/auth/lib/mail.ts @@ -0,0 +1,36 @@ +import { Resend } from 'resend'; + +const resend = new Resend(process.env.RESEND_API_KEY); + +const domain = process.env.NEXT_PUBLIC_APP_URL; + +export const sendTwoFactorTokenEmail = async (email: string, token: string) => { + await resend.emails.send({ + from: 'Acme ', + to: email, + subject: '2FA Code', + html: `

Your 2FA code: ${token}

`, + }); +}; + +export const sendPasswordResetEmail = async (email: string, token: string) => { + const resetLink = `${domain}/auth/new-password?token=${token}`; + + await resend.emails.send({ + from: 'Acme ', + to: email, + subject: 'Reset your password', + html: `

Click here to reset password.

`, + }); +}; + +export const sendVerificationEmail = async (email: string, token: string) => { + const confirmLink = `${domain}/auth/new-verification?token=${token}`; + + await resend.emails.send({ + from: 'Acme ', + to: email, + subject: 'Confirm your email', + html: `

Click here to confirm email.

`, + }); +}; \ No newline at end of file diff --git a/core/auth/lib/token.ts b/core/auth/lib/token.ts new file mode 100644 index 0000000..537f52e --- /dev/null +++ b/core/auth/lib/token.ts @@ -0,0 +1,80 @@ +import crypto from "crypto"; +import { v4 as uuidv4 } from "uuid"; + +import { db } from "@/lib/db"; +import { getVerificationTokenByEmail } from "../data/verification-token"; +import { getPasswordResetTokenByEmail } from "../data/password-reset-token"; +import { getTwoFactorTokenByEmail } from "../data/two-factor-token"; + +export const generateTwoFactorToken = async (email: string) => { + const token = crypto.randomInt(100_000, 1_000_000).toString(); + const expires = new Date(new Date().getTime() + 5 * 60 * 1000); + + const existingToken = await getTwoFactorTokenByEmail(email); + + if (existingToken) { + await db.twoFactorToken.delete({ + where: { + id: existingToken.id, + }, + }); + } + + const twoFactorToken = await db.twoFactorToken.create({ + data: { + email, + token, + expires, + }, + }); + + return twoFactorToken; +}; + +export const generatePasswordResetToken = async (email: string) => { + const token = uuidv4(); + const expires = new Date(new Date().getTime() + 3600 * 1000); + + const existingToken = await getPasswordResetTokenByEmail(email); + + if (existingToken) { + await db.passwordResetToken.delete({ + where: { id: existingToken.id }, + }); + } + + const passwordResetToken = await db.passwordResetToken.create({ + data: { + email, + token, + expires, + }, + }); + + return passwordResetToken; +}; + +export const generateVerificationToken = async (email: string) => { + const token = uuidv4(); + const expires = new Date(new Date().getTime() + 3600 * 1000); + + const existingToken = await getVerificationTokenByEmail(email); + + if (existingToken) { + await db.verificationToken.delete({ + where: { + id: existingToken.id, + }, + }); + } + + const verificationToken = await db.verificationToken.create({ + data: { + email, + token, + expires, + }, + }); + + return verificationToken; +}; diff --git a/core/auth/lib/utils.ts b/core/auth/lib/utils.ts new file mode 100644 index 0000000..1a860ee --- /dev/null +++ b/core/auth/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} \ No newline at end of file diff --git a/core/auth/metadata.json b/core/auth/metadata.json new file mode 100644 index 0000000..e260817 --- /dev/null +++ b/core/auth/metadata.json @@ -0,0 +1,58 @@ +{ + "name": "auth", + "files": [ + "actions/admin.ts", + "actions/login.ts", + "actions/logout.ts", + "actions/new-password.ts", + "actions/new-verification.ts", + "actions/register.ts", + "actions/reset.ts", + "actions/settings.ts", + "app/api/admin/route.ts", + "app/api/auth/[...nextauth]/route.ts", + "auth.config.ts", + "auth.ts", + "components/auth/auth.tsx", + "components/auth/logout-button.tsx", + "components/auth/user-button.tsx", + "components/ui/avatar.tsx", + "components/ui/examples/auth-example.tsx", + "data/account.ts", + "data/password-reset-token.ts", + "data/two-factor-confirmation.ts", + "data/two-factor-token.ts", + "data/user.ts", + "data/verification-token.ts", + "hooks/use-current-role.ts", + "hooks/use-current-user.ts", + "lib/auth.ts", + "lib/db.ts", + "lib/mail.ts", + "lib/token.ts", + "next-auth.d.ts", + "prisma/schema.prisma", + "routes.ts", + "schemas/index.ts" + ], + "envVariables": [ + "NEXTAUTH_SECRET", + "NEXTAUTH_URL", + "GOOGLE_CLIENT_ID", + "GOOGLE_CLIENT_SECRET", + "GITHUB_CLIENT_ID", + "GITHUB_CLIENT_SECRET" + ], + "dependencies": { + "@auth/prisma-adapter": "^1.0.12", + "@hookform/resolvers": "^3.9.1", + "bcrypt": "^5.1.1", + "next-auth": "^5.0.0-beta.4", + "resend": "^4.1.1", + "uuid": "^11.0.5", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6" + } +} \ No newline at end of file diff --git a/core/auth/next-auth.d.ts b/core/auth/next-auth.d.ts new file mode 100644 index 0000000..7246b9f --- /dev/null +++ b/core/auth/next-auth.d.ts @@ -0,0 +1,14 @@ +import { UserRole } from '@prisma/client'; +import NextAuth, { type DefaultSession } from 'next-auth'; + +export type ExtendedUser = DefaultSession['user'] & { + role: UserRole; + isTwoFactorEnabled: boolean; + isOAuth: boolean; +}; + +declare module 'next-auth' { + interface Session { + user: ExtendedUser; + } +} diff --git a/core/auth/prisma/schema.prisma b/core/auth/prisma/schema.prisma new file mode 100644 index 0000000..2a051f8 --- /dev/null +++ b/core/auth/prisma/schema.prisma @@ -0,0 +1,82 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + + +enum UserRole { + ADMIN + USER +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + password String? + role UserRole @default(USER) + accounts Account[] + isTwoFactorEnabled Boolean @default(false) + twoFactorConfirmation TwoFactorConfirmation? +} + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model VerificationToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + +model PasswordResetToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + +model TwoFactorToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + +model TwoFactorConfirmation { + id String @id @default(cuid()) + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId]) +} \ No newline at end of file diff --git a/core/auth/routes.ts b/core/auth/routes.ts new file mode 100644 index 0000000..9d513ac --- /dev/null +++ b/core/auth/routes.ts @@ -0,0 +1,35 @@ +/** + * An array of routes that are accessible to the public + * These routes do not require authentication + * @type {string[]} + */ +export const publicRoutes = [ + "/", + "/auth/new-verification" + ]; + + /** + * An array of routes that are used for authentication + * These routes will redirect logged in users to /settings + * @type {string[]} + */ + export const authRoutes = [ + "/auth/login", + "/auth/register", + "/auth/error", + "/auth/reset", + "/auth/new-password" + ]; + + /** + * The prefix for API authentication routes + * Routes that start with this prefix are used for API authentication purposes + * @type {string} + */ + export const apiAuthPrefix = "/api/auth"; + + /** + * The default redirect path after logging in + * @type {string} + */ + export const DEFAULT_LOGIN_REDIRECT = "/settings"; \ No newline at end of file diff --git a/core/auth/schemas/index.ts b/core/auth/schemas/index.ts new file mode 100644 index 0000000..1a586a9 --- /dev/null +++ b/core/auth/schemas/index.ts @@ -0,0 +1,65 @@ +import * as z from "zod"; +import { UserRole } from "@prisma/client"; + +export const SettingsSchema = z.object({ + name: z.optional(z.string()), + isTwoFactorEnabled: z.optional(z.boolean()), + role: z.enum([UserRole.ADMIN, UserRole.USER]), + email: z.optional(z.string().email()), + password: z.optional(z.string().min(6)), + newPassword: z.optional(z.string().min(6)), +}) + .refine((data) => { + if (data.password && !data.newPassword) { + return false; + } + + return true; + }, { + message: "New password is required!", + path: ["newPassword"] + }) + .refine((data) => { + if (data.newPassword && !data.password) { + return false; + } + + return true; + }, { + message: "Password is required!", + path: ["password"] + }) + +export const NewPasswordSchema = z.object({ + password: z.string().min(6, { + message: "Minimum of 6 characters required", + }), +}); + +export const ResetSchema = z.object({ + email: z.string().email({ + message: "Email is required", + }), +}); + +export const LoginSchema = z.object({ + email: z.string().email({ + message: "Email is required", + }), + password: z.string().min(1, { + message: "Password is required", + }), + code: z.optional(z.string()), +}); + +export const RegisterSchema = z.object({ + email: z.string().email({ + message: "Email is required", + }), + password: z.string().min(6, { + message: "Minimum 6 characters required", + }), + name: z.string().min(1, { + message: "Name is required", + }), +}); \ No newline at end of file diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json new file mode 100644 index 0000000..56d2f9b --- /dev/null +++ b/packages/cli/package-lock.json @@ -0,0 +1,308 @@ +{ + "name": "@libui-next/cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@libui-next/cli", + "version": "1.0.0", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^13.0.0", + "nanospinner": "^1.1.0" + }, + "bin": { + "libui-next": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^20.4.2", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.17.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.12.tgz", + "integrity": "sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", + "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/nanospinner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz", + "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==", + "dependencies": { + "picocolors": "^1.1.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..875e16c --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,24 @@ +{ + "name": "@libui-next/cli", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build:cli": "tsc && chmod +x dist/index.js", + "start": "ts-node src/index.ts", + "prepare": "npm run build" + }, + "bin": { + "libui-next": "dist/index.js" + }, + "dependencies": { + "chalk": "^4.1.2", + "commander": "^13.0.0", + "nanospinner": "^1.1.0" + }, + "devDependencies": { + "@types/node": "^20.4.2", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" + } +} diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts new file mode 100644 index 0000000..778e1a6 --- /dev/null +++ b/packages/cli/src/commands/add.ts @@ -0,0 +1,27 @@ +import { Command } from 'commander'; +import { addComponent } from '../utils/add-utils'; +import { logger } from '../utils/visuals'; +import { handleError } from '../utils/errors'; +import { exists } from '../utils/files'; + +export const addCommand = new Command('add') + .description('Add a new libui component to your project') + .argument('', 'Name of the component to add') + .action(async (componentName: string) => { + try { + // Initial validation + if (componentName !== 'auth') { + logger.error('Only the "auth" component can be added at this time.'); + process.exit(1); + } + if (!(await exists('libui.config.json'))) { + throw new Error('libui.config.json not found. Please run `libui-next init` first.'); + } + + // Run core command logic + await addComponent(componentName); + + } catch (error) { + handleError(error); + } + }); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts new file mode 100644 index 0000000..7297d3d --- /dev/null +++ b/packages/cli/src/commands/init.ts @@ -0,0 +1,23 @@ +import { Command } from 'commander'; +import { runInit } from '../utils/init-utils'; +import { logger } from '../utils/visuals'; +import { handleError } from '../utils/errors'; +import { exists } from '../utils/files'; + +export const initCommand = new Command('init') + .description('Setup libui in your project') + .action(async () => { + try { + // Initial validation + if (await exists('libui.config.json')) { + logger.error('libui.config.json already exists in this directory. Initialization aborted.'); + process.exit(1); + } + + // Run core command logic + await runInit(); + + } catch (error) { + handleError(error); + } + }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..e86f0a6 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import { initCommand } from './commands/init'; +import { addCommand } from './commands/add'; + +const program = new Command(); + +program + .name('libui-next') + .description('add full-stack components to your project') + .version('1.0.0'); + +program.addCommand(initCommand); +program.addCommand(addCommand); + +program.parse(process.argv); \ No newline at end of file diff --git a/packages/cli/src/types/metadata.ts b/packages/cli/src/types/metadata.ts new file mode 100644 index 0000000..ce13ed4 --- /dev/null +++ b/packages/cli/src/types/metadata.ts @@ -0,0 +1,6 @@ +export interface Metadata { + files: string[]; + envVariables: string[]; + dependencies: Record; + devDependencies: Record; +} \ No newline at end of file diff --git a/packages/cli/src/utils/add-utils.ts b/packages/cli/src/utils/add-utils.ts new file mode 100644 index 0000000..690c5bc --- /dev/null +++ b/packages/cli/src/utils/add-utils.ts @@ -0,0 +1,126 @@ +import path from 'path'; +import fs from 'fs/promises'; +import axios from 'axios'; +import { logger, spinner } from './visuals'; +import { Metadata } from '../types/metadata'; +import { exists } from './files'; + +export async function addComponent(componentName: string) { + const spin = spinner(`Adding ${componentName} component...\n`); + try { + // Validate libui.config.json exists and the component is not already added + if (!await exists('libui.config.json')) { + throw new Error('libui.config.json not found in the current directory'); + } + const config = JSON.parse(await fs.readFile('libui.config.json', 'utf8')); + if (componentName in config.addedComponents) { + throw new Error(`${componentName} component already added.`); + } + + // Validate package.json exists + if (!await exists('package.json')) { + throw new Error('package.json not found in the current directory'); + } + + spin.start(); + + const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/cli'; + const basePath = `core/${componentName}`; + + logger.log(`Fetching component metadata...`); + const response = await axios.get(`${remoteRepoBaseURL}/${basePath}/metadata.json`); + if (!response.data) { + throw new Error(`Failed to fetch metadata.json`); + } + const metadata: Metadata = response.data; + + // Validate that none of the target directories exist + logger.log('Checking if files already exist...'); + for (const file of metadata.files) { + if (await exists(file)) { + throw new Error(`File ${file} already exists`); + } + } + + // Download all files first + const downloadedFiles = await Promise.all( + metadata.files.map(async (file) => { + const remoteFileURL = `${remoteRepoBaseURL}/${file}`; + + logger.log(`Downloading file from ${remoteFileURL}`); + const response = await axios.get(remoteFileURL, { responseType: 'text' }); + if (!response.data) { + throw new Error(`No content retrieved from ${remoteFileURL}`); + } + return { file, content: response.data }; + }) + ); + + // Create directories and write files + for (const { file, content } of downloadedFiles) { + const directory = path.dirname(file); + + await fs.mkdir(directory, { recursive: true }); + await fs.writeFile(file, content, 'utf-8'); + logger.log(`Added ${file} to ${file}`); + } + + // Add dependencies to package.json + const packageJson = await fs.readFile('package.json', 'utf-8'); + const packageJsonObject = JSON.parse(packageJson); + + // Handle dependencies + packageJsonObject.dependencies = packageJsonObject.dependencies || {}; + for (const [dep, version] of Object.entries(metadata.dependencies)) { + if (packageJsonObject.dependencies[dep]) { + if (packageJsonObject.dependencies[dep] === version) { + logger.info(`Dependency ${dep}@${version} already exists in package.json`); + } else { + logger.warn(`Dependency ${dep} exists with different version: ${packageJsonObject.dependencies[dep]} (wanted: ${version})`); + } + } else { + packageJsonObject.dependencies[dep] = version; + } + } + + // Handle devDependencies + packageJsonObject.devDependencies = packageJsonObject.devDependencies || {}; + for (const [dep, version] of Object.entries(metadata.devDependencies)) { + if (packageJsonObject.devDependencies[dep]) { + if (packageJsonObject.devDependencies[dep] === version) { + logger.info(`DevDependency ${dep}@${version} already exists in package.json`); + } else { + logger.warn(`DevDependency ${dep} exists with different version: ${packageJsonObject.devDependencies[dep]} (wanted: ${version}). Skipping...`); + } + } else { + packageJsonObject.devDependencies[dep] = version; + } + } + + // Add env variables + const envFile = await fs.readFile('.env', 'utf-8'); + for (const envVar of metadata.envVariables) { + const regex = new RegExp(`^${envVar}\\s*=\\s*`, 'm'); + if (regex.test(envFile)) { + logger.warn(`Env variable ${envVar} already exists in .env file`); + } else { + await fs.writeFile('.env', `\n${envVar}=...\n`, { flag: 'a' }); + } + } + + await fs.writeFile('package.json', JSON.stringify(packageJsonObject, null, 2), 'utf-8'); + + // Add the component to the config + config.addedComponents.push(componentName); + await fs.writeFile('libui.config.json', JSON.stringify(config, null, 2), 'utf-8'); + + spin.succeed('Project updated successfully'); + logger.success(`The ${componentName} component has been added successfully`); + logger.success('1. Update the .env file with the correct values'); + logger.success('2. Run `npm install` to install the new dependencies'); + logger.success('3. Run `npx prisma generate` to update the Prisma Client'); + } catch (error) { + spin.fail(`Failed to add ${componentName} component`); + throw error; + } +} \ No newline at end of file diff --git a/packages/cli/src/utils/errors.ts b/packages/cli/src/utils/errors.ts new file mode 100644 index 0000000..e4e83ad --- /dev/null +++ b/packages/cli/src/utils/errors.ts @@ -0,0 +1,10 @@ +import { logger } from './visuals'; + +export function handleError(error: unknown) { + if (error instanceof Error) { + logger.error(error.message); + } else { + logger.error('An unknown error occurred.'); + } + process.exit(1); +} \ No newline at end of file diff --git a/packages/cli/src/utils/files.ts b/packages/cli/src/utils/files.ts new file mode 100644 index 0000000..af3c6d5 --- /dev/null +++ b/packages/cli/src/utils/files.ts @@ -0,0 +1,12 @@ +import fs from 'fs/promises'; +import path from 'path'; + +export async function exists(relativePath: string): Promise { + const configPath = path.join(process.cwd(), relativePath); + try { + await fs.access(configPath); + return true; + } catch { + return false; + } +} diff --git a/packages/cli/src/utils/init-utils.ts b/packages/cli/src/utils/init-utils.ts new file mode 100644 index 0000000..59dd849 --- /dev/null +++ b/packages/cli/src/utils/init-utils.ts @@ -0,0 +1,123 @@ +import { promises as fs } from 'fs'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { logger, spinner } from './visuals'; + +const execPromise = promisify(exec); + +const helloApiRouteContent = `import { NextResponse } from 'next/server' + +export async function GET() { + return NextResponse.json({ message: 'Hello from the libui API!' }) +} +`; + +const envFileContent = `# This file will contain all the required environment variables for the application +# Make sure to update them every time you add a new component + +# This is a sample database url, you can change it to your own +DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public" +`; + +const utilsContent = `import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} +`; + +export async function runInit() { + // Check if directory is empty + const projectDir = process.cwd(); + const dirContents = await fs.readdir(projectDir); + if (dirContents.length > 0) { + logger.error('Directory is not empty. Please run this command in an empty directory.'); + process.exit(1); + } + + // 1. Create Next.js project + const nextInitSpinner = spinner('Creating Next.js project...').start(); + try { + await execPromise('npx create-next-app@latest . --typescript --use-npm --no-eslint --no-src-dir --no-experimental-app --yes', + { + cwd: projectDir, + env: { ...process.env, FORCE_COLOR: '1' }, + } + ); + nextInitSpinner.success('Next.js project created.'); + } catch (error) { + nextInitSpinner.error('Failed to create Next.js project.'); + throw error; + } + + // 2. Install Prisma + const prismaSpinner = spinner('Installing Prisma...').start(); + try { + await execPromise('npm install -D prisma @prisma/client', { cwd: projectDir }); + prismaSpinner.success('Prisma installed.'); + } catch (error) { + prismaSpinner.error('Failed to install Prisma.'); + throw error; + } + + // 3. Create API directory structure + const apiSpinner = spinner('Creating API structure...').start(); + try { + await fs.mkdir('app/api/hello', { recursive: true }); + await fs.writeFile('app/api/hello/route.ts', helloApiRouteContent, 'utf8'); + apiSpinner.success('API structure created.'); + } catch (error) { + apiSpinner.error('Failed to create API structure.'); + throw error; + } + + // 4. Create .env file + const envSpinner = spinner('Creating environment file...').start(); + try { + await fs.writeFile('.env', envFileContent, 'utf8'); + envSpinner.success('Environment file created.'); + } catch (error) { + envSpinner.error('Failed to create environment file.'); + throw error; + } + + // 5. Create libui.config.json + const config = { + addedComponents: [], + }; + + try { + const configSpinner = spinner('Writing libui.config.json...').start(); + await fs.writeFile('libui.config.json', JSON.stringify(config, null, 2), 'utf8'); + configSpinner.success('libui.config.json written successfully.'); + } catch (error) { + logger.error('Failed to write libui.config.json.'); + throw error; + } + + // 6. Install additional dependencies + const dependenciesSpinner = spinner('Installing additional dependencies...').start(); + try { + await execPromise('npm install react react-dom next @prisma/client clsx tailwind-merge', { cwd: projectDir }); + dependenciesSpinner.success('Additional dependencies installed.'); + } catch (error) { + dependenciesSpinner.error('Failed to install additional dependencies.'); + throw error; + } + + // 7. Create utils directory and cn helper + const utilsSpinner = spinner('Creating utils directory and helpers...').start(); + try { + await fs.mkdir('lib', { recursive: true }); + await fs.writeFile('lib/utils.ts', utilsContent, 'utf8'); + utilsSpinner.success('Utils directory and helpers created.'); + } catch (error) { + utilsSpinner.error('Failed to create utils directory and helpers.'); + throw error; + } + + logger.success('Initialization complete! You can now:'); + logger.success('1. Update your .env file with your database credentials'); + logger.success('2. Start your development server with `npm run dev`'); +} diff --git a/packages/cli/src/utils/visuals.ts b/packages/cli/src/utils/visuals.ts new file mode 100644 index 0000000..9fb42e6 --- /dev/null +++ b/packages/cli/src/utils/visuals.ts @@ -0,0 +1,38 @@ +import chalk from 'chalk'; +import { createSpinner } from 'nanospinner' + +export const logger = { + log: (message: string) => { + console.log(chalk.grey(message)); + }, + info: (message: string) => { + console.info(chalk.blue(message)); + }, + warn: (message: string) => { + console.warn(chalk.yellow(message)); + }, + error: (message: string) => { + console.error(chalk.red(message)); + }, + success: (message: string) => { + console.log(chalk.green(message)); + }, +}; + +export function spinner(message: string) { + const sp = createSpinner(message); + return { + start: () => { + sp.start(); + return sp; + }, + succeed: (text?: string) => { + sp.success({ text }); + return sp; + }, + fail: (text?: string) => { + sp.error({ text }); + return sp; + }, + }; +} \ No newline at end of file diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..12a98ef --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "moduleResolution": "node", + "skipLibCheck": true + }, + "include": ["src/**/*"] +}