├── .env.development ├── .env.example ├── .eslintignore ├── .gitattributes ├── .github └── workflows │ ├── MERGE_MASTER.yml │ └── PR.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── apps ├── styleguide │ ├── .eslintrc.cjs │ ├── .lintstagedrc │ ├── .storybook │ │ ├── main.ts │ │ └── preview.ts │ ├── package.json │ ├── postcss.config.cjs │ ├── src │ │ ├── app.html │ │ ├── app.postcss │ │ └── stories │ │ │ └── Introduction.mdx │ ├── svelte.config.js │ ├── tailwind.config.ts │ ├── vite.config.ts │ └── wrangler.toml ├── swagger-ui │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .lintstagedrc │ ├── .prettierignore │ ├── package.json │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── lib │ │ │ └── index.ts │ │ └── routes │ │ │ └── +page.svelte │ ├── static │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ ├── svelte.config.js │ ├── tsconfig.json │ ├── vite.config.ts │ └── wrangler.toml └── web │ ├── .eslintrc.cjs │ ├── .lintstagedrc │ ├── package.json │ ├── postcss.config.cjs │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── app.postcss │ ├── error.html │ ├── hooks.server.ts │ ├── lib │ │ ├── api │ │ │ ├── auth-service-api.ts │ │ │ ├── base-api-service.ts │ │ │ └── user-service-api.ts │ │ ├── client │ │ │ └── auth │ │ │ │ └── schemas.ts │ │ ├── components │ │ │ ├── molecues │ │ │ │ └── user │ │ │ │ │ ├── user-card-list.svelte │ │ │ │ │ ├── user-card-list.test.ts │ │ │ │ │ └── user-details.svelte │ │ │ ├── organisms │ │ │ │ ├── modals │ │ │ │ │ ├── authenticate-modal.svelte │ │ │ │ │ ├── error-modal.svelte │ │ │ │ │ └── register-modal.svelte │ │ │ │ └── navbar │ │ │ │ │ ├── protected-navbar.svelte │ │ │ │ │ └── public-navbar.svelte │ │ │ ├── root-conainer.svelte │ │ │ └── sections │ │ │ │ ├── about-this-project.svelte │ │ │ │ ├── auth-flow.svelte │ │ │ │ ├── data-proxy.svelte │ │ │ │ ├── github-actions.svelte │ │ │ │ ├── pr.svelte │ │ │ │ ├── prerequisites.svelte │ │ │ │ ├── prisma-code-org.svelte │ │ │ │ ├── road-map.svelte │ │ │ │ ├── run-local.svelte │ │ │ │ ├── run-prod.svelte │ │ │ │ ├── scripts.svelte │ │ │ │ ├── tech.svelte │ │ │ │ └── welcome.svelte │ │ ├── queries │ │ │ └── fetch-random-avatar-query-config.ts │ │ ├── server │ │ │ ├── auth │ │ │ │ └── lucia.ts │ │ │ ├── errors │ │ │ │ └── index.ts │ │ │ ├── middleware │ │ │ │ ├── rate-limiter.ts │ │ │ │ └── validate.ts │ │ │ ├── repository │ │ │ │ ├── image-repository.ts │ │ │ │ ├── prisma-client.ts │ │ │ │ └── user-repository.ts │ │ │ └── services │ │ │ │ └── user-service.ts │ │ └── stores │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ └── user-store.ts │ └── routes │ │ ├── (protected) │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ ├── api │ │ │ └── v1 │ │ │ │ └── user │ │ │ │ ├── +server.ts │ │ │ │ └── [userId] │ │ │ │ └── +server.ts │ │ └── profile │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── (public) │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ └── api │ │ │ └── v1 │ │ │ └── auth │ │ │ └── +server.ts │ │ ├── +error.svelte │ │ ├── +layout.svelte │ │ └── +layout.ts │ ├── static │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── fonts │ │ └── Quicksand.ttf │ ├── icons │ │ ├── LICENSE │ │ ├── add.svg │ │ ├── admin.svg │ │ ├── alert.svg │ │ ├── briefcase.svg │ │ ├── browsers.svg │ │ ├── cal.svg │ │ ├── checkFalse.svg │ │ ├── checkTrue.svg │ │ ├── close.svg │ │ ├── communityPlaceholder.svg │ │ ├── edit.svg │ │ ├── eye.svg │ │ ├── kebab-menu.svg │ │ ├── loading.svg │ │ ├── logo.png │ │ ├── mapPin.svg │ │ ├── member.svg │ │ ├── menu.svg │ │ ├── package.svg │ │ ├── placeholder.svg │ │ ├── printer.svg │ │ ├── save.svg │ │ ├── upload.svg │ │ └── users.svg │ ├── images │ │ ├── placeholder-community-logo.webp │ │ └── placeholder.png │ ├── robots.txt │ └── site.webmanifest │ ├── svelte.config.js │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── vite.config.ts │ ├── vitest-setup.ts │ └── wrangler.toml ├── package.json ├── packages ├── api-domain │ ├── .eslintrc.cjs │ ├── .lintstagedrc │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── api-mocks │ ├── docker-compose.yaml │ ├── package.json │ └── sk-db-dump.sql ├── db-schema │ ├── .eslintrc.cjs │ ├── .lintstagedrc │ ├── package.json │ ├── prisma │ │ ├── schema.prisma │ │ ├── seed.js │ │ └── seedUtils.js │ ├── src │ │ ├── index.ts │ │ └── utils │ │ │ └── index.ts │ └── tsconfig.json ├── eslint-config │ ├── .lintstagedrc │ ├── index.cjs │ ├── package.json │ └── vite.config.ts ├── service-contract │ ├── .eslintrc.cjs │ ├── .lintstagedrc │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── models.ts │ ├── tsconfig.json │ └── vite.config.ts ├── tests │ ├── .eslintrc.cjs │ ├── .lintstagedrc │ ├── e2e │ │ ├── managing-groups.spec.ts │ │ └── user-auth-flows.spec.ts │ ├── package.json │ ├── playwright.config.ci.ts │ ├── playwright.config.dev.ts │ ├── tsconfig.json │ └── unit │ │ └── index.spec.ts └── ui-core │ ├── .eslintrc.cjs │ ├── .lintstagedrc │ ├── package.json │ ├── src │ ├── api │ │ └── base-api-client.ts │ ├── app.html │ ├── cache │ │ └── index.ts │ ├── components │ │ ├── buttons │ │ │ ├── index.ts │ │ │ ├── navigation-button.stories.svelte │ │ │ ├── navigation-button.svelte │ │ │ ├── simple-button.stories.svelte │ │ │ ├── simple-button.svelte │ │ │ └── simple-button.test.ts │ │ ├── card │ │ │ ├── card-container.svelte │ │ │ └── card.svelte │ │ ├── forms │ │ │ ├── input-types.ts │ │ │ ├── text-edit.svelte │ │ │ ├── text-input-area.svelte │ │ │ ├── text-input-autocomplete.svelte │ │ │ └── text-input.svelte │ │ ├── icons │ │ │ ├── icon-a-i.svelte │ │ │ ├── icon-action-menu.svelte │ │ │ ├── icon-add.svelte │ │ │ ├── icon-admin.svelte │ │ │ ├── icon-arrow-right.svelte │ │ │ ├── icon-brief-case.svelte │ │ │ ├── icon-calendar.svelte │ │ │ ├── icon-camera.svelte │ │ │ ├── icon-check-false.svelte │ │ │ ├── icon-check-true.svelte │ │ │ ├── icon-close.svelte │ │ │ ├── icon-dle.svelte │ │ │ ├── icon-edit.svelte │ │ │ ├── icon-eye.svelte │ │ │ ├── icon-groups.svelte │ │ │ ├── icon-image-placeholder.svelte │ │ │ ├── icon-kebab-menu.svelte │ │ │ ├── icon-loading.svelte │ │ │ ├── icon-map-pin.svelte │ │ │ ├── icon-member.svelte │ │ │ ├── icon-menu.svelte │ │ │ ├── icon-package.svelte │ │ │ ├── icon-placeholder.svelte │ │ │ ├── icon-print.svelte │ │ │ ├── icon-q-r-code.svelte │ │ │ ├── icon-save.svelte │ │ │ ├── icon-search.svelte │ │ │ ├── icon-upload.svelte │ │ │ ├── icon-users.svelte │ │ │ ├── icon-view.svelte │ │ │ └── index.ts │ │ ├── lists │ │ │ └── list-tem.svelte │ │ ├── media │ │ │ ├── avatar.svelte │ │ │ ├── image-resource.svelte │ │ │ └── qr-code.svelte │ │ ├── modals │ │ │ └── index.ts │ │ ├── nav │ │ │ ├── link.svelte │ │ │ └── navbar-link.svelte │ │ ├── skeletons │ │ │ └── skeleton-card-list.svelte │ │ └── typography │ │ │ ├── typography.svelte │ │ │ └── typography.utils.ts │ ├── constants │ │ └── index.ts │ ├── index.ts │ ├── model │ │ └── index.ts │ ├── routes │ │ └── +page.svelte │ ├── routing │ │ └── index.ts │ ├── schemas │ │ └── index.ts │ └── utils │ │ └── index.ts │ ├── svelte.config.js │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest-setup.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── tsconfig-base.json ├── turbo.json └── utils ├── createCloudflareProject.js └── loadImagesToCloudflare.js /.env.development: -------------------------------------------------------------------------------- 1 | ############ 2 | ### DEV ### 3 | ############ 4 | # DB 5 | DATABASE_URL="postgres://admin:pass123@localhost:7500/cloudkit-db" 6 | RATE_LIMIT="ngDPzgdE2apsLdSXLLnQYjbgNkt8ODeX36mq1hnK3NcfmVYwvMrqqkKgHLHNcLdjkcjdaIK9yoAzx0NLf5GLPfYVHbcfWCt83aSet88kgxkAhySGBCwvVuJE4aSRKtOd" 7 | # Images with docker-compose for local development 8 | PUBLIC_IMAGE_API_URL="http://localhost:7501/image" 9 | IMAGE_API_TOKEN="" # Only used in Prod builds 10 | IMAGE_API_ACCOUNT_IDENTIFIER="" # Only used in Prod builds 11 | 12 | 13 | IS_CI=false 14 | 15 | # prisma seed variables 16 | SEED_DEV=true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ############ 2 | ### DEV ### 3 | ############ 4 | # DB 5 | DATABASE_URL="postgres://admin:pass123@localhost:6500/sk-db" 6 | DATA_PROXY="" # Only used in Prod builds 7 | 8 | # REDIS 9 | REDIS_URL="redis://default:redispw@localhost:6379" 10 | REDIS_TOKEN="" # Only used in Prod builds 11 | 12 | # Images: Cloudflare 13 | PUBLIC_IMAGE_API_TOKEN="" # Only used in Prod builds 14 | PUBLIC_IMAGE_API="http://localhost:6501/image" 15 | PUBLIC_IMAGE_DELIVERY="http://localhost:6501/image" 16 | 17 | IS_CI=false 18 | 19 | # prisma seed variables 20 | SEED_DEV=true 21 | 22 | ## USED FOR CREATING THE CLOUDFLARE PROJECT 23 | ACCOUNT_ID="" 24 | CLOUDFLARE_API_TOKEN="" 25 | CLOUDFLARE_PROJECT_NAME="" 26 | GITHUB_PROJECT_OWNER="" 27 | GITHUB_REPO_NAME="" 28 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | packages/eslint-config 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | #common settings that generally should always be used with your language specific settings 2 | 3 | # Auto detect text files and perform LF normalization 4 | # http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # Documents 12 | *.doc diff=astextplain 13 | *.DOC diff=astextplain 14 | *.docx diff=astextplain 15 | *.DOCX diff=astextplain 16 | *.dot diff=astextplain 17 | *.DOT diff=astextplain 18 | *.pdf diff=astextplain 19 | *.PDF diff=astextplain 20 | *.rtf diff=astextplain 21 | *.RTF diff=astextplain 22 | *.md text 23 | *.adoc text 24 | *.textile text 25 | *.mustache text 26 | *.csv text 27 | *.tab text 28 | *.tsv text 29 | *.sql text 30 | 31 | # Graphics 32 | *.png binary 33 | *.jpg binary 34 | *.jpeg binary 35 | *.gif binary 36 | *.ico binary 37 | *.svg text 38 | 39 | # These files are text and should be normalized (Convert crlf => lf) 40 | *.css text 41 | *.df text 42 | *.htm text 43 | *.html text 44 | *.java text 45 | *.js text 46 | *.json text 47 | *.jsp text 48 | *.jspf text 49 | *.tld text 50 | *.txt text 51 | *.xml text 52 | 53 | # set "lf" line ending when checkout 54 | *.properties text eol=lf 55 | *.sh text eol=lf 56 | 57 | # These files are binary and should be left untouched 58 | # (binary is a macro for -text -diff) 59 | *.class binary 60 | *.dll binary 61 | *.ear binary 62 | *.jar binary 63 | *.so binary 64 | *.war binary 65 | 66 | 67 | db/** binary 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | generated 3 | db-schema/src/index.ts // generated by db-schema 4 | node_modules 5 | .wrangler 6 | .svelte-kit 7 | .vscode 8 | build 9 | .env 10 | .env.* 11 | !.env.example 12 | !.env.development 13 | vite.config.js.timestamp-* 14 | vite.config.ts.timestamp-* 15 | playwright-report 16 | test-results 17 | .wrangler./db 18 | ./.wrangler/ 19 | .wrangler 20 | db 21 | docker-images.tar 22 | .env.ci 23 | prisma/generated 24 | .eslintcache 25 | dist 26 | .turbo 27 | .idea 28 | 29 | *storybook.log 30 | storybook-static -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | pnpm lint-staged 4 | 5 | if git diff --cached --name-only | grep -q '\.svelte$'; then 6 | pnpm run -r --filter=@cloudkit/web check 7 | pnpm run -r --filter=@cloudkit/ui-core check 8 | fi 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true # https://pnpm.io/npmrc#engine-strict 2 | auto-install-peers=true # https://pnpm.io/npmrc#auto-install-peers 3 | link-workspace-packages=true # https://pnpm.io/npmrc#link-workspace-packages 4 | shared-workspace-lockfile=true # https://pnpm.io/npmrc#shared-workspace-lockfile 5 | prefer-workspace-packages=true # https://pnpm.io/npmrc#prefer-workspace-packages 6 | save-workspace-protocol=rolling # https://pnpm.io/npmrc#save-workspace-protocol 7 | resolution-mode=highest # https://pnpm.io/npmrc#resolution-mode 8 | node-linker=hoisted # https://pnpm.io/npmrc#node-linker -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.9.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | .svelte-kit 5 | .vscode 6 | .idea 7 | .wrangler 8 | build 9 | .env 10 | .env.* 11 | !.env.example 12 | 13 | # Ignore files for PNPM, NPM and YARN 14 | pnpm-lock.yaml 15 | package-lock.json 16 | .svelte-kit/** 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }], 8 | "htmlWhitespaceSensitivity": "ignore" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.documentSelectors": ["**/*.svelte"], 3 | "tailwindCSS.classAttributes": [ 4 | "class", 5 | "accent", 6 | "active", 7 | "aspectRatio", 8 | "background", 9 | "badge", 10 | "bgBackdrop", 11 | "bgDark", 12 | "bgDrawer", 13 | "bgLight", 14 | "blur", 15 | "border", 16 | "button", 17 | "buttonAction", 18 | "buttonBack", 19 | "buttonClasses", 20 | "buttonComplete", 21 | "buttonDismiss", 22 | "buttonNeutral", 23 | "buttonNext", 24 | "buttonPositive", 25 | "buttonTextCancel", 26 | "buttonTextConfirm", 27 | "buttonTextFirst", 28 | "buttonTextLast", 29 | "buttonTextNext", 30 | "buttonTextPrevious", 31 | "buttonTextSubmit", 32 | "caretClosed", 33 | "caretOpen", 34 | "chips", 35 | "color", 36 | "controlSeparator", 37 | "controlVariant", 38 | "cursor", 39 | "display", 40 | "element", 41 | "fill", 42 | "fillDark", 43 | "fillLight", 44 | "flex", 45 | "gap", 46 | "gridColumns", 47 | "height", 48 | "hover", 49 | "indent", 50 | "justify", 51 | "meter", 52 | "padding", 53 | "position", 54 | "regionBackdrop", 55 | "regionBody", 56 | "regionCaption", 57 | "regionCaret", 58 | "regionCell", 59 | "regionChildren", 60 | "regionCone", 61 | "regionContent", 62 | "regionControl", 63 | "regionDefault", 64 | "regionDrawer", 65 | "regionFoot", 66 | "regionFootCell", 67 | "regionFooter", 68 | "regionHead", 69 | "regionHeadCell", 70 | "regionHeader", 71 | "regionIcon", 72 | "regionInterface", 73 | "regionInterfaceText", 74 | "regionLabel", 75 | "regionLead", 76 | "regionLegend", 77 | "regionList", 78 | "regionNavigation", 79 | "regionPage", 80 | "regionPanel", 81 | "regionRowHeadline", 82 | "regionRowMain", 83 | "regionSummary", 84 | "regionSymbol", 85 | "regionTab", 86 | "regionTrail", 87 | "ring", 88 | "rounded", 89 | "select", 90 | "shadow", 91 | "slotDefault", 92 | "slotFooter", 93 | "slotHeader", 94 | "slotLead", 95 | "slotMessage", 96 | "slotMeta", 97 | "slotPageContent", 98 | "slotPageFooter", 99 | "slotPageHeader", 100 | "slotSidebarLeft", 101 | "slotSidebarRight", 102 | "slotTrail", 103 | "spacing", 104 | "text", 105 | "track", 106 | "width", 107 | "zIndex" 108 | ], 109 | "terminal.integrated.scrollback": 100000, 110 | 111 | "editor.codeActionsOnSave": { 112 | "source.fixAll": "always" 113 | }, 114 | "eslint.validate": [ 115 | "javascript", 116 | "javascriptreact", 117 | { 118 | "language": "svelte", 119 | "autoFix": true 120 | } 121 | ] 122 | } 123 | -------------------------------------------------------------------------------- /apps/styleguide/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@cloudkit/eslint-config"] 3 | }; 4 | -------------------------------------------------------------------------------- /apps/styleguide/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,svelte,css,scss,postcss,md,json,yml}": [ 3 | "prettier --config ../../.prettierrc --write --cache ./src" 4 | ]} 5 | -------------------------------------------------------------------------------- /apps/styleguide/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/sveltekit'; 2 | import { dirname, join } from 'path'; 3 | 4 | /** 5 | * This function is used to resolve the absolute path of a package. 6 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 7 | */ 8 | function getAbsolutePath(value: string) { 9 | return dirname(require.resolve(join(value, 'package.json'))); 10 | } 11 | 12 | const config: StorybookConfig = { 13 | core: { 14 | builder: '@storybook/builder-vite', 15 | disableTelemetry: true 16 | }, 17 | stories: [ 18 | '../src/**/*.mdx', 19 | '../src/**/*.stories.@(js|ts)', 20 | '../../../packages/ui-core/src/**/*.mdx', 21 | '../../../packages/ui-core/src/**/*.stories.@(svelte|ts)', 22 | '../tailwind.config.ts' 23 | ], 24 | addons: [ 25 | '@storybook/addon-svelte-csf', 26 | 'storybook-addon-tailwind-autodocs', 27 | '@storybook/addon-themes', 28 | 'storybook-dark-mode', 29 | getAbsolutePath('@storybook/addon-links'), 30 | getAbsolutePath('@storybook/addon-essentials'), 31 | getAbsolutePath('@storybook/addon-interactions'), 32 | getAbsolutePath('storybook-addon-tailwind-autodocs'), 33 | { 34 | name: 'storybook-addon-sass-postcss', 35 | options: { 36 | sassLoaderOptions: { 37 | implementation: require('postcss') 38 | } 39 | } 40 | } 41 | ], 42 | framework: { 43 | name: '@storybook/sveltekit', 44 | options: {} 45 | } 46 | }; 47 | export default config; 48 | -------------------------------------------------------------------------------- /apps/styleguide/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import { withThemeByDataAttribute } from '@storybook/addon-themes'; 2 | import { Preview, SvelteRenderer } from '@storybook/svelte'; 3 | import '../src/app.postcss'; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | darkMode: { stylePreview: true, classTarget: 'html' }, 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/i 12 | } 13 | } 14 | }, 15 | decorators: [ 16 | withThemeByDataAttribute({ 17 | themes: { 18 | wintry: 'wintry' 19 | }, 20 | defaultTheme: 'wintry', 21 | parentSelector: 'body', 22 | attributeName: 'data-theme' 23 | }) 24 | ] 25 | }; 26 | 27 | export default preview; 28 | -------------------------------------------------------------------------------- /apps/styleguide/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudkit/styleguide", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "storybook dev -p 4200", 9 | "build": "storybook build" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@skeletonlabs/skeleton": "2.10.2", 16 | "@skeletonlabs/tw-plugin": "^0.4.0", 17 | "@storybook/addon-essentials": "^8.3.5", 18 | "@storybook/addon-interactions": "^8.3.5", 19 | "@storybook/addon-links": "^8.3.5", 20 | "@storybook/addon-svelte-csf": "^4.1.7", 21 | "@storybook/addon-themes": "^8.3.5", 22 | "@storybook/blocks": "^8.3.5", 23 | "@storybook/builder-vite": "^8.3.5", 24 | "@storybook/svelte": "^8.3.5", 25 | "@storybook/svelte-vite": "^8.3.5", 26 | "@storybook/sveltekit": "^8.3.5", 27 | "@storybook/test": "^8.3.5", 28 | "@sveltejs/adapter-node": "5.2.2", 29 | "@sveltejs/kit": "^2.7.0", 30 | "@sveltejs/vite-plugin-svelte": "^3.1.2", 31 | "postcss": "^8.4.47", 32 | "postcss-load-config": "^6.0.1", 33 | "sass": "^1.79.5", 34 | "storybook": "^8.3.5", 35 | "storybook-addon-sass-postcss": "^0.3.2", 36 | "storybook-addon-tailwind-autodocs": "^1.0.8", 37 | "storybook-dark-mode": "^4.0.2", 38 | "svelte": "^4.2.19", 39 | "tailwindcss": "^3.4.10", 40 | "vite": "^5.0.3" 41 | }, 42 | "dependencies": { 43 | "@tailwindcss/container-queries": "^0.1.1", 44 | "@tailwindcss/forms": "0.5.9", 45 | "@tailwindcss/typography": "0.5.15" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/styleguide/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /apps/styleguide/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %sveltekit.head% 5 | 6 | 7 | 14 | 15 | 16 |
%sveltekit.body%
17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/styleguide/src/app.postcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @tailwind variants; 5 | @tailwind screens; 6 | 7 | :root [data-theme='wintry'] { 8 | --theme-rounded-base: 4px; 9 | --theme-rounded-container: 2px; 10 | --theme-font-family-base: 'Quicksand', sans-serif; 11 | --theme-font-family-heading: 'Quicksand'; 12 | } 13 | /*place global styles here */ 14 | html, 15 | body { 16 | @apply h-full; 17 | @apply min-h-full; 18 | @apply flex; 19 | @apply flex-col; 20 | } 21 | main { 22 | @apply flex-1; 23 | } 24 | 25 | body { 26 | background-image: radial-gradient( 27 | at 0% 0%, 28 | rgba(var(--color-secondary-500) / 0.33) 0px, 29 | transparent 50% 30 | ), 31 | radial-gradient(at 98% 1%, rgba(var(--color-error-500) / 0.33) 0px, transparent 50%); 32 | background-attachment: fixed; 33 | background-position: center; 34 | background-repeat: no-repeat; 35 | background-size: cover; 36 | } 37 | @font-face { 38 | font-family: 'Quicksand'; 39 | src: url('/fonts/Quicksand.ttf'); 40 | font-display: swap; 41 | } 42 | 43 | [data-popup] { 44 | /* Display */ 45 | display: none; 46 | /* Position */ 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | /* Transitions */ 51 | transition: none; 52 | } 53 | .tree-item-content { 54 | @apply w-full; 55 | } 56 | -------------------------------------------------------------------------------- /apps/styleguide/src/stories/Introduction.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/blocks'; 2 | 3 | 4 |

