component,
+ * immediately followed by the document's and .
+ *
+ * Don't remove the `` and `` elements.
+ */
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
diff --git a/qwik/github-oauth/src/routes/index.tsx b/qwik/github-oauth/src/routes/index.tsx
new file mode 100644
index 0000000..de2b4f2
--- /dev/null
+++ b/qwik/github-oauth/src/routes/index.tsx
@@ -0,0 +1,52 @@
+import { component$ } from "@builder.io/qwik";
+import {
+ type DocumentHead,
+ Form,
+ routeAction$,
+ routeLoader$
+} from "@builder.io/qwik-city";
+
+import { auth } from "~/auth/lucia";
+
+export const useSessionUser = routeLoader$(async (event) => {
+ const authRequest = auth.handleRequest(event);
+ const session = await authRequest.validate();
+ if (!session) throw event.redirect(302, "/signin/");
+ return session.user;
+});
+
+export const useSignOutAction = routeAction$(async (_, event) => {
+ const authRequest = auth.handleRequest(event);
+ const session = await authRequest.validate();
+ if (!session) throw event.error(401, "Unauthorized");
+
+ await auth.invalidateSession(session.sessionId);
+ const { name, value, attributes } = auth.createSessionCookie(null);
+ event.cookie.set(name, value, attributes);
+ throw event.redirect(302, "/login/");
+});
+
+export default component$(() => {
+ const user = useSessionUser();
+ const signout = useSignOutAction();
+ return (
+ <>
+ Profile
+ User id: {user.value.userId}
+ GitHub username: {user.value.username}
+
+ >
+ );
+});
+
+export const head: DocumentHead = {
+ title: "GitHub OAuth with Lucia",
+ meta: [
+ {
+ name: "description",
+ content: "Qwik site description"
+ }
+ ]
+};
diff --git a/qwik/github-oauth/src/routes/layout.tsx b/qwik/github-oauth/src/routes/layout.tsx
new file mode 100644
index 0000000..f9bdd58
--- /dev/null
+++ b/qwik/github-oauth/src/routes/layout.tsx
@@ -0,0 +1,17 @@
+import { Slot, component$ } from "@builder.io/qwik";
+import type { RequestHandler } from "@builder.io/qwik-city";
+
+export const onGet: RequestHandler = async ({ cacheControl }) => {
+ // Control caching for this request for best performance and to reduce hosting costs:
+ // https://qwik.builder.io/docs/caching/
+ cacheControl({
+ // Always serve a cached response by default, up to a week stale
+ staleWhileRevalidate: 60 * 60 * 24 * 7,
+ // Max once every 5 seconds, revalidate on the server to get a fresh version of this page
+ maxAge: 5
+ });
+};
+
+export default component$(() => {
+ return ;
+});
diff --git a/qwik/github-oauth/src/routes/service-worker.ts b/qwik/github-oauth/src/routes/service-worker.ts
new file mode 100644
index 0000000..8ae1dc9
--- /dev/null
+++ b/qwik/github-oauth/src/routes/service-worker.ts
@@ -0,0 +1,18 @@
+/*
+ * WHAT IS THIS FILE?
+ *
+ * The service-worker.ts file is used to have state of the art prefetching.
+ * https://qwik.builder.io/qwikcity/prefetching/overview/
+ *
+ * Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline.
+ * You can also use this file to add more functionality that runs in the service worker.
+ */
+import { setupServiceWorker } from "@builder.io/qwik-city/service-worker";
+
+setupServiceWorker();
+
+declare const self: ServiceWorkerGlobalScope;
+
+addEventListener("install", () => self.skipWaiting());
+
+addEventListener("activate", () => self.clients.claim());
diff --git a/qwik/github-oauth/src/routes/signin/github/callback/index.ts b/qwik/github-oauth/src/routes/signin/github/callback/index.ts
new file mode 100644
index 0000000..a8299ab
--- /dev/null
+++ b/qwik/github-oauth/src/routes/signin/github/callback/index.ts
@@ -0,0 +1,38 @@
+import type { RequestHandler } from "@builder.io/qwik-city";
+
+import { auth, githubAuth } from "~/auth/lucia";
+
+export const onRequest: RequestHandler = async (event) => {
+ const authRequest = auth.handleRequest(event);
+ const session = await authRequest.validate();
+ if (session) throw event.redirect(302, "/");
+ const storedState = event.cookie.get("github_oauth_state")?.value;
+ const state = event.query.get("state");
+ const code = event.query.get("code");
+ if (!storedState || !state || storedState !== state || !code) {
+ throw event.error(400, "");
+ }
+
+ const { getExistingUser, githubUser, createUser } =
+ await githubAuth.validateCallback(code);
+
+ const getUser = async () => {
+ const existingUser = await getExistingUser();
+ if (existingUser) return existingUser;
+ const user = await createUser({
+ attributes: {
+ username: githubUser.login
+ }
+ });
+ return user;
+ };
+
+ const user = await getUser();
+ const newSession = await auth.createSession({
+ userId: user.userId,
+ attributes: {}
+ });
+ const { name, value, attributes } = auth.createSessionCookie(newSession);
+ event.cookie.set(name, value, attributes);
+ throw event.redirect(302, "/");
+};
diff --git a/qwik/github-oauth/src/routes/signin/github/index.ts b/qwik/github-oauth/src/routes/signin/github/index.ts
new file mode 100644
index 0000000..fd36709
--- /dev/null
+++ b/qwik/github-oauth/src/routes/signin/github/index.ts
@@ -0,0 +1,14 @@
+import type { RequestHandler } from "@builder.io/qwik-city";
+
+import { githubAuth } from "~/auth/lucia";
+
+export const onGet: RequestHandler = async (event) => {
+ const [url, state] = await githubAuth.getAuthorizationUrl();
+ event.cookie.set("github_oauth_state", state, {
+ httpOnly: true,
+ secure: false,
+ path: "/",
+ maxAge: 60 * 60
+ });
+ throw event.redirect(302, url.toString());
+};
diff --git a/qwik/github-oauth/src/routes/signin/index.tsx b/qwik/github-oauth/src/routes/signin/index.tsx
new file mode 100644
index 0000000..1aa4030
--- /dev/null
+++ b/qwik/github-oauth/src/routes/signin/index.tsx
@@ -0,0 +1,10 @@
+import { component$ } from "@builder.io/qwik";
+
+export default component$(() => {
+ return (
+ <>
+ Sign in
+ Sign in with GitHub
+ >
+ );
+});
diff --git a/qwik/github-oauth/tsconfig.json b/qwik/github-oauth/tsconfig.json
new file mode 100644
index 0000000..6fb74a8
--- /dev/null
+++ b/qwik/github-oauth/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "target": "ES2017",
+ "module": "ES2022",
+ "lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "jsxImportSource": "@builder.io/qwik",
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "incremental": true,
+ "isolatedModules": true,
+ "outDir": "tmp",
+ "noEmit": true,
+ "types": ["node", "vite/client"],
+ "paths": {
+ "~/*": ["./src/*"]
+ }
+ },
+ "include": ["src", "./*.d.ts", "./*.config.ts"]
+}
diff --git a/qwik/github-oauth/vite.config.ts b/qwik/github-oauth/vite.config.ts
new file mode 100644
index 0000000..5c11249
--- /dev/null
+++ b/qwik/github-oauth/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from "vite";
+import { qwikVite } from "@builder.io/qwik/optimizer";
+import { qwikCity } from "@builder.io/qwik-city/vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig(() => {
+ return {
+ plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
+ preview: {
+ headers: {
+ "Cache-Control": "public, max-age=600",
+ },
+ },
+ };
+});
diff --git a/qwik/username-and-password/.env.example b/qwik/username-and-password/.env.example
new file mode 100644
index 0000000..0e0e5f9
--- /dev/null
+++ b/qwik/username-and-password/.env.example
@@ -0,0 +1,2 @@
+GITHUB_CLIENT_ID=""
+GITHUB_CLIENT_SECRET=""
\ No newline at end of file
diff --git a/qwik/username-and-password/.gitignore b/qwik/username-and-password/.gitignore
new file mode 100644
index 0000000..48dce73
--- /dev/null
+++ b/qwik/username-and-password/.gitignore
@@ -0,0 +1,41 @@
+# Build
+/dist
+/lib
+/lib-types
+/server
+
+# Development
+node_modules
+*.local
+
+# Cache
+.cache
+.mf
+.rollup.cache
+tsconfig.tsbuildinfo
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# Editor
+.vscode/*
+!.vscode/launch.json
+!.vscode/*.code-snippets
+
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Yarn
+.yarn/*
+!.yarn/releases
diff --git a/qwik/username-and-password/README.md b/qwik/username-and-password/README.md
new file mode 100644
index 0000000..5ea2da3
--- /dev/null
+++ b/qwik/username-and-password/README.md
@@ -0,0 +1,26 @@
+# Username & password example with Lucia and Qwik
+
+This example uses `better-sqlite3`.
+
+```bash
+# install dependencies
+pnpm i
+
+# run dev server
+pnpm dev
+```
+
+## Runtime
+
+This example is built for Node.js 20. If you're using Node.js 16/18, un-comment the following lines in `auth/lucia.ts`:
+
+```ts
+// import "lucia/polyfill/node";
+```
+
+## User schema
+
+| id | type | unique |
+| ---------- | -------- | :----: |
+| `id` | `string` | |
+| `username` | `string` | ✓ |
diff --git a/qwik/username-and-password/package.json b/qwik/username-and-password/package.json
new file mode 100644
index 0000000..b824b08
--- /dev/null
+++ b/qwik/username-and-password/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "username-and-password",
+ "description": "Username & password example with Lucia and Qwik",
+ "engines": {
+ "node": ">=15.0.0"
+ },
+ "scripts": {
+ "build": "qwik build",
+ "build.client": "vite build",
+ "build.preview": "vite build --ssr src/entry.preview.tsx",
+ "build.types": "tsc --incremental --noEmit",
+ "dev": "vite --mode ssr",
+ "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force",
+ "lint": "eslint \"src/**/*.ts*\"",
+ "preview": "qwik build preview && vite preview --open",
+ "start": "vite --open --mode ssr",
+ "qwik": "qwik"
+ },
+ "devDependencies": {
+ "@builder.io/qwik": "^1.2.10",
+ "@builder.io/qwik-city": "^1.2.10",
+ "@libsql/client": "^0.3.4",
+ "@lucia-auth/adapter-sqlite": "latest",
+ "@types/better-sqlite3": "^7.6.4",
+ "@types/eslint": "8.44.1",
+ "@types/node": "^20.4.5",
+ "@typescript-eslint/eslint-plugin": "6.2.0",
+ "@typescript-eslint/parser": "6.2.0",
+ "better-sqlite3": "^8.4.0",
+ "eslint": "8.45.0",
+ "eslint-plugin-qwik": "^1.2.10",
+ "lucia": "latest",
+ "prettier": "3.0.0",
+ "typescript": "5.1.6",
+ "undici": "5.22.1",
+ "vite": "4.4.7",
+ "vite-tsconfig-paths": "4.2.0"
+ }
+}
\ No newline at end of file
diff --git a/qwik/username-and-password/public/favicon.svg b/qwik/username-and-password/public/favicon.svg
new file mode 100644
index 0000000..0ded7c1
--- /dev/null
+++ b/qwik/username-and-password/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/qwik/username-and-password/public/manifest.json b/qwik/username-and-password/public/manifest.json
new file mode 100644
index 0000000..c18e75f
--- /dev/null
+++ b/qwik/username-and-password/public/manifest.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json.schemastore.org/web-manifest-combined.json",
+ "name": "qwik-project-name",
+ "short_name": "Welcome to Qwik",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#fff",
+ "description": "A Qwik project app."
+}
diff --git a/qwik/username-and-password/public/robots.txt b/qwik/username-and-password/public/robots.txt
new file mode 100644
index 0000000..e69de29
diff --git a/qwik/username-and-password/schema.sql b/qwik/username-and-password/schema.sql
new file mode 100644
index 0000000..c62ad0c
--- /dev/null
+++ b/qwik/username-and-password/schema.sql
@@ -0,0 +1,17 @@
+CREATE TABLE user (
+ id VARCHAR(15) PRIMARY KEY,
+ username VARCHAR(31) NOT NULL UNIQUE
+);
+CREATE TABLE user_key (
+ id VARCHAR(255) PRIMARY KEY,
+ user_id VARCHAR(15) NOT NULL,
+ hashed_password VARCHAR(255),
+ FOREIGN KEY (user_id) REFERENCES user(id)
+);
+CREATE TABLE user_session (
+ id VARCHAR(127) PRIMARY KEY,
+ user_id VARCHAR(15) NOT NULL,
+ active_expires BIGINT NOT NULL,
+ idle_expires BIGINT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES user(id)
+);
\ No newline at end of file
diff --git a/qwik/username-and-password/src/app.d.ts b/qwik/username-and-password/src/app.d.ts
new file mode 100644
index 0000000..8a2c49f
--- /dev/null
+++ b/qwik/username-and-password/src/app.d.ts
@@ -0,0 +1,8 @@
+///
+declare namespace Lucia {
+ type Auth = import("./auth/lucia").Auth;
+ type DatabaseUserAttributes = {
+ username: string;
+ };
+ type DatabaseSessionAttributes = {};
+}
diff --git a/qwik/username-and-password/src/auth/lucia.ts b/qwik/username-and-password/src/auth/lucia.ts
new file mode 100644
index 0000000..bdd0fba
--- /dev/null
+++ b/qwik/username-and-password/src/auth/lucia.ts
@@ -0,0 +1,29 @@
+import fs from "node:fs";
+import { betterSqlite3 } from "@lucia-auth/adapter-sqlite";
+import sqlite from "better-sqlite3";
+import { lucia } from "lucia";
+import { qwik } from "lucia/middleware";
+// import "lucia/polyfill/node";
+
+const db = sqlite(":memory:");
+db.exec(fs.readFileSync("schema.sql", "utf8"));
+
+export const auth = lucia({
+ adapter: betterSqlite3(db, {
+ user: "user",
+ session: "user_session",
+ key: "user_key"
+ }),
+ env: process.env.NODE_ENV === "development" ? "DEV" : "PROD",
+ middleware: qwik(),
+ sessionCookie: {
+ expires: false
+ },
+ getUserAttributes: (data) => {
+ return {
+ username: data.username
+ };
+ }
+});
+
+export type Auth = typeof auth;
diff --git a/qwik/username-and-password/src/entry.dev.tsx b/qwik/username-and-password/src/entry.dev.tsx
new file mode 100644
index 0000000..3343dc4
--- /dev/null
+++ b/qwik/username-and-password/src/entry.dev.tsx
@@ -0,0 +1,18 @@
+/*
+ * WHAT IS THIS FILE?
+ *
+ * Development entry point using only client-side modules:
+ * - Do not use this mode in production!
+ * - No SSR
+ * - No portion of the application is pre-rendered on the server.
+ * - All of the application is running eagerly in the browser.
+ * - More code is transferred to the browser than in SSR mode.
+ * - Optimizer/Serialization/Deserialization code is not exercised!
+ */
+import { type RenderOptions, render } from "@builder.io/qwik";
+
+import Root from "./root";
+
+export default function (opts: RenderOptions) {
+ return render(document, , opts);
+}
diff --git a/qwik/username-and-password/src/entry.preview.tsx b/qwik/username-and-password/src/entry.preview.tsx
new file mode 100644
index 0000000..44aa4e7
--- /dev/null
+++ b/qwik/username-and-password/src/entry.preview.tsx
@@ -0,0 +1,21 @@
+/*
+ * WHAT IS THIS FILE?
+ *
+ * It's the bundle entry point for `npm run preview`.
+ * That is, serving your app built in production mode.
+ *
+ * Feel free to modify this file, but don't remove it!
+ *
+ * Learn more about Vite's preview command:
+ * - https://vitejs.dev/config/preview-options.html#preview-options
+ *
+ */
+import { createQwikCity } from "@builder.io/qwik-city/middleware/node";
+import qwikCityPlan from "@qwik-city-plan";
+
+import render from "./entry.ssr";
+
+/**
+ * The default export is the QwikCity adapter used by Vite preview.
+ */
+export default createQwikCity({ render, qwikCityPlan });
diff --git a/qwik/username-and-password/src/entry.ssr.tsx b/qwik/username-and-password/src/entry.ssr.tsx
new file mode 100644
index 0000000..836b078
--- /dev/null
+++ b/qwik/username-and-password/src/entry.ssr.tsx
@@ -0,0 +1,31 @@
+/**
+ * WHAT IS THIS FILE?
+ *
+ * SSR entry point, in all cases the application is rendered outside the browser, this
+ * entry point will be the common one.
+ *
+ * - Server (express, cloudflare...)
+ * - npm run start
+ * - npm run preview
+ * - npm run build
+ *
+ */
+import {
+ type RenderToStreamOptions,
+ renderToStream
+} from "@builder.io/qwik/server";
+import { manifest } from "@qwik-client-manifest";
+
+import Root from "./root";
+
+export default function (opts: RenderToStreamOptions) {
+ return renderToStream(, {
+ manifest,
+ ...opts,
+ // Use container attributes to set attributes on the html tag.
+ containerAttributes: {
+ lang: "en-us",
+ ...opts.containerAttributes
+ }
+ });
+}
diff --git a/qwik/username-and-password/src/global.css b/qwik/username-and-password/src/global.css
new file mode 100644
index 0000000..f4a9ade
--- /dev/null
+++ b/qwik/username-and-password/src/global.css
@@ -0,0 +1,42 @@
+/**
+ * WHAT IS THIS FILE?
+ *
+ * Globally applied styles. No matter which components are in the page or matching route,
+ * the styles in here will be applied to the Document, without any sort of CSS scoping.
+ *
+ */
+html {
+ -webkit-text-size-adjust: 100%;
+ -moz-tab-size: 4;
+ -o-tab-size: 4;
+ tab-size: 4;
+ font-family:
+ ui-sans-serif,
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ "Helvetica Neue",
+ Arial,
+ "Noto Sans",
+ sans-serif,
+ "Apple Color Emoji",
+ "Segoe UI Emoji",
+ "Segoe UI Symbol",
+ "Noto Color Emoji";
+}
+
+body {
+ padding: 2rem;
+ line-height: inherit;
+}
+
+* {
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.error {
+ color: red;
+}
diff --git a/qwik/username-and-password/src/root.tsx b/qwik/username-and-password/src/root.tsx
new file mode 100644
index 0000000..61fba7a
--- /dev/null
+++ b/qwik/username-and-password/src/root.tsx
@@ -0,0 +1,31 @@
+import { component$ } from "@builder.io/qwik";
+import {
+ QwikCityProvider,
+ RouterOutlet,
+ ServiceWorkerRegister
+} from "@builder.io/qwik-city";
+
+import "./global.css";
+
+export default component$(() => {
+ /**
+ * The root of a QwikCity site always start with the component,
+ * immediately followed by the document's and .
+ *
+ * Don't remove the `` and `` elements.
+ */
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
diff --git a/qwik/username-and-password/src/routes/(auth)/layout.tsx b/qwik/username-and-password/src/routes/(auth)/layout.tsx
new file mode 100644
index 0000000..ebd15c2
--- /dev/null
+++ b/qwik/username-and-password/src/routes/(auth)/layout.tsx
@@ -0,0 +1,9 @@
+import type { RequestHandler } from "@builder.io/qwik-city";
+
+import { auth } from "~/auth/lucia";
+
+export const onGet: RequestHandler = async (event) => {
+ const authRequest = auth.handleRequest(event);
+ const session = await authRequest.validate();
+ if (session) throw event.redirect(302, "/");
+};
diff --git a/qwik/username-and-password/src/routes/(auth)/signin/index.tsx b/qwik/username-and-password/src/routes/(auth)/signin/index.tsx
new file mode 100644
index 0000000..1326e6b
--- /dev/null
+++ b/qwik/username-and-password/src/routes/(auth)/signin/index.tsx
@@ -0,0 +1,82 @@
+import { component$ } from "@builder.io/qwik";
+import { Form, Link, routeAction$, z, zod$ } from "@builder.io/qwik-city";
+import { LuciaError } from "lucia";
+
+import { auth } from "~/auth/lucia";
+
+export const useSigninAction = routeAction$(
+ async (data, event) => {
+ const { username, password } = data;
+ try {
+ const key = await auth.useKey(
+ "username",
+ username.toLowerCase(),
+ password
+ );
+ const session = await auth.createSession({
+ userId: key.userId,
+ attributes: {}
+ });
+ const { name, value, attributes } = auth.createSessionCookie(session);
+ event.cookie.set(name, value, attributes);
+ event.redirect(302, "/");
+ } catch (e) {
+ if (
+ e instanceof LuciaError &&
+ (e.message === "AUTH_INVALID_KEY_ID" ||
+ e.message === "AUTH_INVALID_PASSWORD")
+ ) {
+ return {
+ success: false,
+ error: "Incorrect username or password"
+ };
+ }
+ }
+ },
+ zod$({
+ username: z.string().min(1).max(31),
+ password: z.string()
+ })
+);
+
+export default component$(() => {
+ const signinAction = useSigninAction();
+ return (
+ <>
+ Sign in
+
+ {signinAction.value?.fieldErrors?.username?.length && (
+ <>
+ Username Issue(s):
+ {signinAction.value.fieldErrors.username.map((error, index) => (
+
+ {error}
+
+ ))}
+ >
+ )}
+ {signinAction.value?.fieldErrors?.password?.length && (
+ <>
+ Password Issue(s):
+ {signinAction.value.fieldErrors.password.map((error, index) => (
+
+ {error}
+
+ ))}
+ >
+ )}
+ {signinAction.value?.error && (
+ Invalid Username or Password
+ )}
+ Create an account
+ >
+ );
+});
diff --git a/qwik/username-and-password/src/routes/(auth)/signup/index.tsx b/qwik/username-and-password/src/routes/(auth)/signup/index.tsx
new file mode 100644
index 0000000..b650f9a
--- /dev/null
+++ b/qwik/username-and-password/src/routes/(auth)/signup/index.tsx
@@ -0,0 +1,99 @@
+import { component$ } from "@builder.io/qwik";
+import { Form, Link, routeAction$, zod$ } from "@builder.io/qwik-city";
+import { SqliteError } from "better-sqlite3";
+
+import { auth } from "~/auth/lucia";
+
+export const useSignUpAction = routeAction$(
+ async (data, event) => {
+ try {
+ const { username, password } = data;
+ const user = await auth.createUser({
+ key: {
+ providerId: "username",
+ providerUserId: username.toLowerCase(),
+ password
+ },
+ attributes: {
+ username
+ }
+ });
+ const session = await auth.createSession({
+ userId: user.userId,
+ attributes: {}
+ });
+ const authRequest = auth.handleRequest(event);
+ authRequest.setSession(session);
+ throw event.redirect(302, "/");
+ } catch (e) {
+ if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") {
+ return {
+ success: false,
+ error: "Unable to create user"
+ };
+ }
+ }
+ },
+ zod$((z) => {
+ return z
+ .object({
+ username: z.string().min(4).max(31),
+ password: z.string().min(6).max(255),
+ confirmPassword: z.string().optional()
+ })
+ .superRefine(({ password, confirmPassword }, ctx) => {
+ if (confirmPassword !== password) {
+ ctx.addIssue({
+ code: "custom",
+ message: "The passwords did not match",
+ path: ["password"]
+ });
+ }
+ });
+ })
+);
+
+export default component$(() => {
+ const signUpAction = useSignUpAction();
+ return (
+ <>
+ Sign up
+
+ {signUpAction.value?.fieldErrors?.username?.length && (
+ <>
+ Username Issue(s):
+ {signUpAction.value.fieldErrors.username.map((error, index) => (
+
+ {error}
+
+ ))}
+ >
+ )}
+ {signUpAction.value?.fieldErrors?.password?.length && (
+ <>
+ Password Issue(s):
+ {signUpAction.value.fieldErrors.password.map((error, index) => (
+
+ {error}
+
+ ))}
+ >
+ )}
+ {signUpAction.value?.error && (
+ {signUpAction.value.error}
+ )}
+ Sign in
+ >
+ );
+});
diff --git a/qwik/username-and-password/src/routes/index.tsx b/qwik/username-and-password/src/routes/index.tsx
new file mode 100644
index 0000000..f54b7bf
--- /dev/null
+++ b/qwik/username-and-password/src/routes/index.tsx
@@ -0,0 +1,51 @@
+import { component$ } from "@builder.io/qwik";
+import {
+ type DocumentHead,
+ Form,
+ routeAction$,
+ routeLoader$
+} from "@builder.io/qwik-city";
+
+import { auth } from "~/auth/lucia";
+
+export const useSessionUser = routeLoader$(async (event) => {
+ const authRequest = auth.handleRequest(event);
+ const session = await authRequest.validate();
+ if (!session) throw event.redirect(302, "/signin/");
+ return session.user;
+});
+
+export const useSignOutAction = routeAction$(async (_, event) => {
+ const authRequest = auth.handleRequest(event);
+ const session = await authRequest.validate();
+ if (!session) throw event.error(401, "Unauthorized");
+
+ await auth.invalidateSession(session.sessionId);
+ authRequest.setSession(null);
+ throw event.redirect(302, "/login/");
+});
+
+export default component$(() => {
+ const user = useSessionUser();
+ const signout = useSignOutAction();
+ return (
+ <>
+ Profile
+ User id: {user.value.userId}
+ Username: {user.value.username}
+
+ >
+ );
+});
+
+export const head: DocumentHead = {
+ title: "GitHub OAuth with Lucia",
+ meta: [
+ {
+ name: "description",
+ content: "Qwik site description"
+ }
+ ]
+};
diff --git a/qwik/username-and-password/src/routes/layout.tsx b/qwik/username-and-password/src/routes/layout.tsx
new file mode 100644
index 0000000..f9bdd58
--- /dev/null
+++ b/qwik/username-and-password/src/routes/layout.tsx
@@ -0,0 +1,17 @@
+import { Slot, component$ } from "@builder.io/qwik";
+import type { RequestHandler } from "@builder.io/qwik-city";
+
+export const onGet: RequestHandler = async ({ cacheControl }) => {
+ // Control caching for this request for best performance and to reduce hosting costs:
+ // https://qwik.builder.io/docs/caching/
+ cacheControl({
+ // Always serve a cached response by default, up to a week stale
+ staleWhileRevalidate: 60 * 60 * 24 * 7,
+ // Max once every 5 seconds, revalidate on the server to get a fresh version of this page
+ maxAge: 5
+ });
+};
+
+export default component$(() => {
+ return ;
+});
diff --git a/qwik/username-and-password/src/routes/service-worker.ts b/qwik/username-and-password/src/routes/service-worker.ts
new file mode 100644
index 0000000..8ae1dc9
--- /dev/null
+++ b/qwik/username-and-password/src/routes/service-worker.ts
@@ -0,0 +1,18 @@
+/*
+ * WHAT IS THIS FILE?
+ *
+ * The service-worker.ts file is used to have state of the art prefetching.
+ * https://qwik.builder.io/qwikcity/prefetching/overview/
+ *
+ * Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline.
+ * You can also use this file to add more functionality that runs in the service worker.
+ */
+import { setupServiceWorker } from "@builder.io/qwik-city/service-worker";
+
+setupServiceWorker();
+
+declare const self: ServiceWorkerGlobalScope;
+
+addEventListener("install", () => self.skipWaiting());
+
+addEventListener("activate", () => self.clients.claim());
diff --git a/qwik/username-and-password/tsconfig.json b/qwik/username-and-password/tsconfig.json
new file mode 100644
index 0000000..6fb74a8
--- /dev/null
+++ b/qwik/username-and-password/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "target": "ES2017",
+ "module": "ES2022",
+ "lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "jsxImportSource": "@builder.io/qwik",
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "incremental": true,
+ "isolatedModules": true,
+ "outDir": "tmp",
+ "noEmit": true,
+ "types": ["node", "vite/client"],
+ "paths": {
+ "~/*": ["./src/*"]
+ }
+ },
+ "include": ["src", "./*.d.ts", "./*.config.ts"]
+}
diff --git a/qwik/username-and-password/vite.config.ts b/qwik/username-and-password/vite.config.ts
new file mode 100644
index 0000000..5c11249
--- /dev/null
+++ b/qwik/username-and-password/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from "vite";
+import { qwikVite } from "@builder.io/qwik/optimizer";
+import { qwikCity } from "@builder.io/qwik-city/vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig(() => {
+ return {
+ plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
+ preview: {
+ headers: {
+ "Cache-Control": "public, max-age=600",
+ },
+ },
+ };
+});