Welcome to the cloudkit.dle.dev Styleguide!

5 |

6 | This is a styleguide for the cloudkit.dle.dev project. It is a collection of components and 7 | patterns that can be used to build the cloudkit.dle.dev website. It is built using Svelte and 8 | Storybook. 9 |

10 | 11 |

12 | This is currently a work in progress. The components and patterns are being developed and will be 13 | added to the styleguide as they are completed. Some functions (such as the Theme documentation) 14 | are not yet implemented. They will follow at a later date. 15 |

16 | -------------------------------------------------------------------------------- /apps/styleguide/svelte.config.js: -------------------------------------------------------------------------------- 1 | import nodeAdapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | 5 | const config = { 6 | preprocess: [vitePreprocess()], 7 | 8 | kit: { 9 | adapter: nodeAdapter(), 10 | csrf: { 11 | checkOrigin: true 12 | }, 13 | env: { 14 | dir: './../../' 15 | }, 16 | } 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /apps/styleguide/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-expect-error 3 | import forms from '@tailwindcss/forms'; 4 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 5 | // @ts-expect-error 6 | import typography from '@tailwindcss/typography'; 7 | import { join } from 'path'; 8 | import type { Config } from 'tailwindcss'; 9 | 10 | // 1. Import the Skeleton plugin 11 | import { skeleton } from '@skeletonlabs/tw-plugin'; 12 | 13 | const config = { 14 | // 2. Opt for dark mode to be handled via the class method 15 | darkMode: 'class', 16 | content: [ 17 | './src/**/*.{html,js,svelte,ts}', 18 | '../../packages/ui-core/src/**/*.{html,js,svelte,ts}', 19 | // 3. Append the path to the Skeleton package 20 | join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}'), 21 | join( 22 | require.resolve('@skeletonlabs/skeleton'), 23 | '../../../packages/ui-core/src/**/*.{html,js,svelte,ts}' 24 | ) 25 | ], 26 | theme: { 27 | extend: {} 28 | }, 29 | plugins: [ 30 | forms, 31 | typography, 32 | // 4. Append the Skeleton plugin (after other plugins) 33 | require('@tailwindcss/container-queries'), 34 | skeleton({ 35 | themes: { 36 | // Register each theme within this array: 37 | preset: [{ name: 'wintry', enhancements: true }] 38 | } 39 | }) 40 | ] 41 | } satisfies Config; 42 | 43 | export default config; 44 | -------------------------------------------------------------------------------- /apps/styleguide/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | 3 | import { defineConfig } from 'vitest/config'; 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /apps/swagger-ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@cloudkit/eslint-config'] 3 | }; 4 | -------------------------------------------------------------------------------- /apps/swagger-ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /apps/swagger-ui/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,svelte,css,scss,postcss,md,json,yml}": [ 3 | "prettier --config ../../.prettierrc --write --cache ./src" 4 | ], 5 | "*.{ts,svelte,css,scss,yml}": ["eslint --cache --fix"] 6 | } 7 | -------------------------------------------------------------------------------- /apps/swagger-ui/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /apps/swagger-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudkit/swagger-ui", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "preview:cf": "pnpm build && wrangler pages dev ./.svelte-kit/cloudflare", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 | "lint": "eslint ./src", 14 | "format": "eslint ./src --fix && prettier --write ./src", 15 | "sync": "svelte-kit sync" 16 | }, 17 | "devDependencies": { 18 | "@sveltejs/adapter-auto": "^3.0.0", 19 | "@sveltejs/adapter-cloudflare": "4.7.2", 20 | "@sveltejs/adapter-node": "5.2.2", 21 | "@sveltejs/kit": "^2.0.0", 22 | "@sveltejs/vite-plugin-svelte": "^3.1.2", 23 | "@types/swagger-ui": "^3.52.4", 24 | "@cloudkit/eslint-config": "workspace:^", 25 | "globals": "^15.0.0", 26 | "svelte": "^4.2.7", 27 | "svelte-check": "^4.0.0", 28 | "vite": "^5.0.3" 29 | }, 30 | "dependencies": { 31 | "@cloudkit/service-contract": "workspace:*", 32 | "swagger-ui": "^5.17.14" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/swagger-ui/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /apps/swagger-ui/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/swagger-ui/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /apps/swagger-ui/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | cloudkit API 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /apps/swagger-ui/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/swagger-ui/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/swagger-ui/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/swagger-ui/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/swagger-ui/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/swagger-ui/static/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/swagger-ui/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/swagger-ui/static/favicon-16x16.png -------------------------------------------------------------------------------- /apps/swagger-ui/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/swagger-ui/static/favicon-32x32.png -------------------------------------------------------------------------------- /apps/swagger-ui/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/swagger-ui/static/favicon.ico -------------------------------------------------------------------------------- /apps/swagger-ui/svelte.config.js: -------------------------------------------------------------------------------- 1 | import cloudflareAdapter from '@sveltejs/adapter-cloudflare'; 2 | import nodeAdapter from '@sveltejs/adapter-node'; 3 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 4 | 5 | const adapter = 6 | process.env.ENVIRONMENT === 'ci' 7 | ? nodeAdapter() 8 | : cloudflareAdapter({ 9 | // See below for an explanation of these options 10 | 11 | routes: { 12 | include: ['/*'], 13 | exclude: [''] 14 | }, 15 | platformProxy: { 16 | configPath: 'wrangler.toml', 17 | environment: process.env.ENVIRONMENT, 18 | experimentalJsonConfig: false, 19 | persist: false 20 | } 21 | }); 22 | /** @type {import('@sveltejs/kit').Config} */ 23 | const config = { 24 | preprocess: [vitePreprocess()], 25 | 26 | kit: { 27 | adapter, 28 | csrf: { 29 | checkOrigin: true 30 | }, 31 | env: { 32 | dir: './../../' 33 | } 34 | } 35 | }; 36 | 37 | export default config; 38 | -------------------------------------------------------------------------------- /apps/swagger-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /apps/swagger-ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | export default defineConfig({ 4 | plugins: [sveltekit()], 5 | server: { 6 | port: 4100 7 | }, 8 | preview: { 9 | port: 4100 10 | }, 11 | optimizeDeps: { 12 | esbuildOptions: { 13 | target: 'esnext' 14 | } 15 | }, 16 | build: { 17 | target: 'es2020' 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@cloudkit/eslint-config'] 3 | }; 4 | -------------------------------------------------------------------------------- /apps/web/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,svelte,css,scss,postcss,md,json,yml}": [ 3 | "prettier --config ../../.prettierrc --write --cache ./src" 4 | ], 5 | "*.{ts,svelte,css,scss,yml}": ["eslint --cache --fix"] 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/src/app.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace App { 3 | interface Locals { 4 | user: import('lucia').User | null; 5 | session: import('lucia').Session | null; 6 | } 7 | } 8 | } 9 | 10 | export {}; 11 | 12 | // /// 13 | 14 | // import type { Group, Image } from '@prisma/client'; 15 | 16 | // /// 17 | // declare global { 18 | // namespace App { 19 | // interface Locals { 20 | // auth: import('lucia').AuthRequest; 21 | // user: Lucia.UserAttributes; 22 | // } 23 | // } 24 | // namespace Lucia { 25 | // type Auth = import('$lib/server/auth/lucia').Auth; 26 | // type DatabaseUserAttributes = { 27 | // firstName: string; 28 | // lastName: string; 29 | // email: string; 30 | // verified: boolean; 31 | // createdAt: Date; 32 | // updatedAt: Date; 33 | // groups: Group[]; 34 | // avatar: Image | null; 35 | // }; 36 | // type DatabaseSessionAttributes = { 37 | // firstName: string; 38 | // lastName: string; 39 | // email: string; 40 | // verified: boolean; 41 | // createdAt: Date; 42 | // updatedAt: Date; 43 | // groups: Group[]; 44 | // avatar: Image | null; 45 | // }; 46 | // } 47 | // } 48 | 49 | // // THIS IS IMPORTANT!!! 50 | // export {}; 51 | -------------------------------------------------------------------------------- /apps/web/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %sveltekit.head% 5 | 6 | 7 | 14 | 15 | 16 |
%sveltekit.body%
17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web/src/app.postcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @tailwind variants; 5 | @tailwind screens; 6 | 7 | :root [data-theme='wintry'] { 8 | --theme-rounded-base: 4px; 9 | --theme-rounded-container: 2px; 10 | --theme-font-family-base: 'Quicksand', sans-serif; 11 | --theme-font-family-heading: 'Quicksand'; 12 | } 13 | /*place global styles here */ 14 | html, 15 | body { 16 | @apply h-full; 17 | @apply min-h-full; 18 | @apply flex; 19 | @apply flex-col; 20 | } 21 | main { 22 | @apply flex-1; 23 | } 24 | 25 | body { 26 | background-image: radial-gradient( 27 | at 0% 0%, 28 | rgba(var(--color-secondary-500) / 0.33) 0px, 29 | transparent 50% 30 | ), 31 | radial-gradient(at 98% 1%, rgba(var(--color-error-500) / 0.33) 0px, transparent 50%); 32 | background-attachment: fixed; 33 | background-position: center; 34 | background-repeat: no-repeat; 35 | background-size: cover; 36 | } 37 | @font-face { 38 | font-family: 'Quicksand'; 39 | src: url('/fonts/Quicksand.ttf'); 40 | font-display: swap; 41 | } 42 | 43 | [data-popup] { 44 | /* Display */ 45 | display: none; 46 | /* Position */ 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | /* Transitions */ 51 | transition: none; 52 | } 53 | .tree-item-content { 54 | @apply w-full; 55 | } 56 | -------------------------------------------------------------------------------- /apps/web/src/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /apps/web/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { isDevOrCi, PATH_GROUPS, PATHS } from '@cloudkit/ui-core'; 2 | import { auth } from '@lib/server/auth/lucia'; 3 | import { type Handle, redirect } from '@sveltejs/kit'; 4 | 5 | export const handle: Handle = async ({ event, resolve }) => { 6 | await auth.deleteExpiredSessions(); 7 | const sessionId = event.cookies.get(auth.sessionCookieName); 8 | if (!sessionId) { 9 | event.locals.user = null; 10 | event.locals.session = null; 11 | if (event.route.id?.startsWith(PATH_GROUPS.PROTECTED)) { 12 | redirect(302, PATHS.ROOT); 13 | // TODO Implement email verification 14 | // if (!user.verified) redirect(302, '/auth/verify/email'); 15 | } 16 | return resolve(event); 17 | } 18 | 19 | const { session, user } = await auth.validateSession(sessionId); 20 | 21 | if (session && session.fresh) { 22 | const sessionCookie = auth.createSessionCookie(session.id); 23 | event.cookies.set(sessionCookie.name, sessionCookie.value, { 24 | path: PATHS.ROOT, 25 | ...sessionCookie.attributes 26 | }); 27 | } 28 | if (!session) { 29 | const sessionCookie = auth.createBlankSessionCookie(); 30 | event.cookies.set(sessionCookie.name, sessionCookie.value, { 31 | path: PATHS.ROOT, 32 | ...sessionCookie.attributes 33 | }); 34 | } 35 | event.locals.user = user; 36 | event.locals.session = session; 37 | 38 | if (user && event.route.id === PATH_GROUPS.PUBLIC) { 39 | redirect(302, PATHS.PROFILE); 40 | } 41 | return await resolve(event); 42 | }; 43 | 44 | export function handleError({ event, error, message }) { 45 | console.error(error); 46 | console.error(message); 47 | if (!isDevOrCi) { 48 | const { cookies } = event; 49 | const token = cookies.get('auth_session'); 50 | if (token) { 51 | cookies.delete('auth_session', { path: PATHS.ROOT }); 52 | } 53 | } 54 | 55 | const errorId = crypto.randomUUID(); 56 | return { 57 | message: `${errorId}: We're having some trouble logging you in. Please try again later` 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /apps/web/src/lib/api/auth-service-api.ts: -------------------------------------------------------------------------------- 1 | import type { UserWithRelations } from '@cloudkit/ui-core'; 2 | import type { AuthenticateUserSchema, RegisterUserSchema } from '@lib/client/auth/schemas'; 3 | import type { AxiosResponse } from 'axios'; 4 | import axios from 'axios'; 5 | import type { Infer, SuperValidated } from 'sveltekit-superforms/client'; 6 | import { ApiServiceBase } from './base-api-service'; 7 | 8 | class AuthApiService extends ApiServiceBase { 9 | constructor() { 10 | super('/api/v1/auth'); 11 | } 12 | 13 | createNewSession(data: SuperValidated>['data']) { 14 | return this.http.put('', data); 15 | } 16 | createNewUser(data: SuperValidated>['data']) { 17 | // We have to use a separate axios config here because the default config would convert the avatar to an base64 encoded string 18 | return axios.postForm< 19 | SuperValidated>['data'], 20 | AxiosResponse 21 | >('/api/v1/auth', data); 22 | } 23 | deleteSession() { 24 | return this.http.delete(''); 25 | } 26 | deleteSessionById(id: string) { 27 | return this.http.delete(`/${id}`); 28 | } 29 | } 30 | 31 | const api = Object.freeze(new AuthApiService()); 32 | export { api as AuthApiService }; 33 | -------------------------------------------------------------------------------- /apps/web/src/lib/api/base-api-service.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance, AxiosRequestConfig } from 'axios'; 2 | import axios from 'axios'; 3 | 4 | export abstract class ApiServiceBase { 5 | public context: string; 6 | 7 | protected readonly http: AxiosInstance; 8 | 9 | constructor( 10 | context: string, 11 | commonRequestConfig: AxiosRequestConfig = { 12 | transformRequest: [ 13 | (data) => { 14 | return JSON.stringify(data); 15 | } 16 | ] 17 | } 18 | ) { 19 | this.context = context; 20 | 21 | let { headers = {} } = commonRequestConfig; 22 | if (headers['Content-Type'] == null) { 23 | headers = { 24 | ...headers, 25 | 'Content-Type': 'application/json' 26 | }; 27 | } 28 | 29 | this.http = axios.create({ 30 | ...commonRequestConfig, 31 | headers, 32 | baseURL: context, 33 | paramsSerializer: { indexes: null } 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/web/src/lib/api/user-service-api.ts: -------------------------------------------------------------------------------- 1 | import { RegisterUserSchema } from '@lib/client/auth/schemas'; 2 | import type { Infer, SuperValidated } from 'sveltekit-superforms/client'; 3 | import { ApiServiceBase } from './base-api-service'; 4 | 5 | class UserApiService extends ApiServiceBase { 6 | constructor() { 7 | super('/api/v1/user'); 8 | } 9 | 10 | getCurrentUser() { 11 | return this.http.get('/'); 12 | } 13 | 14 | deleteCurrentUser() { 15 | return this.http.delete('/'); 16 | } 17 | 18 | updateUser(data: SuperValidated>['data']) { 19 | return this.http.patch('/', data); 20 | } 21 | getUserById(id: string) { 22 | return this.http.get(`/${id}`); 23 | } 24 | } 25 | 26 | const api = Object.freeze(new UserApiService()); 27 | export { api as UserApiService }; 28 | -------------------------------------------------------------------------------- /apps/web/src/lib/client/auth/schemas.ts: -------------------------------------------------------------------------------- 1 | import { ALLOWED_STRINGS, ERROR_MESSAGE } from '@cloudkit/ui-core'; 2 | import { z } from 'zod'; 3 | 4 | export const MAX_FILE_SIZE = 500000; 5 | export const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; 6 | export const ImageSchema = z.preprocess( 7 | (value) => (Array.isArray(value) ? value : [value]), 8 | 9 | z 10 | .any() 11 | .refine((files) => files?.length == 1, 'Image is required.') 12 | .refine((files) => files?.[0]?.size <= MAX_FILE_SIZE, `Max file size is 5MB.`) 13 | .refine( 14 | (files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type), 15 | '.jpg, .jpeg, .png and .webp files are accepted.' 16 | ) 17 | ); 18 | 19 | export const RegisterUserSchema = z.object({ 20 | firstName: z.string().min(1).max(32).regex(ALLOWED_STRINGS, ERROR_MESSAGE), 21 | lastName: z.string().min(1).max(32).regex(ALLOWED_STRINGS, ERROR_MESSAGE), 22 | password: z.string().min(8), 23 | confirmPassword: z.string().min(8), 24 | email: z.string().email().min(3), 25 | avatar: z.instanceof(File).optional() // only optional because we validate the form before we add a missing avatar 26 | }); 27 | 28 | export const AuthenticateUserSchema = z.object({ 29 | email: z.string().email().min(3), 30 | password: z.string().min(8) 31 | }); 32 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/molecues/user/user-card-list.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |

List of Example Users

8 |
12 | {#each users as { id, avatar, firstName }} 13 | {#if avatar && avatar.updatedAt} 14 | 15 |
16 | 22 |
{firstName}
23 |
24 |
25 | {/if} 26 | {/each} 27 |
28 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/molecues/user/user-card-list.test.ts: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/svelte'; 2 | 3 | import { expect, test } from 'vitest'; 4 | 5 | import UserCardList from './user-card-list.svelte'; 6 | test('button with event', async () => { 7 | render(UserCardList, { 8 | users: [ 9 | { 10 | createdAt: new Date(), 11 | firstName: 'test', 12 | id: '123', 13 | lastName: 'test', 14 | avatar: { 15 | url: 'test' 16 | }, 17 | email: 'test@test.com', 18 | firstTime: true, 19 | verified: true, 20 | updatedAt: new Date() 21 | } 22 | ] 23 | }); 24 | const text = screen.queryByText(/List of Example Users/); 25 | expect(text).toBeInTheDocument(); 26 | }); 27 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/organisms/modals/error-modal.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
21 | 22 | 23 |

24 | Something went wrong while trying to process your request. Please try again later. 25 |

26 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/organisms/navbar/public-navbar.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 48 | 49 | 93 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/root-conainer.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/about-this-project.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Notes about this Project

3 |
4 | This project is a work in progress. I'm using it to build a few projects and will update it 5 | accordingly. If you have any questions, feel free to reach out by creating an issue or 6 | discussion on GitHUb. 7 |
8 |
9 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/auth-flow.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Auth Flow

3 |
4 | You can try out the Auth Flow right here. Either create a new user by signing up or use one of 5 | the existing users. The default Username and Password is admin@dle.dev. Feel free to try it out! 6 | This website will be redeployed every 24h. 7 |
8 |
9 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/data-proxy.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Data Proxy

3 |

4 | As previously mentioned, prisma doesn't a direct connection to Databases form edge functions. 5 | You need to create a 9 | Data Proxy 10 | 11 | , which does some prisma magic and then you can use Prisma in Edge Functions. This is why there are 12 | two prisma schemas. The "normal" way of defining your data source is this 13 |

14 | 15 |
16 | 		datasource db {'{'}
17 | 			provider = "postgresql"
18 | 			url = env("DATABASE_URL")
19 | 		{'}'}
20 | 	
21 |

If you're using the Data Proxy, you have to declare it like this

22 |
23 | 		datasource db {'{'}
24 | 			provider = "postgresql"
25 | 			url = env("DATA_PROXY")
26 | 			directUrl = env("DATABASE_URL")
27 | 		{'}'}
28 | 	
29 |

30 | The DATABASE_URL 31 | refers to the standard Database URL and 32 | DATA_PROXY 33 | refers to the connection string you generate when creating a 34 | Cloud Prisma Project 35 | - Its free for mostly everything. 36 |

37 |
38 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/github-actions.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Github Actions Environemnts

3 |

4 | There are two workflow files. PR.yml 5 | and 6 | MERGE_MASTER.yml 7 | . The 8 | PR.yml 9 | is used for all pull requests and the 10 | MERGE_MASTER.yml 11 | is used for merging to the master branch. 12 |

13 |

14 | Both workflows are set up to deploy all three apps to their respective cloudflare projects, 15 | including feature branch deployments. 16 |

17 |
18 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/pr.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Understanding the PR.yaml Github Action

3 |
4 | The Github PR.yaml action triggers on every opened PR and on fresh pushes to that PR and runs 5 | the following jobs: 6 |
7 |
8 |
9 | 1. 10 |
11 | INSTALL 12 |
13 | Installs depdendencies from pnpm-lock.yaml 14 | and caches them 15 |
16 |
17 |
18 | 19 |
20 | 2. 21 |
22 | LINT 23 |
Runs prettier and the sveltekit-check command
24 |
25 |
26 | 27 |
28 | 3. 29 |
30 | UNIT_TEST 31 |
32 | Runs unit tests using vitest 33 |
34 |
35 |
36 | 37 |
38 | 4. 39 |
40 | PUBLISH 41 |
42 | There are three publish jobs. One for the web-app, one for the swagger-ui and one for the 43 | storybook. 44 |
45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/prerequisites.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Prerequisites

3 |
4 | Becase PR.yaml 5 | action uses docker containers, you'll have to configure the following Github Action Secrets in a 6 | CI 7 | environment in order for the Workflow to run through without errors. Example Values of these can 8 | be cound in the 9 | .env.ci 10 | file. 11 |
12 |
13 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/prisma-code-org.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Prisma Code Organization

3 |

4 | I couldn't find a good way to keep all my prisma relevant code together so I came up with the 5 | following solution. There are two Repository 6 | classes. One for managing Users and one for managing Images. I've split the Prisma code up like this 7 | in order to keep the primsa imports to a bare minimum. All DB Actions I do via these classes and 8 | they are called only from the server. 9 |

10 |

11 | Note: 12 | The 13 | ImageRepository 14 | can interchangebly be used with the local thumbor container or with Cloudflare's Image Service. 15 |

16 |
17 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/road-map.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Roadmap

3 |
4 | There are a few to-do items here, but nothing that wills top you from using this as a starting 5 | point. Feel free to open issues/PRs if you have ideas and want to address issues. 6 |
7 |
8 |
9 | 1. 10 |
11 | Remove Lucia Auth 12 |
13 | Unforunately Lucia Auth has been depcricated and will no longer receiver updates beyond 14 | V3. Therefore I'm going to migrate this template away from lucia. I don't know to what 15 | yet, so I'm open for suggestions. 16 |
17 |
18 |
19 | 20 |
21 | 2. 22 |
23 | Redis Sessions 24 |
25 | Lucia V3 dropped support for redis session management. I'd like to get this back as I feel 26 | that keeping sessions in redis and not hitting the DB for every request is good the 27 | performance and would keep DB costs lower. 28 |
29 |
30 |
31 | 32 |
33 | 3. 34 |
35 | API Rate Limiting 36 |
37 | I'd like to add basic rate limiting functions to the API endpoints using 42 | sveltekit-rate-limiter 43 | 44 |
45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/run-local.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Running this locally

3 |
4 |
5 | 1. 6 |
7 |
8 | Create a .env.development 9 |
10 | 11 |

12 | It's used to configure access to the local thumbor instance (for storing and retrieving 13 | images) and the local database 14 |

15 |
16 |
17 | 18 |
19 | 2. 20 |
21 |
22 | Run pnpm dev 23 |
24 | 25 |
26 |

27 | The turbo.json 28 | configuration is set up with all the appropriate dependencies and will take care of the following. 29 |

30 |
    31 |
  1. 32 | 1. 33 | 34 | Pull and start two docker containers (thumbor for image storage and postgresql for 35 | user/data storage) 36 | 37 |
  2. 38 |
  3. 39 | 2. 40 | 41 | Generate the prisma client based on the schema from the db-schema 42 | module 43 | 44 |
  4. 45 |
  5. 46 | 3. 47 | 48 | Push the gererated prisma client to the local psql container. 49 | Note: This might fail if the containers are not up and running yet. If it does, 50 | just run pnpm dev 51 | a couple of times until it takes. 52 | 53 | 54 |
  6. 55 |
  7. 56 | 4. 57 | 58 | Generate zod schemas, which are consumed in the service-contract 59 | module. 60 | 61 |
  8. 62 |
  9. 63 | 5. 64 | 65 | Launch the swagger-ui sveltekit app. This allows you to test out your API endpoints 66 | locally. 67 | 68 |
  10. 69 |
  11. 70 | 6. 71 | 72 | Launch storybook so you can keep an eye on all your current components and their 73 | states. 74 | 75 |
  12. 76 |
  13. 77 | 7. 78 | Launch the web-app. 79 |
  14. 80 |
81 |
82 |
83 |
84 |
85 |
86 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/run-prod.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Running this in Production

3 |
4 | You'll need to configure some services and environment variables in the Github Actions to run 5 | this in Prod. 6 |
7 |
8 |
9 | 1. 10 |
11 | Cloudflare Pages 12 |

There is a free tier but even the paid tier isn't very expensive.

13 |

14 | You'll also have to create the projects in Cloudflare and set them up. The project is set 15 | up to run three cloudflare projects. The web-app, the swagger-ui and storybook. 16 |

17 |
18 |
19 |
20 | 2. 21 |
22 | 23 | Cloudflare Images 24 | 25 | 26 |
27 | This one is paid only. It's 5 USD / Month / 100'000 Image served with 20 configured 28 | variations (the variations do not count to the 100'000 image limit). Delivering images is 29 | 1 USD for every 100'000 images. There's also a enterprise version. Check out the details 33 | here 34 | 35 | . 36 |
37 |
38 |
39 |
40 | 3. 41 |
42 | Neon.Tech 43 |
44 | Database - I've been using their free tier for ages without hitting any limits so far. The 45 | only reason I went ahead and upgraded was because you can only have one project on the 46 | free tier. They don't charge you for a month if the bill is under 50 Cent. It's very 47 | affordable for what you're getting. Seriously, 51 | check them out 52 | 53 | . 54 |
55 |
56 |
57 | 58 |
59 | 4. 60 |
61 | Prisma Cloud 62 |
You'll have to create a project there and connect it to the database.
63 |
64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/scripts.svelte: -------------------------------------------------------------------------------- 1 |
2 |

3 | Scripts in package.json 4 |

5 |
6 | I've included a lot of scripts in the package.json 7 | file. The mostly depend on each other. The naming follows this pattern in general 8 | 9 |
[COMMAND]:[ACTION]:[ENV]
10 | For example, If I want to generate my prisma schema for local development I would run 11 | pnpm prisma:gen:dev 12 | . 13 | 14 |
Here are some useful scripts
15 |
16 |
17 |
18 | 1. 19 |
20 | pnpm psql:dump 21 |
22 | Dumps the content of the current database into a cloudkit-db-dump.sql 23 | file. You can 24 | ./cloudkit-db-dump.sql:/docker-entrypoint-initdb.d/init.sql 25 | which will initialize the psql container with those contents on start up every time. 26 |
27 |
28 |
29 | 30 |
31 | 2. 32 |
33 | pnpm psql:restore 34 |
35 | Restores from the latest cloudkit-db-dump.sql 36 | file, if it exists. 37 |
38 |
39 |
40 |
41 | 2. 42 |
43 | pnpm test:e2e:dev 44 |
45 | Runs the sveltekit development server and Playwright in UI mode. This way you can code and 46 | verify your tests at the same time. 47 |
48 |
49 |
50 |
51 | 2. 52 |
53 | pnpm clean 54 |
55 | Deletes the ./playwright-report 56 | , 57 | ./.wrangler 58 | and 59 | ./.svelte-kit 60 | folders for a clean slate 61 |
62 |
63 |
64 |
65 | 2. 66 |
67 | pnpm prep 68 |
69 | If your docker containers are running, this will generate the prisma schema, push it to 70 | the psql container and seed the database 71 |
72 |
73 |
74 |
75 |
76 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/sections/welcome.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 |
5 |
6 |

7 | Welcome to CloudKit 8 |

9 |

A production ready sveltekit cloudflare template

10 |

11 | Author: Daniel Einars 12 |

13 |
14 |
15 | -------------------------------------------------------------------------------- /apps/web/src/lib/queries/fetch-random-avatar-query-config.ts: -------------------------------------------------------------------------------- 1 | export const fetchRandomAvatarQueryConfig = { 2 | queryKey: ['Avatar'], 3 | queryFn: async () => { 4 | const resp = await fetch('https://picsum.photos/736'); 5 | 6 | const image = (await resp.blob()) as unknown as File; 7 | return image; 8 | // 9 | // const converted = await convertFileToBase64(image); 10 | // return converted; 11 | }, 12 | enabled: false 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/lib/server/auth/lucia.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@lib/server/repository/prisma-client'; 2 | import { PrismaAdapter } from '@lucia-auth/adapter-prisma'; 3 | import { Lucia } from 'lucia'; 4 | 5 | /** 6 | * Returns a Lucia instance with the appropriate adapter and middleware based on the environment. 7 | */ 8 | async function getConfiguration() { 9 | const adapter = new PrismaAdapter(db.session, db.user); 10 | const lucia = new Lucia(adapter, { 11 | sessionCookie: { 12 | attributes: { 13 | // set to `true` when using HTTPS 14 | secure: process.env.NODE_ENV === 'production' 15 | } 16 | }, 17 | 18 | getUserAttributes: (attributes) => { 19 | return { 20 | firstName: attributes.firstName, 21 | lastName: attributes.lastName, 22 | email: attributes.email, 23 | createdAt: attributes.createdAt, 24 | updatedAt: attributes.updatedAt, 25 | verified: attributes.verified, 26 | firstTime: attributes.firstTime 27 | }; 28 | } 29 | }); 30 | return lucia; 31 | } 32 | 33 | /** 34 | * The instance of Lucia authentication middleware. 35 | */ 36 | export const auth = await getConfiguration(); 37 | declare module 'lucia' { 38 | interface Register { 39 | Lucia: typeof auth; 40 | DatabaseUserAttributes: DatabaseUserAttributes; 41 | } 42 | } 43 | 44 | type DatabaseUserAttributes = { 45 | firstName: string; 46 | lastName: string; 47 | email: string; 48 | verified: boolean; 49 | createdAt: Date; 50 | updatedAt: Date; 51 | firstTime: boolean; 52 | }; 53 | -------------------------------------------------------------------------------- /apps/web/src/lib/server/errors/index.ts: -------------------------------------------------------------------------------- 1 | export class AccessDeniedError extends Error { 2 | constructor(resource: string) { 3 | super(`Access denied to ${resource}`); 4 | } 5 | } 6 | 7 | export class InvalidSessionError extends Error { 8 | constructor() { 9 | super('Invalid Session'); 10 | } 11 | } 12 | 13 | export class ResourceNotFoundError extends Error { 14 | constructor(resource: string) { 15 | super(`Resources not found: ${resource}`); 16 | } 17 | } 18 | 19 | export class CollectionAlreadyExistsError extends Error { 20 | constructor(collectionName: string) { 21 | super(`Collection with this name already exists: ${collectionName}`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/src/lib/server/middleware/rate-limiter.ts: -------------------------------------------------------------------------------- 1 | import { RateLimiter } from 'sveltekit-rate-limiter/server'; 2 | 3 | export const limiter = new RateLimiter({ 4 | // A rate is defined as [number, unit] 5 | IP: [10, 'h'], // IP address limiter 6 | IPUA: [5, 'm'], // IP + User Agent limiter 7 | cookie: { 8 | // Cookie limiter 9 | name: 'limiterid', // Unique cookie name for this limiter 10 | secret: 'SECRETKEY-SERVER-ONLY', // Use $env/static/private 11 | rate: [2, 'm'], 12 | preflight: true // Require preflight call (see load function) 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /apps/web/src/lib/server/repository/prisma-client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient as PrismaClientNode, Prisma as PrismaNode } from '@prisma/client'; 2 | import { PrismaClient as PrismaClientEdge, Prisma as PrismaEdge } from '@prisma/client/edge'; 3 | import { withAccelerate } from '@prisma/extension-accelerate'; 4 | 5 | import { DATABASE_URL } from '$env/static/private'; 6 | import { isDevOrCi } from '@cloudkit/ui-core'; 7 | 8 | const prismaConfiguration = { 9 | datasources: { 10 | db: { 11 | url: DATABASE_URL 12 | } 13 | } 14 | }; 15 | 16 | export const db = isDevOrCi 17 | ? new PrismaClientNode(prismaConfiguration).$extends(withAccelerate()) 18 | : new PrismaClientEdge(prismaConfiguration).$extends(withAccelerate()); 19 | 20 | export const PrismaClientKnownRequestError = isDevOrCi 21 | ? PrismaNode.PrismaClientKnownRequestError 22 | : PrismaEdge.PrismaClientKnownRequestError; 23 | -------------------------------------------------------------------------------- /apps/web/src/lib/server/services/user-service.ts: -------------------------------------------------------------------------------- 1 | import type { User, UserWithRelations } from '@cloudkit/ui-core'; 2 | import type { RegisterUserSchema } from '@lib/client/auth/schemas'; 3 | import type { Infer, SuperValidated } from 'sveltekit-superforms/server'; 4 | import { UserRepository } from '../repository/user-repository'; 5 | 6 | // TODO implement ZenStack to manage access 7 | class UserService { 8 | static _instanceCache: UserService; 9 | 10 | static instance(): UserService { 11 | if (!this._instanceCache) { 12 | this._instanceCache = new this(); 13 | } 14 | 15 | return this._instanceCache; 16 | } 17 | 18 | async createUser( 19 | data: SuperValidated>['data'] 20 | ): Promise { 21 | return UserRepository.create(data); 22 | } 23 | 24 | async updateUser(data: User): Promise { 25 | return UserRepository.updateFromSession({ ...data, firstTime: false }); 26 | } 27 | 28 | async deleteUser(data: UserWithRelations): Promise { 29 | return UserRepository.deleteById(data.id); 30 | } 31 | } 32 | 33 | const repo = Object.freeze(new UserService()); 34 | export { repo as UserService }; 35 | -------------------------------------------------------------------------------- /apps/web/src/lib/stores/constants.ts: -------------------------------------------------------------------------------- 1 | export const STORE_CONTEXTS = { 2 | User: 'user' 3 | } as const; 4 | -------------------------------------------------------------------------------- /apps/web/src/lib/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-store'; 2 | -------------------------------------------------------------------------------- /apps/web/src/lib/stores/user-store.ts: -------------------------------------------------------------------------------- 1 | import { getContext, setContext } from 'svelte'; 2 | import { writable, type Writable } from 'svelte/store'; 3 | import { STORE_CONTEXTS } from './constants'; 4 | import type { UserWithRelations } from '@cloudkit/ui-core'; 5 | 6 | export function initUserStore(user: T | null) { 7 | const store = writable(user); 8 | setContext(STORE_CONTEXTS.User, store); 9 | return store; 10 | } 11 | 12 | export function getUserStore< 13 | T extends Record = NonNullable 14 | >(storeExtension?: T) { 15 | const userStore: Writable = getContext(STORE_CONTEXTS.User); 16 | if (storeExtension) { 17 | userStore.update((user) => ({ 18 | ...user, 19 | ...storeExtension 20 | })); 21 | } 22 | return userStore; 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/src/routes/(protected)/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { PATHS } from '@cloudkit/ui-core'; 2 | import { auth } from '@lib/server/auth/lucia'; 3 | import { UserRepository } from '@lib/server/repository/user-repository'; 4 | import { redirect } from '@sveltejs/kit'; 5 | 6 | import type { LayoutServerLoad } from './$types'; 7 | 8 | export const load = (async ({ url, cookies }) => { 9 | const { pathname } = url; 10 | 11 | const sessionId = cookies.get(auth.sessionCookieName); 12 | if (!sessionId) { 13 | redirect(302, PATHS.ROOT); 14 | } 15 | 16 | const { user } = await auth.validateSession(sessionId); 17 | return { 18 | user: await UserRepository.findById(user?.id ?? ''), 19 | pathname 20 | }; 21 | }) satisfies LayoutServerLoad; 22 | -------------------------------------------------------------------------------- /apps/web/src/routes/(protected)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /apps/web/src/routes/(protected)/api/v1/user/+server.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@lib/server/auth/lucia'; 2 | import { InvalidSessionError } from '@lib/server/errors'; 3 | import { RequestValidator } from '@lib/server/middleware/validate'; 4 | import { UserService } from '@lib/server/services/user-service'; 5 | import type { RequestHandler } from '@sveltejs/kit'; 6 | 7 | export const GET: RequestHandler = async () => { 8 | return new Response(null, { status: 500 }); 9 | }; 10 | 11 | export const PATCH: RequestHandler = async () => { 12 | return new Response(null, { status: 500 }); 13 | }; 14 | 15 | export const DELETE: RequestHandler = async ({ cookies }) => { 16 | try { 17 | const { user, session } = await RequestValidator.validateSession(cookies); 18 | const success = await UserService.deleteUser(user!); 19 | if (success) { 20 | await auth.invalidateSession(session!.id); 21 | return new Response(null, { status: 200 }); 22 | } 23 | } catch (e) { 24 | if (e instanceof InvalidSessionError) { 25 | return new Response(null, { status: 401 }); 26 | } 27 | } 28 | return new Response(null, { status: 500 }); 29 | }; 30 | -------------------------------------------------------------------------------- /apps/web/src/routes/(protected)/api/v1/user/[userId]/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit'; 2 | 3 | export const GET: RequestHandler = async () => { 4 | return new Response(null, { status: 500 }); 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/src/routes/(protected)/profile/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { SERVER_FORM_ACTIONS } from '@cloudkit/ui-core'; 2 | 3 | import { EditUserSchema, PATHS } from '@cloudkit/ui-core'; 4 | 5 | import { ImageRepository } from '@lib/server/repository/image-repository'; 6 | import { UserRepository } from '@lib/server/repository/user-repository'; 7 | import { fail, redirect } from '@sveltejs/kit'; 8 | import { zod } from 'sveltekit-superforms/adapters'; 9 | import { superValidate } from 'sveltekit-superforms/server'; 10 | 11 | import type { Actions, PageServerLoad } from './$types'; 12 | 13 | export const load = (async (event) => { 14 | const user = event.locals.user; 15 | 16 | if (!user) { 17 | redirect(302, PATHS.ROOT); 18 | } 19 | 20 | const editUserForm = await superValidate( 21 | { 22 | firstName: user.firstName, 23 | lastName: user.lastName, 24 | email: user.email 25 | }, 26 | zod(EditUserSchema) 27 | ); 28 | return { 29 | editUserForm 30 | }; 31 | }) satisfies PageServerLoad; 32 | 33 | export const actions: Actions = { 34 | [SERVER_FORM_ACTIONS.UPDATE_USER]: async ({ request, locals }) => { 35 | const user = locals.user; 36 | if (!user) { 37 | redirect(302, PATHS.ROOT); 38 | } 39 | const formData = await request.formData(); 40 | const editUserForm = await superValidate(formData, zod(EditUserSchema)); 41 | if (!editUserForm.valid) { 42 | return fail(400, { form: editUserForm }); 43 | } 44 | 45 | const userWithRelations = await UserRepository.findByIdWithRelations(user.id); 46 | 47 | const avatar = formData.get('avatar'); 48 | if (avatar instanceof File && userWithRelations !== null) { 49 | const image = await ImageRepository.update({ 50 | data: userWithRelations, 51 | image: avatar 52 | }); 53 | 54 | editUserForm.data.avatar = image; 55 | // await UserRepository.updateAvatar(locals.user!, avatarEntry); 56 | } 57 | // await UserRepository.updateData({ 58 | // ...locals.user, 59 | // firstName: editUserForm.data.firstName, 60 | // lastName: editUserForm.data.lastName, 61 | // email: editUserForm.data.email 62 | // }); 63 | 64 | return { 65 | editUserForm 66 | }; 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /apps/web/src/routes/(protected)/profile/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /apps/web/src/routes/(public)/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import type { Infer, SuperValidated } from 'sveltekit-superforms'; 2 | import { zod } from 'sveltekit-superforms/adapters'; 3 | import { superValidate } from 'sveltekit-superforms/server'; 4 | 5 | import { AuthenticateUserSchema, RegisterUserSchema } from '@lib/client/auth/schemas'; 6 | import type { LayoutServerLoad } from './$types'; 7 | 8 | let authenticate: SuperValidated> | null = null; 9 | let register: SuperValidated> | null = null; 10 | export const load = (async ({ url }) => { 11 | if (authenticate === null) { 12 | authenticate = await superValidate(zod(AuthenticateUserSchema)); 13 | } 14 | 15 | if (register === null) { 16 | register = await superValidate(zod(RegisterUserSchema)); 17 | } 18 | 19 | const { pathname } = url; 20 | 21 | return { 22 | register, 23 | authenticate, 24 | pathname 25 | }; 26 | }) satisfies LayoutServerLoad; 27 | -------------------------------------------------------------------------------- /apps/web/src/routes/(public)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /apps/web/src/routes/(public)/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { PATHS, SERVER_FORM_ACTIONS } from '@cloudkit/ui-core'; 2 | 3 | import { auth } from '@lib/server/auth/lucia'; 4 | import { UserRepository } from '@lib/server/repository/user-repository'; 5 | import { fail } from '@sveltejs/kit'; 6 | import { Scrypt } from 'lucia'; 7 | import { zod } from 'sveltekit-superforms/adapters'; 8 | import { message, superValidate } from 'sveltekit-superforms/server'; 9 | 10 | import { AuthenticateUserSchema } from '@lib/client/auth/schemas'; 11 | import type { Actions } from './$types'; 12 | 13 | export const actions: Actions = { 14 | [SERVER_FORM_ACTIONS.AUTHENTICATE]: async ({ request, cookies }) => { 15 | const signIn = await superValidate(request, zod(AuthenticateUserSchema)); 16 | if (!signIn.valid) { 17 | return fail(400, { form: signIn }); 18 | } 19 | 20 | const userExists = await UserRepository.exists(signIn.data.email); 21 | if (!userExists) { 22 | return message(signIn, 'E-Mail or password incorrect', { status: 400 }); 23 | } 24 | const user = await UserRepository.findByEmail(signIn.data.email); 25 | const validPassword = await new Scrypt().verify(user.hashedPassword, signIn.data.password); 26 | if (!validPassword) { 27 | return message(signIn, 'E-Mail or password incorrect', { status: 400 }); 28 | } 29 | const session = await auth.createSession(user.id, {}); 30 | const sessionCookie = auth.createSessionCookie(session.id); 31 | cookies.set(sessionCookie.name, sessionCookie.value, { 32 | path: PATHS.ROOT, 33 | ...sessionCookie.attributes 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /apps/web/src/routes/(public)/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/web/src/routes/(public)/api/v1/auth/+server.ts: -------------------------------------------------------------------------------- 1 | import { PATHS } from '@cloudkit/ui-core'; 2 | import { AuthenticateUserSchema, RegisterUserSchema } from '@lib/client/auth/schemas'; 3 | import { auth } from '@lib/server/auth/lucia'; 4 | import { UserRepository } from '@lib/server/repository/user-repository'; 5 | import { UserService } from '@lib/server/services/user-service'; 6 | import type { RequestHandler } from '@sveltejs/kit'; 7 | import { json } from '@sveltejs/kit'; 8 | import { Scrypt } from 'lucia'; 9 | 10 | export const POST: RequestHandler = async ({ request, cookies }) => { 11 | await auth.validateSession(cookies.get(auth.sessionCookieName) ?? ''); 12 | 13 | const data = await request.formData(); 14 | const { 15 | success, 16 | error, 17 | data: postData 18 | } = RegisterUserSchema.safeParse(Object.fromEntries(data.entries())); 19 | if (success && postData) { 20 | const userExists = await UserRepository.exists(postData.email); 21 | 22 | if (!userExists) { 23 | const created = await UserService.createUser(postData); 24 | const session = await auth.createSession(created.id, {}); 25 | const sessionCookie = auth.createSessionCookie(session.id); 26 | cookies.set(sessionCookie.name, sessionCookie.value, { 27 | path: PATHS.ROOT, 28 | ...sessionCookie.attributes 29 | }); 30 | return json(created); 31 | } else { 32 | return new Response(null, { status: 409 }); 33 | } 34 | } else { 35 | console.error('errors:', error); 36 | } 37 | return new Response(null, { status: 500 }); 38 | }; 39 | export const PUT: RequestHandler = async ({ request, cookies }) => { 40 | const formData = await request.json(); 41 | 42 | const { success, data, error } = AuthenticateUserSchema.safeParse(formData); 43 | if (!success && error) { 44 | return json(error, { 45 | status: 400 46 | }); 47 | } 48 | 49 | const userExists = await UserRepository.exists(data.email); 50 | 51 | if (!userExists) { 52 | return json({ message: 'computer sais no' }, { status: 400 }); 53 | } 54 | const user = await UserRepository.findByEmail(data.email); 55 | const validPassword = await new Scrypt().verify(user.hashedPassword, data.password); 56 | 57 | if (!validPassword) { 58 | return new Response(null, { status: 500 }); 59 | } 60 | const session = await auth.createSession(user.id, {}); 61 | const sessionCookie = auth.createSessionCookie(session.id); 62 | cookies.set(sessionCookie.name, sessionCookie.value, { 63 | path: PATHS.ROOT, 64 | ...sessionCookie.attributes 65 | }); 66 | 67 | return json(user); 68 | }; 69 | export const DELETE = async ({ locals, cookies }) => { 70 | if (!locals.session) { 71 | return new Response(null, { status: 401 }); 72 | } 73 | await auth.invalidateSession(locals.session.id); 74 | const sessionCookie = auth.createBlankSessionCookie(); 75 | cookies.set(sessionCookie.name, sessionCookie.value, { 76 | path: PATHS.ROOT, 77 | ...sessionCookie.attributes 78 | }); 79 | 80 | return new Response(null, { status: 200 }); 81 | }; 82 | -------------------------------------------------------------------------------- /apps/web/src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

Whoopsie. Looks like something went wrong

6 |

{$page.error?.message}

7 | -------------------------------------------------------------------------------- /apps/web/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | {@html ''} 18 | 19 | 20 | cloudkit.FYI 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 |
33 | 54 | -------------------------------------------------------------------------------- /apps/web/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment'; 2 | import { QueryClient } from '@tanstack/svelte-query'; 3 | import type { LayoutLoad } from './$types'; 4 | 5 | export const load: LayoutLoad = async () => { 6 | const queryClient = new QueryClient({ 7 | defaultOptions: { 8 | queries: { 9 | enabled: browser, 10 | staleTime: 60 * 1000 11 | } 12 | } 13 | }); 14 | 15 | return { queryClient }; 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/web/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/web/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/web/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/favicon-16x16.png -------------------------------------------------------------------------------- /apps/web/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/favicon-32x32.png -------------------------------------------------------------------------------- /apps/web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/favicon.ico -------------------------------------------------------------------------------- /apps/web/static/fonts/Quicksand.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/fonts/Quicksand.ttf -------------------------------------------------------------------------------- /apps/web/static/icons/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Skeleton Labs, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /apps/web/static/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/admin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/web/static/icons/briefcase.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/browsers.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/cal.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /apps/web/static/icons/checkFalse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/checkTrue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/communityPlaceholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /apps/web/static/icons/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/kebab-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kebab-Menu 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web/static/icons/loading.svg: -------------------------------------------------------------------------------- 1 | 10 | 14 | 23 | 24 | 28 | 37 | 38 | 42 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /apps/web/static/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/icons/logo.png -------------------------------------------------------------------------------- /apps/web/static/icons/mapPin.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /apps/web/static/icons/member.svg: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /apps/web/static/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /apps/web/static/icons/package.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/placeholder.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /apps/web/static/icons/printer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/static/icons/save.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /apps/web/static/icons/upload.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /apps/web/static/icons/users.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /apps/web/static/images/placeholder-community-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/images/placeholder-community-logo.webp -------------------------------------------------------------------------------- /apps/web/static/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaroidkidd/cloudkit/829aad4f6f4e198c438c654d682d15ddb002e06f/apps/web/static/images/placeholder.png -------------------------------------------------------------------------------- /apps/web/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /apps/web/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Whats In", 3 | "short_name": "WI", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/svelte.config.js: -------------------------------------------------------------------------------- 1 | import cloudflareAdapter from '@sveltejs/adapter-cloudflare'; 2 | import nodeAdapter from '@sveltejs/adapter-node'; 3 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 4 | 5 | const adapter = 6 | process.env.ENVIRONMENT === 'ci' 7 | ? nodeAdapter() 8 | : cloudflareAdapter({ 9 | // See below for an explanation of these options 10 | 11 | routes: { 12 | include: ['/*'], 13 | exclude: [''] 14 | }, 15 | platformProxy: { 16 | configPath: 'wrangler.toml', 17 | environment: process.env.ENVIRONMENT, 18 | experimentalJsonConfig: false, 19 | persist: false 20 | } 21 | }); 22 | /** @type {import('@sveltejs/kit').Config} */ 23 | const config = { 24 | preprocess: [vitePreprocess()], 25 | 26 | kit: { 27 | adapter, 28 | csrf: { 29 | checkOrigin: true 30 | }, 31 | env: { 32 | dir: './../../' 33 | }, 34 | alias: { 35 | '@mocks/*': './src/__mocks__/*', 36 | '@hooks/*': './src/hooks/*', 37 | '@components/*': './src/lib/components/*', 38 | '@lib/*': './src/lib/*', 39 | '@pages/*': './src/pages/*', 40 | '@services/*': './src/lib/services/*', 41 | '@styles/*': './src/styles/*', 42 | '@utils/*': './src/lib/utils/*', 43 | '@assets/*': './src/assets/*', 44 | '@middleware/*': './src/lib/server/middleware/*' 45 | } 46 | } 47 | }; 48 | 49 | export default config; 50 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import forms from '@tailwindcss/forms'; 2 | import typography from '@tailwindcss/typography'; 3 | import { join } from 'path'; 4 | import type { Config } from 'tailwindcss'; 5 | 6 | // 1. Import the Skeleton plugin 7 | import { skeleton } from '@skeletonlabs/tw-plugin'; 8 | 9 | const config = { 10 | // 2. Opt for dark mode to be handled via the class method 11 | darkMode: 'class', 12 | content: [ 13 | './src/**/*.{html,js,svelte,ts}', 14 | '../../packages/ui-core/src/**/*.{html,js,svelte,ts}', 15 | // 3. Append the path to the Skeleton package 16 | join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}'), 17 | join( 18 | require.resolve('@skeletonlabs/skeleton'), 19 | '../../packages/ui-core/src/**/*.{html,js,svelte,ts}' 20 | ) 21 | ], 22 | theme: { 23 | extend: {} 24 | }, 25 | plugins: [ 26 | forms, 27 | typography, 28 | // 4. Append the Skeleton plugin (after other plugins) 29 | require('@tailwindcss/container-queries'), 30 | skeleton({ 31 | themes: { 32 | // Register each theme within this array: 33 | preset: [{ name: 'wintry', enhancements: true }] 34 | } 35 | }) 36 | ] 37 | } satisfies Config; 38 | 39 | export default config; 40 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler", 13 | "types": ["@testing-library/jest-dom/vitest"] 14 | } 15 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 16 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 17 | // 18 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 19 | // from the referenced tsconfig.json - TypeScript does not merge them in 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { fileURLToPath } from 'node:url'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | import { defineConfig } from 'vitest/config'; 5 | 6 | import { svelteTesting } from '@testing-library/svelte/vite'; 7 | 8 | export default defineConfig({ 9 | plugins: [tsconfigPaths(), sveltekit(), svelteTesting()], 10 | server: { 11 | port: 4000 12 | }, 13 | preview: { 14 | port: 4000 15 | }, 16 | optimizeDeps: { 17 | esbuildOptions: { 18 | target: 'esnext' 19 | } 20 | }, 21 | build: { 22 | target: 'es2020' 23 | }, 24 | test: { 25 | environment: 'jsdom', 26 | globals: true, 27 | setupFiles: ['./vitest-setup.ts'] 28 | }, 29 | css: { 30 | preprocessorOptions: { 31 | scss: { 32 | api: 'modern' 33 | } 34 | } 35 | }, 36 | define: { 37 | SUPERFORMS_LEGACY: true 38 | }, 39 | resolve: { 40 | alias: { 41 | '@cloudkit/db-schema': fileURLToPath( 42 | new URL('./../../packages/db-schema/src/generated/index.ts', import.meta.url) 43 | ) 44 | } 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /apps/web/vitest-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | 3 | import { vi } from 'vitest'; 4 | 5 | Object.defineProperty(window, 'matchMedia', { 6 | writable: true, 7 | value: vi.fn().mockImplementation((query) => ({ 8 | matches: false, 9 | media: query, 10 | onchange: null, 11 | addListener: vi.fn(), // deprecated 12 | removeListener: vi.fn(), // deprecated 13 | addEventListener: vi.fn(), 14 | removeEventListener: vi.fn(), 15 | dispatchEvent: vi.fn() 16 | })) 17 | }); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudkit.dle.dev", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build:api": "turbo run @cloudkit/service-contract#build", 8 | "build:prod:api": "turbo run @cloudkit/swagger-ui#build", 9 | "dev:storybook": "turbo run @cloudkit/styleguide#storybook", 10 | "dev": "dotenv -e .env.development -- turbo run dev", 11 | "dev:api": "turbo run @cloudkit/swagger-ui#dev", 12 | "prepare": "husky", 13 | "clean": "rimraf --glob .svelte-kit build dist .turbo", 14 | "lint": "turbo run lint -- --fix", 15 | "check": "turbo run check", 16 | "test": "turbo run test", 17 | "sync": "turbo run sync", 18 | "build:local": "dotenv -e .env.development -- turbo run build", 19 | "build": "turbo run build", 20 | "studio":"pnpm --filter=@cloudkit/db-schema studio" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "22.7.6", 24 | "@types/uuid": "^10.0.0", 25 | "dotenv": "^16.4.5", 26 | "dotenv-cli": "^7.4.2", 27 | "husky": "^9.1.5", 28 | "lint-staged": "15.2.10", 29 | "rimraf": "^6.0.1", 30 | "tsx": "^4.19.0", 31 | "typescript": "^5.5.4", 32 | "vite": "5.4.9", 33 | "vite-plugin-tailwind-purgecss": "0.3.3", 34 | "vite-tsconfig-paths": "^5.0.1", 35 | "vitest": "2.1.3", 36 | "wrangler": "3.84.0" 37 | }, 38 | "keywords": [], 39 | "author": "", 40 | "license": "ISC", 41 | "dependencies": { 42 | "@prisma/client": "5.21.0", 43 | "eslint-plugin-yml": "^1.14.0", 44 | "turbo": "^2.2.1" 45 | }, 46 | "packageManager": "pnpm@9.15.2", 47 | "engines": { 48 | "node": ">=v20", 49 | "pnpm": ">=9" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/api-domain/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@cloudkit/eslint-config'] 3 | }; 4 | -------------------------------------------------------------------------------- /packages/api-domain/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,svelte,css,scss,postcss,md,json,yml}": [ 3 | "prettier --config ../../.prettierrc --write --cache ./src" 4 | ], 5 | "*.{ts,svelte,css,scss,yml}": ["eslint --cache --fix"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/api-domain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudkit/api-domain", 3 | "version": "1.0.0", 4 | "description": "Types for cloudkit's API", 5 | "main": "index.js", 6 | "scripts": { 7 | "gen": "tsx ./src/index.ts" 8 | }, 9 | "keywords": [ 10 | "cloudkit", 11 | "api", 12 | "types" 13 | ], 14 | "author": "Daniel Einars", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@asteasolutions/zod-to-openapi": "^7.1.1", 18 | "@cloudkit/db-schema": "workspace:^", 19 | "@cloudkit/eslint-config": "workspace:^", 20 | "tsx": "^4.19.0", 21 | "yaml": "^2.5.1", 22 | "zod": "^3.23.8" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/api-domain/src/index.ts: -------------------------------------------------------------------------------- 1 | throw new Error('Not yet implemented'); 2 | -------------------------------------------------------------------------------- /packages/api-domain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/api-mocks/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: cloudkit 2 | 3 | networks: 4 | cloudkit-net: 5 | name: cloudkit-net 6 | driver: bridge 7 | services: 8 | img: 9 | image: ghcr.io/minimalcompact/thumbor 10 | container_name: cloudkit-img 11 | environment: 12 | # VIRTUAL_HOST is picked up by nginx-proxy. Here it's set for localhost 13 | # but you usually need to point it to your domain, e.g. thumbor.example.com 14 | - VIRTUAL_HOST=localhost 15 | # THUMBOR_NUM_PROCESSES control how many processes run inside the container 16 | # Normally this is set in connection with the number of CPU cores 17 | # Note however that you can also use the docker-compose scale option to dynamically 18 | # scale your thumbor instances 19 | - THUMBOR_NUM_PROCESSES=4 20 | # this would allow CORS from any origin (you can restrict to specific origins if you want) 21 | - CORS_ALLOW_ORIGIN=* 22 | # returns a webp image if browser Accept headers match 23 | - AUTO_WEBP=True 24 | # nginx-proxy does caching automatically, so no need to store the result storage cache 25 | # (this greatly speeds up and saves on CPU) 26 | - RESULT_STORAGE=thumbor.result_storages.no_storage 27 | - RESULT_STORAGE_STORES_UNSAFE=True 28 | - STORAGE=thumbor.storages.file_storage 29 | - UPLOAD_ENABLED=True 30 | - UPLOAD_PUT_ALLOWED=True 31 | - UPLOAD_DELETE_ALLOWED=True 32 | - THUMBOR_PORT=6501 33 | networks: 34 | - cloudkit-net 35 | ports: 36 | - '7501:6501' 37 | 38 | # redis: 39 | # image: redis:6.2 40 | # container_name: cloudkit-auth-cache 41 | # networks: 42 | # - cloudkit-net 43 | # ports: 44 | # - '6379:6379' 45 | # command: redis-server --loglevel warning 46 | db: 47 | image: postgres:latest 48 | container_name: cloudkit-db 49 | networks: 50 | - cloudkit-net 51 | environment: 52 | - POSTGRES_USER=admin 53 | - POSTGRES_PASSWORD=pass123 54 | - POSTGRES_DB=cloudkit-db 55 | ports: 56 | - 7500:5432 57 | # volumes: 58 | # - ./sk-db-dump.sql:/docker-entrypoint-initdb.d/sk-db-dump.sql 59 | -------------------------------------------------------------------------------- /packages/api-mocks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudkit/api-mocks", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "db:dump": "docker exec -i cloudkit-db /bin/bash -c \"PGPASSWORD=pass123 pg_dump --username admin cloudkit-db\" > ./cloudkit-db-dump.sql", 7 | "db:restore": "docker exec -i cloudkit-db /bin/bash -c \"PGPASSWORD=pass123 psql --username admin cloudkit-db\" < ./cloudkit-db-dump.sql", 8 | "db:seed": "pnpm prisma:gen:dev && pnpm prisma:push:dev && dotenv -e .env.development -- node prisma/seed.js", 9 | "start": "docker compose up -d", 10 | "stop": "docker compose stop" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /packages/db-schema/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@cloudkit/eslint-config'] 3 | }; 4 | -------------------------------------------------------------------------------- /packages/db-schema/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,svelte,css,scss,postcss,md,json,yml}": [ 3 | "prettier --config ../../.prettierrc --write --cache ./src" 4 | ], 5 | "*.{ts,svelte,css,scss,yml}": ["eslint --cache --fix"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/db-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudkit/db-schema", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "./src/index.ts", 7 | "types": "./src/index.ts", 8 | "scripts": { 9 | "generate": "prisma generate --no-engine --schema ./prisma/schema.prisma && pnpm format", 10 | "push": "prisma db push --schema ./prisma/schema.prisma && pnpm format", 11 | "push:prod": "dotenv -e ../../.env.production -- prisma db push --schema ./prisma/schema.prisma", 12 | "push:manual": "dotenv -e ../../.env.development -- prisma db push --schema ./prisma/schema.prisma && pnpm format", 13 | "studio": "dotenv -e ../../.env.development -- prisma studio --schema ./prisma/schema.prisma && pnpm format", 14 | "studio:prod": "dotenv -e ../../.env.production -- prisma studio --schema ./prisma/schema.prisma && pnpm format", 15 | "format": "prettier --config ../../.prettierrc --write ." 16 | }, 17 | "dependencies": { 18 | "@prisma/client": "5.21.0", 19 | "@cloudkit/eslint-config": "workspace:^", 20 | "prisma": "^5.19.1", 21 | "zod": "^3.23.8", 22 | "zod-prisma-types": "^3.1.8" 23 | }, 24 | "keywords": [], 25 | "author": "", 26 | "license": "ISC" 27 | } 28 | -------------------------------------------------------------------------------- /packages/db-schema/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | previewFeatures = ["fullTextSearch"] 4 | } 5 | 6 | generator zod { 7 | provider = "zod-prisma-types" 8 | createOptionalDefaultValuesTypes = true 9 | createRelationValuesTypes = true 10 | writeNullishInModelTypes = true 11 | output = "./../src/generated" 12 | } 13 | 14 | datasource db { 15 | provider = "postgresql" 16 | url = env("DATABASE_URL") 17 | } 18 | 19 | model Session { 20 | id String @id 21 | userId String 22 | expiresAt DateTime 23 | user User @relation(references: [id], fields: [userId], onDelete: Cascade) 24 | } 25 | 26 | /// @zod.import(["import { ALLOWED_STRINGS, ERROR_MESSAGE } from '../utils';"]) 27 | model User { 28 | id String @id @unique @default(uuid()) 29 | /// @zod.string.email().min(3) 30 | email String @unique 31 | hashedPassword String 32 | /// @zod.string.min(1).max(32).regex(ALLOWED_STRINGS, ERROR_MESSAGE) 33 | firstName String 34 | /// @zod.string.min(1).max(32).regex(ALLOWED_STRINGS, ERROR_MESSAGE) 35 | lastName String 36 | verified Boolean @default(false) 37 | createdAt DateTime @default(now()) 38 | updatedAt DateTime @default(now()) 39 | avatar Image? 40 | 41 | sessions Session[] 42 | firstTime Boolean @default(true) 43 | 44 | @@index(fields: [id], type: Hash) 45 | } 46 | 47 | 48 | model Image { 49 | id String @id @default(uuid()) 50 | createdAt DateTime @default(now()) 51 | updatedAt DateTime @default(now()) 52 | url String @unique 53 | user User? @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) 54 | userId String? @unique 55 | 56 | 57 | 58 | @@index(fields: [id], type: Hash) 59 | 60 | @@index(fields: [userId], type: Hash) 61 | } 62 | -------------------------------------------------------------------------------- /packages/db-schema/prisma/seed.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { prisma } from '@lucia-auth/adapter-prisma'; 3 | import { PrismaClient } from '@prisma/client'; 4 | import { Lucia } from 'lucia'; 5 | import { createImage, userAttributes } from './seedUtils.js'; 6 | import { PrismaAdapter } from '@lucia-auth/adapter-prisma'; 7 | 8 | const db = new PrismaClient({ 9 | datasources: { 10 | db: { 11 | url: process.env.DATABASE_URL 12 | } 13 | } 14 | }); 15 | 16 | const adapter = new PrismaAdapter(db.session, db.user); 17 | const lucia = new Lucia(adapter, { 18 | sessionCookie: { 19 | attributes: { 20 | // set to `true` when using HTTPS 21 | secure: process.env.NODE_ENV === 'production' 22 | } 23 | }, 24 | 25 | getUserAttributes: (attributes) => { 26 | return { 27 | firstName: attributes.firstName, 28 | lastName: attributes.lastName, 29 | email: attributes.email, 30 | createdAt: attributes.createdAt, 31 | updatedAt: attributes.updatedAt, 32 | verified: attributes.verified 33 | }; 34 | } 35 | }); 36 | const prefix = 'cloudkit'; 37 | 38 | async function freshInit() { 39 | await db.user.deleteMany({}); 40 | await db.group.deleteMany({}); 41 | await db.item.deleteMany({}); 42 | await db.image.deleteMany({}); 43 | // CREATE ADMIN USER 44 | 45 | const user = await auth.createUser({ 46 | key: { 47 | providerId: 'username', 48 | providerUserId: 'admin@dle.dev', 49 | password: 'adminadmin' 50 | }, 51 | attributes: { 52 | email: 'admin@dle.dev' 53 | } 54 | }); 55 | const url = `${process.env.IMAGE_API}/${prefix}/${user.userId}/avatar`; 56 | await db.user.update({ 57 | where: { 58 | id: user.userId, 59 | email: user.email 60 | }, 61 | data: { 62 | firstName: 'Daniel', 63 | lastName: 'Kit', 64 | verified: false, 65 | avatar: { 66 | create: { 67 | url, 68 | id: user.userId 69 | } 70 | } 71 | } 72 | }); 73 | await createImage(url); 74 | 75 | // CREATE RANDOM USERS 76 | for (let i = 0; i < 10; i++) { 77 | const firstName = faker.person.firstName(); 78 | const lastName = faker.person.lastName(); 79 | const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@dle.dev`; 80 | const user = await auth.createUser({ 81 | key: { 82 | providerId: 'username', 83 | providerUserId: email, 84 | password: 'adminadmin' 85 | }, 86 | attributes: { 87 | email 88 | } 89 | }); 90 | const url = `${process.env.IMAGE_API}/${prefix}/${user.userId}/avatar`; 91 | await db.user.update({ 92 | where: { 93 | id: user.userId, 94 | email: user.email 95 | }, 96 | data: { 97 | email, 98 | firstName, 99 | lastName, 100 | verified: false, 101 | avatar: { 102 | create: { 103 | url, 104 | id: user.userId 105 | } 106 | } 107 | } 108 | }); 109 | 110 | await createImage(url); 111 | } 112 | } 113 | 114 | async function main() { 115 | await freshInit(); 116 | } 117 | main() 118 | .then(async () => { 119 | await db.$disconnect(); 120 | }) 121 | .catch(async (e) => { 122 | console.error(e); 123 | await db.$disconnect(); 124 | process.exit(1); 125 | }); 126 | -------------------------------------------------------------------------------- /packages/db-schema/prisma/seedUtils.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import sharp from 'sharp'; 3 | const dev = process.env.SEED_DEV === 'true'; 4 | import fs from 'fs'; 5 | export async function createImage(url) { 6 | const image = await fetch('https://picsum.photos/700') 7 | .then((response) => response.arrayBuffer()) 8 | .then((buffer) => sharp(buffer).webp({ quality: 80 }).toBuffer()); 9 | 10 | return dev 11 | ? await postToLocalThumborInstance(image, url) 12 | : await postToCloudflareImageService(image, url); 13 | } 14 | async function postToCloudflareImageService(image, url) { 15 | const form = new FormData(); 16 | form.append('file', new Blob([image]), url); 17 | form.append('id', url); 18 | const response = await fetch(process.env.IMAGE_API, { 19 | method: 'POST', 20 | headers: { 21 | Authorization: `Bearer ${process.env.IMAGE_API_TOKEN}` 22 | }, 23 | body: form 24 | }); 25 | 26 | console.info('response: ', response); 27 | } 28 | 29 | async function postToLocalThumborInstance(image, url) { 30 | await fetch(url, { 31 | method: 'PUT', 32 | headers: { 33 | 'Content-prefix': 'image/webp', 34 | Slug: `${url}.webp` 35 | }, 36 | body: image 37 | }); 38 | } 39 | 40 | export const userAttributes = (data) => ({ 41 | firstName: data.firstName, 42 | lastName: data.lastName, 43 | email: data.email, 44 | avatar: data.avatar, 45 | createdAt: data.createdAt, 46 | updatedAt: data.updatedAt, 47 | verified: data.verified, 48 | collections: data.collections 49 | }); 50 | -------------------------------------------------------------------------------- /packages/db-schema/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generated'; 2 | export * from './utils'; 3 | -------------------------------------------------------------------------------- /packages/db-schema/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const ALLOWED_STRINGS = 2 | /^[\s!?_"+.,():;&#@%ÀÁÂÃÄÅǍĀĄÆÇČĆÈÉÊËĚĒĘǦÌÍÎÏĪÐĐĎĹĽŁÑŇŃÒÓÔÕÖŌרŘŔŠŚŤÙÚÛÜŮŪÝΫŽŻŹÞßàáâãäåǎāąæçčćďđèéêëěēęǧìíîïīðĺľłñňńòóôõöō÷øřŕšśťùúûüůūýþÿžżźa-zA-Z0-9-\u0027\u2019ŐőŰűĞğİıŞş¡¿ªº£¢€§]*$/; 3 | export const ERROR_MESSAGE = 'This field may only contain letters or numbers'; 4 | 5 | export const MAX_FILE_SIZE = 500000; 6 | export const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; 7 | -------------------------------------------------------------------------------- /packages/db-schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/eslint-config/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{cjs}": [ 3 | "prettier --config ../../.prettierrc --write --cache ./index.cjs" 4 | ]} 5 | -------------------------------------------------------------------------------- /packages/eslint-config/index.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.Config } */ 2 | module.exports = { 3 | root: false, 4 | plugins: ['@typescript-eslint', 'unused-imports'], 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:@typescript-eslint/recommended', 8 | 'plugin:svelte/recommended', 9 | 'plugin:@tanstack/eslint-plugin-query/recommended', 10 | 'plugin:yml/standard', 11 | 'prettier' 12 | ], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | sourceType: 'module', 16 | ecmaVersion: 2020, 17 | extraFileExtensions: ['.svelte'] 18 | }, 19 | env: { 20 | browser: true, 21 | es2017: true, 22 | node: true 23 | }, 24 | overrides: [ 25 | { 26 | files: ['*.svelte'], 27 | parser: 'svelte-eslint-parser', 28 | parserOptions: { 29 | parser: '@typescript-eslint/parser' 30 | } 31 | }, 32 | { 33 | files: ['*.yml'], 34 | parser: 'yaml-eslint-parser' 35 | } 36 | ], 37 | rules: { 38 | 'no-console': ['error', { allow: ['warn', 'error'] }], 39 | 'lines-around-directive': ['error', { before: 'always', after: 'always' }] 40 | }, 41 | ignorePatterns: ['*.js', '*.json', '*.md', '!turbo.json', '*.css'] 42 | }; 43 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudkit/eslint-config", 3 | "version": "0.0.1", 4 | "main": "./index.cjs", 5 | "module": "./index.cjs", 6 | "author": "Daniel Einars", 7 | "type": "module", 8 | "description": "Eslint config for WhatsIn", 9 | "exports": { 10 | ".": { 11 | "import": "./index.cjs", 12 | "default": "./index.cjs" 13 | } 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/polaroidkidd/whatsin.fyi.git" 18 | }, 19 | "keywords": [ 20 | "whatsin" 21 | ], 22 | "license": "ISC", 23 | "dependencies": { 24 | "@typescript-eslint/eslint-plugin": "^7.17.0", 25 | "@typescript-eslint/parser": "^7.17.0", 26 | "eslint": "^8.57.0", 27 | "eslint-config-prettier": "^9.1.0", 28 | "eslint-config-turbo": "^2.1.2", 29 | "eslint-plugin-simple-import-sort": "^12.1.1", 30 | "eslint-plugin-svelte": "^2.43.0", 31 | "eslint-plugin-unused-imports": "^3.2.0", 32 | "eslint-plugin-yml": "^1.14.0", 33 | "prettier": "3.3.3", 34 | "prettier-plugin-svelte": "^3.2.6", 35 | "yaml-eslint-parser": "^1.2.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/eslint-config/vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { resolve } from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | // https://vitejs.dev/guide/build.html#library-mode 6 | export default defineConfig({ 7 | build: { 8 | lib: { 9 | entry: resolve(__dirname, 'index.cjs'), 10 | name: '@cloudkit/eslint-config', 11 | fileName: 'index' 12 | } 13 | }, 14 | plugins: [] 15 | }); 16 | -------------------------------------------------------------------------------- /packages/service-contract/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@cloudkit/eslint-config'] 3 | }; 4 | -------------------------------------------------------------------------------- /packages/service-contract/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,svelte,css,scss,postcss,md,json,yml}": [ 3 | "prettier --config ../../.prettierrc --write --cache ./src" 4 | ], 5 | "*.{ts,svelte,css,scss,yml}": ["eslint --cache --fix"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/service-contract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudkit/service-contract", 3 | "version": "0.0.1", 4 | "main": "./dist/cloudkit-openapi-docs.json", 5 | "author": "Daniel Einars", 6 | "description": "OpenAPI Spec for cloudkit", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/cloudkit-openapi-docs.json", 10 | "require": "./dist/cloudkit-openapi-docs.json" 11 | } 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/polaroidkidd/cloudkit.dle.dev.git" 16 | }, 17 | "scripts": { 18 | "build": "pnpm dist && pnpm format", 19 | "dist": "tsx ./src/index.ts", 20 | "format": "prettier --config ../../.prettierrc --write ." 21 | }, 22 | "keywords": [ 23 | "cloudkit", 24 | "openapi" 25 | ], 26 | "license": "ISC", 27 | "dependencies": { 28 | "@asteasolutions/zod-to-openapi": "^7.1.1", 29 | "@cloudkit/db-schema": "workspace:^", 30 | "@cloudkit/eslint-config": "workspace:^", 31 | "tsx": "^4.19.0", 32 | "yaml": "^2.5.1", 33 | "zod": "^3.23.8" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/service-contract/src/models.ts: -------------------------------------------------------------------------------- 1 | import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; 2 | import { 3 | ACCEPTED_IMAGE_TYPES, 4 | ImageOptionalDefaultsSchema, 5 | MAX_FILE_SIZE, 6 | UserOptionalDefaultsSchema 7 | } from '@cloudkit/db-schema'; 8 | 9 | import { z } from 'zod'; 10 | extendZodWithOpenApi(z); 11 | 12 | export const UserDTO = UserOptionalDefaultsSchema; 13 | export const FileDTO = z 14 | .any() 15 | .refine((files) => files?.length == 1, 'Image is required.') 16 | .refine((files) => files?.[0]?.size <= MAX_FILE_SIZE, `Max file size is 5MB.`) 17 | .refine( 18 | (files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type), 19 | '.jpg, .jpeg, .png and .webp files are accepted.' 20 | ) 21 | .optional() 22 | .or(z.string().optional()); 23 | 24 | export const ImageDTO = ImageOptionalDefaultsSchema.merge( 25 | z.object({ 26 | file: FileDTO.optional() 27 | }) 28 | ); 29 | 30 | export const ZodErrorSchemaDTO = z.object({ 31 | name: z.string(), 32 | issues: z.array( 33 | z.object({ 34 | code: z.string(), 35 | expected: z.string(), 36 | received: z.string(), 37 | path: z.array(z.string()), 38 | message: z.string() 39 | }) 40 | ) 41 | }); 42 | -------------------------------------------------------------------------------- /packages/service-contract/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/service-contract/vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { resolve } from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | // https://vitejs.dev/guide/build.html#library-mode 6 | export default defineConfig({ 7 | build: { 8 | lib: { 9 | entry: resolve(__dirname, 'index.cjs'), 10 | name: '@cloudkit/service-contract', 11 | fileName: 'index' 12 | } 13 | }, 14 | plugins: [] 15 | }); 16 | -------------------------------------------------------------------------------- /packages/tests/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@cloudkit/eslint-config'] 3 | }; 4 | -------------------------------------------------------------------------------- /packages/tests/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,svelte,css,scss,postcss,md,json,yml}": [ 3 | "prettier --config ../../.prettierrc --write --cache ./e2e" 4 | ], 5 | "*.{ts,svelte,css,scss,yml}": ["eslint --cache --fix"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudkit/tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint ./e2e" 8 | }, 9 | "dependencies": { 10 | "@playwright/test": "^1.47.0", 11 | "@cloudkit/eslint-config": "workspace:^", 12 | "jest": "^29.7.0", 13 | "playwright": "^1.47.0" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC" 18 | } 19 | -------------------------------------------------------------------------------- /packages/tests/playwright.config.ci.ts: -------------------------------------------------------------------------------- 1 | import { devices } from '@playwright/test'; 2 | import { defineConfig } from '@playwright/test'; 3 | import dotenv from 'dotenv'; 4 | import path from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file 8 | const __dirname = path.dirname(__filename); // get the name of the directory 9 | dotenv.config({ path: path.resolve(__dirname, '.env.development') }); 10 | 11 | export default defineConfig({ 12 | webServer: { 13 | command: 'pnpm run-ci', 14 | port: 3000 15 | }, 16 | timeout: 30_000, 17 | expect: { 18 | timeout: 30_000 19 | }, 20 | 21 | retries: 2, 22 | 23 | use: { 24 | trace: 'retain-on-failure', 25 | video: 'retain-on-failure', 26 | 27 | actionTimeout: 10_000 28 | }, 29 | 30 | reporter: 'github', 31 | testDir: 'tests/e2e', 32 | testMatch: /(.+\.)?(test|spec)\.[jt]s/, 33 | 34 | projects: [ 35 | { 36 | name: 'chromium', 37 | use: { 38 | ...devices['Desktop Chrome'], 39 | channel: 'chrome', 40 | viewport: { width: 1920, height: 1080 }, 41 | baseURL: 'http://localhost:4000' 42 | } 43 | } 44 | ] 45 | }); 46 | -------------------------------------------------------------------------------- /packages/tests/playwright.config.dev.ts: -------------------------------------------------------------------------------- 1 | import { devices } from '@playwright/test'; 2 | import { defineConfig } from '@playwright/test'; 3 | import dotenv from 'dotenv'; 4 | import path from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file 8 | const __dirname = path.dirname(__filename); // get the name of the directory 9 | dotenv.config({ path: path.resolve(__dirname, '.env.development') }); 10 | 11 | export default defineConfig({ 12 | webServer: { 13 | command: 'pnpm run-ci', 14 | port: 3000 15 | }, 16 | retries: 0, 17 | // test timeout set to 10 seconds 18 | timeout: 30_000, 19 | expect: { 20 | // expect timeout set to 10 seconds 21 | timeout: 30_000 22 | }, 23 | // Reporter to use 24 | use: { 25 | actionTimeout: 10_000 26 | }, 27 | // Reporter to use 28 | reporter: 'html', 29 | testDir: 'tests/e2e', 30 | testMatch: /(.+\.)?(test|spec)\.[jt]s/, 31 | projects: [ 32 | { 33 | name: 'chromium', 34 | use: { 35 | ...devices['Desktop Chrome'], 36 | channel: 'chrome', 37 | viewport: { width: 1920, height: 1080 }, 38 | baseURL: 'http://localhost:4000' 39 | } 40 | } 41 | ] 42 | }); 43 | -------------------------------------------------------------------------------- /packages/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/tests/unit/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ui-core/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@cloudkit/eslint-config'] 3 | }; 4 | -------------------------------------------------------------------------------- /packages/ui-core/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,svelte,css,scss,postcss,md,json,yml}": [ 3 | "prettier --config ../../.prettierrc --write --cache ./src" 4 | ], 5 | "*.{ts,svelte,css,scss,yml}": ["eslint --cache --fix"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudkit/ui-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "svelte-kit sync && svelte-package -w", 8 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 9 | "lint": "eslint ./src", 10 | "test": "vitest run", 11 | "sync": "svelte-kit sync" 12 | }, 13 | "exports": { 14 | ".": { 15 | "types": "./src/index.ts", 16 | "default": "./src/index.ts" 17 | } 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "dependencies": { 23 | "@cloudkit/db-schema": "workspace:^", 24 | "@cloudkit/eslint-config": "workspace:^", 25 | "@floating-ui/dom": "^1.6.10", 26 | "@skeletonlabs/skeleton": "2.10.2", 27 | "@skeletonlabs/tw-plugin": "^0.4.0", 28 | "@svelte-put/qr": "^1.2.1", 29 | "@sveltejs/adapter-node": "5.2.2", 30 | "@sveltejs/kit": "^2.5.26", 31 | "@tailwindcss/container-queries": "^0.1.1", 32 | "@tailwindcss/forms": "0.5.9", 33 | "@tailwindcss/typography": "0.5.15", 34 | "axios": "^1.7.7", 35 | "browser-image-compression": "^2.0.2", 36 | "classnames": "^2.5.1", 37 | "sass": "^1.78.0", 38 | "svelte": "^4.2.19", 39 | "sveltekit-superforms": "^2.19.1", 40 | "tailwind-merge": "^2.5.2", 41 | "tailwindcss": "^3.4.10", 42 | "uuid": "^10.0.0", 43 | "zod": "^3.23.8" 44 | }, 45 | "devDependencies": { 46 | "@storybook/addon-svelte-csf": "^4.1.7", 47 | "@storybook/svelte": "^8.3.5", 48 | "@sveltejs/adapter-auto": "^3.0.0", 49 | "@sveltejs/adapter-static": "^3.0.5", 50 | "@sveltejs/package": "^2.3.5", 51 | "@sveltejs/vite-plugin-svelte": "^3.1.2", 52 | "@testing-library/jest-dom": "^6.6.2", 53 | "@testing-library/svelte": "^5.2.4", 54 | "@testing-library/user-event": "^14.5.2", 55 | "@vitest/ui": "^2.1.3", 56 | "autoprefixer": "^10.4.20", 57 | "jsdom": "^25.0.1", 58 | "postcss": "^8.4.45", 59 | "postcss-load-config": "^6.0.1", 60 | "svelte-check": "^4.0.1", 61 | "svelte-preprocess": "^6.0.2", 62 | "type-fest": "^4.26.1", 63 | "vite-tsconfig-paths": "^5.0.1", 64 | "vitest": "2.1.3" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/ui-core/src/api/base-api-client.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'; 2 | import axios from 'axios'; 3 | 4 | export abstract class ApiServiceBase { 5 | public context: string; 6 | 7 | protected readonly http: AxiosInstance; 8 | 9 | constructor( 10 | context: string, 11 | commonRequestConfig: AxiosRequestConfig = { 12 | transformRequest: [ 13 | (data) => { 14 | return JSON.stringify(data); 15 | } 16 | ] 17 | } 18 | ) { 19 | this.context = context; 20 | 21 | let { headers = {} } = commonRequestConfig; 22 | if (headers['Content-Type'] == null) { 23 | headers = { 24 | ...headers, 25 | 'Content-Type': 'application/json' 26 | }; 27 | } 28 | 29 | this.http = axios.create({ 30 | ...commonRequestConfig, 31 | headers, 32 | baseURL: context, 33 | paramsSerializer: { indexes: null } 34 | }); 35 | // /** 36 | // * Use this to log out all requests during a test run. 37 | // */ 38 | // if (process.env.NODE_ENV === 'test') { 39 | // this.http.interceptors.request.use( 40 | // (config) => { 41 | // console.log(`${config.method.toUpperCase()} ${config.baseURL}${config.url}`); 42 | // 43 | // return config; 44 | // } 45 | // ); 46 | // } 47 | 48 | this.http.interceptors.response.use(null, (error: AxiosError) => { 49 | if (error.response && typeof error.response.data === 'string') { 50 | // Use the verify middleware here verifySessionTimeout(error.response); 51 | // verifyMaintenanceMode(error.response as AxiosResponse); 52 | } 53 | 54 | return Promise.reject(error); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/ui-core/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %sveltekit.head% 5 | 6 | 7 | 14 | 15 | 16 |
%sveltekit.body%
17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/ui-core/src/cache/index.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment'; 2 | import { error } from '@sveltejs/kit'; 3 | 4 | export const cache = new Map(); 5 | 6 | export const cacheFetch = async (key: string, fetchCallback: () => ReturnType) => { 7 | if (browser && cache.has(key)) { 8 | console.warn(`Cache hit for ${key}`); 9 | return cache.get(key) as T; 10 | } 11 | const response = await fetchCallback(); 12 | 13 | if (!response.ok) { 14 | const message = await response.json(); 15 | throw error(response.status, message); 16 | } 17 | const result = await response.json(); 18 | console.warn(`Caching ${key}`); 19 | cache.set(key, result); 20 | 21 | return result as T; 22 | }; 23 | 24 | export const clearCache = (key: string) => { 25 | if (browser && cache.has(key)) { 26 | console.warn(`Clearing cache for ${key}`); 27 | cache.delete(key); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/buttons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from './simple-button.svelte'; 2 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/buttons/navigation-button.stories.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 | 36 | 37 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/buttons/navigation-button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 56 | {#if $page.url.pathname === href} 57 | 58 | {/if} 59 | 60 | 61 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/buttons/simple-button.stories.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/buttons/simple-button.test.ts: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/svelte'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { expect, test, vi } from 'vitest'; 4 | 5 | import SimpleButton from './simple-button.svelte'; 6 | 7 | test('button with event', async () => { 8 | const user = userEvent.setup(); 9 | const onClick = vi.fn(); 10 | 11 | const { component } = render(SimpleButton); 12 | component.$on('click', onClick); 13 | 14 | const button = screen.getByRole('button'); 15 | await user.click(button); 16 | 17 | expect(onClick).toHaveBeenCalledOnce(); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/card/card-container.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
16 | 17 |
18 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/card/card.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
36 | {#if title.length > 0} 37 | 38 | {title} 39 | 40 | {/if} 41 | {#if actions.length > 0} 42 | 49 | {/if} 50 | 51 |
52 |
53 | {#each actions as action} 54 | {#if action.href} 55 | {action.title} 56 | {:else} 57 | 69 | {/if} 70 | {/each} 71 |
72 |
73 |
74 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/forms/input-types.ts: -------------------------------------------------------------------------------- 1 | export type HTMLInputTypeAttribute = 2 | | 'button' 3 | | 'checkbox' 4 | | 'color' 5 | | 'date' 6 | | 'datetime-local' 7 | | 'email' 8 | | 'file' 9 | | 'hidden' 10 | | 'image' 11 | | 'month' 12 | | 'number' 13 | | 'password' 14 | | 'radio' 15 | | 'range' 16 | | 'reset' 17 | | 'search' 18 | | 'submit' 19 | | 'tel' 20 | | 'text' 21 | | 'time' 22 | | 'url' 23 | | 'week'; 24 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/forms/text-edit.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | value.trim()} 23 | autocomplete={'false'} 24 | /> 25 | 26 | 33 |
34 | {#if invalidData} 35 | 36 | {invalidData} 37 | 38 | {/if} 39 | 40 | 67 | -------------------------------------------------------------------------------- /packages/ui-core/src/components/forms/text-input-area.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 |