commit ea1438363b9206f347efe219fc3785c8965376cc Author: Korbs Date: Tue Jul 16 02:05:26 2024 -0400 Init diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..5df4df0 --- /dev/null +++ b/.env.sample @@ -0,0 +1,3 @@ +PUBLIC_APPWRITE_ENDPOINT="https://example.com/v1" +PUBLIC_APPWRITE_PROJECT_ID="" +APPWRITE_KEY="" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffe304a --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env + +# macOS-specific files +.DS_Store diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..fe6b0d1 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,12 @@ +import { defineConfig } from "astro/config"; +import node from "@astrojs/node"; + +export default defineConfig({ + output: "server", + adapter: node({ + mode: "standalone", + }), + security: { + checkOrigin: true + } +}); diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..14de6bf Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..b0622b1 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "server-side-rendering", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro check && astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@appwrite.io/pink": "^0.3.0", + "@appwrite.io/pink-icons": "^0.3.0", + "@astrojs/check": "^0.4.1", + "@astrojs/node": "^8.1.0", + "appwrite": "^15.0.0", + "astro": "^4.11.5", + "node-appwrite": "^12.0.0", + "typescript": "^5.3.3" + } +} diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..f52e718 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,18 @@ +/// + +declare namespace App { + interface Locals { + user?: import("node-appwrite").Models.User >; + users?: import("node-appwrite").Service.UserList >; + } +} + +interface ImportMetaEnv { + readonly PUBLIC_APPWRITE_ENDPOINT: string; + readonly PUBLIC_APPWRITE_PROJECT_ID: string; + readonly APPWRITE_KEY: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file diff --git a/src/images/appwrite-logo-dark.svg b/src/images/appwrite-logo-dark.svg new file mode 100644 index 0000000..af9916d --- /dev/null +++ b/src/images/appwrite-logo-dark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/images/login-dark-mode.png b/src/images/login-dark-mode.png new file mode 100644 index 0000000..682b517 Binary files /dev/null and b/src/images/login-dark-mode.png differ diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro new file mode 100644 index 0000000..4fa864c --- /dev/null +++ b/src/layouts/Layout.astro @@ -0,0 +1 @@ + diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..0fdff14 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,11 @@ +import { defineMiddleware } from "astro:middleware"; +import { createSessionClient } from "./server/appwrite"; + +export const onRequest = defineMiddleware(async ({ request, locals }, next) => { + try { + const { account } = createSessionClient(request); + locals.user = await account.get(); + } catch {} + + return next(); +}); diff --git a/src/oauth.ts b/src/oauth.ts new file mode 100644 index 0000000..433d85d --- /dev/null +++ b/src/oauth.ts @@ -0,0 +1,16 @@ +export async function signInWithDiscord() { + const url = new URL( + `${import.meta.env.PUBLIC_APPWRITE_ENDPOINT}/account/sessions/oauth2/discord` + ); + + const origin = window.location.origin; + + url.searchParams.set("project", import.meta.env.PUBLIC_APPWRITE_PROJECT_ID!); + url.searchParams.set("success", `${origin}/account`); + url.searchParams.set("failure", `${origin}/signin`); + + /* Important: For SSR we set token=true to get a auth token in the success URL */ + url.searchParams.set("token", `true`); + + window.location.href = url.toString(); +} diff --git a/src/pages/account.astro b/src/pages/account.astro new file mode 100644 index 0000000..165f316 --- /dev/null +++ b/src/pages/account.astro @@ -0,0 +1,45 @@ +--- +const { user, users } = Astro.locals +if (!user) {return Astro.redirect("/signin")} +import { SESSION_COOKIE, createAdminClient } from "../server/appwrite"; + +if (user.emailVerification) {var EmailIsVerified = true} +else {var EmailIsVerified = false} + +if (Astro.cookies.get('anonymous')) {var AN = true} +else {var AN = false} + +const { account } = createAdminClient(); +// const session = await account.createMagicURLToken(user.$id,user.email); +--- + +

Email: {AN ? None : {user.email}}

+

ID: {user.$id}

+ +

Name: {AN ? Anonymous : {user.name}}

+

Created at: {user.$createdAt}

+

Last login: {user.accessedAt}

+

Last updated: {user.$updatedAt}

+

Labels: + {user.labels.map((data) => +

    +
  • {data}
  • +
+ )} +

+ +Theme: {user.prefs.darkTheme} + +{EmailIsVerified ? +

Your account is verified

+ : +

Your account is not verified

+} + +{AN ? +

This is an anonmyous account.

+ : +

Not an anonmyous account.

+} + +Sign Out \ No newline at end of file diff --git a/src/pages/api/an.ts b/src/pages/api/an.ts new file mode 100644 index 0000000..cc22f94 --- /dev/null +++ b/src/pages/api/an.ts @@ -0,0 +1,24 @@ +import type { APIRoute } from "astro"; +import { createAdminClient, SESSION_COOKIE } from "../../server/appwrite"; + +export const GET: APIRoute = async ({ cookies, redirect, url }) => { + const { account } = createAdminClient(); + + cookies.set('anonymous', 'true') + const secret = url.searchParams.get("secret"); + + const session = await account.createAnonymousSession(secret); + if (!session.secret) { + throw new Error("Failed to create session from token"); + } + + cookies.set(SESSION_COOKIE, session.secret, { + sameSite: "strict", + expires: new Date(session.expire), + secure: true, + httpOnly: true, + path: "/", + }); + + return redirect("/account"); +}; diff --git a/src/pages/api/email/send-verification.ts b/src/pages/api/email/send-verification.ts new file mode 100644 index 0000000..0189dba --- /dev/null +++ b/src/pages/api/email/send-verification.ts @@ -0,0 +1,15 @@ +// When given all roles to ID that is used, the system says +// the role "applications" is still a missing scope. +import type { APIRoute } from "astro"; +import { createAdminClient, SESSION_COOKIE } from "../../../server/appwrite"; + +export const GET: APIRoute = async ({ cookies, redirect, url }) => { + const { account } = createAdminClient(); + + const result = await account.createVerification( + 'http://localhost:4321/account/' + ); + + console.log(result); + return redirect("/account"); +}; diff --git a/src/pages/api/oauth/discord.ts b/src/pages/api/oauth/discord.ts new file mode 100644 index 0000000..6ee5ea8 --- /dev/null +++ b/src/pages/api/oauth/discord.ts @@ -0,0 +1,40 @@ +import type { APIRoute } from "astro"; +import { createAdminClient, SESSION_COOKIE } from "../../../server/appwrite"; + +export const POST: APIRoute = async ({ redirect, url }) => { + const { account } = createAdminClient(); + + const redirectUrl = await account.createOAuth2Token( + "discord", + `${url.origin}/api/oauth`, + `${url.origin}/signin` + ); + + return redirect(redirectUrl); +}; + +export const GET: APIRoute = async ({ cookies, redirect, url }) => { + const userId = url.searchParams.get("userId"); + const secret = url.searchParams.get("secret"); + + if (!userId || !secret) { + throw new Error("OAuth2 did not provide userId or secret"); + } + + const { account } = createAdminClient(); + + const session = await account.createSession(userId, secret); + if (!session || !session.secret) { + throw new Error("Failed to create session from token"); + } + + cookies.set(SESSION_COOKIE, session.secret, { + sameSite: "strict", + expires: new Date(session.expire), + secure: true, + httpOnly: true, + path: "/", + }); + + return redirect("/account"); +}; diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..e068fca --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,8 @@ +--- +const { user } = Astro.locals; +if (user) { + return Astro.redirect("/account"); +} + +return Astro.redirect("/signin"); +--- diff --git a/src/pages/settings/update-username.astro b/src/pages/settings/update-username.astro new file mode 100644 index 0000000..a1880de --- /dev/null +++ b/src/pages/settings/update-username.astro @@ -0,0 +1,34 @@ +--- +import { Users } from "node-appwrite"; +import { createAdminClient } from "../../server/appwrite"; + +if (Astro.request.method === "POST") { + const data = await Astro.request.formData(); + const name = data.get("name") as string; + + const { account } = createAdminClient(); + + const promise = await account.updateName(name); + + promise.then(function (response) { + console.log(response); + }, function (error) { + console.log(error); + }); + + + return Astro.redirect("/account"); +} +--- + +
+ + + +
\ No newline at end of file diff --git a/src/pages/signin.astro b/src/pages/signin.astro new file mode 100644 index 0000000..d00c3ac --- /dev/null +++ b/src/pages/signin.astro @@ -0,0 +1,105 @@ +--- +import Layout from "../layouts/Layout.astro"; +import { SESSION_COOKIE, createAdminClient } from "../server/appwrite"; + +const { user } = Astro.locals; +if (user) { + return Astro.redirect("/account"); +} + +if (Astro.request.method === "POST") { + const data = await Astro.request.formData(); + + const email = data.get("email") as string; + const password = data.get("password") as string; + + const { account } = createAdminClient(); + const session = await account.createEmailPasswordSession(email, password); + + Astro.cookies.set(SESSION_COOKIE, session.secret, { + path: "/", + expires: new Date(session.expire), + sameSite: "strict", + secure: true, + httpOnly: true, + }); + + return Astro.redirect("/account"); +} +--- + + +
+

Demo sign in

+
+
+
    +
  • +

    + This is a demo project for Appwrite server side rendering. View the source code on the + GitHub repository. +

    +
  • +
  • + +
    + +
    +
  • +
  • + +
    + + +
    +
  • +
  • + +
  • + or +
  • +
+
+
+ +
+
+ +
+
diff --git a/src/pages/signout.astro b/src/pages/signout.astro new file mode 100644 index 0000000..3ab2b47 --- /dev/null +++ b/src/pages/signout.astro @@ -0,0 +1,6 @@ +--- +Astro.cookies.delete('session-token') +Astro.cookies.delete('anonymous') +--- + + \ No newline at end of file diff --git a/src/pages/signup.astro b/src/pages/signup.astro new file mode 100644 index 0000000..4d2fe7c --- /dev/null +++ b/src/pages/signup.astro @@ -0,0 +1,88 @@ +--- +import { ID } from "node-appwrite"; +import { SESSION_COOKIE, createAdminClient } from "../server/appwrite"; + +const { user } = Astro.locals; +if (user) { + return Astro.redirect("/account"); +} + +if (Astro.request.method === "POST") { + const data = await Astro.request.formData(); + + const email = data.get("email") as string; + const password = data.get("password") as string; + const name = data.get("name") as string; + + const { account } = createAdminClient(); + + await account.create(ID.unique(), email, password, name); + const session = await account.createEmailPasswordSession(email, password); + const promise = account.createVerification("http://localhost:4321/verify"); + + promise.then( + function (response) { + console.log('VERI:' + response); + }, + function (error) { + console.log(error); + }, + ); + + Astro.cookies.set(SESSION_COOKIE, session.secret, { + path: "/", + expires: new Date(session.expire), + sameSite: "strict", + secure: true, + httpOnly: true, + }); + + return Astro.redirect("/account"); +} +--- + +
+ + + + + + + + + + + +
+ + + + +
+ + +
+ +Create Anonymous Session \ No newline at end of file diff --git a/src/server/appwrite.ts b/src/server/appwrite.ts new file mode 100644 index 0000000..2f9385c --- /dev/null +++ b/src/server/appwrite.ts @@ -0,0 +1,45 @@ +import { Client, Account } from "node-appwrite"; + +export const SESSION_COOKIE = "session-token"; + +export function createAdminClient() { + const client = new Client() + .setEndpoint(import.meta.env.PUBLIC_APPWRITE_ENDPOINT) + .setProject(import.meta.env.PUBLIC_APPWRITE_PROJECT_ID) + .setKey(import.meta.env.APPWRITE_KEY); + + return { + get account() { + return new Account(client); + }, + }; +} + +export function createSessionClient(request: Request) { + const client = new Client() + .setEndpoint(import.meta.env.PUBLIC_APPWRITE_ENDPOINT) + .setProject(import.meta.env.PUBLIC_APPWRITE_PROJECT_ID); + + const cookies = parseCookies(request.headers.get("cookie") ?? ""); + const session = cookies.get(SESSION_COOKIE); + if (!session) { + throw new Error("No session"); + } + + client.setSession(session); + + return { + get account() { + return new Account(client); + }, + }; +} + +function parseCookies(cookies: string): Map { + const map = new Map(); + for (const cookie of cookies.split(";")) { + const [name, value] = cookie.split("="); + map.set(name.trim(), value ?? null); + } + return map; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bcbf8b5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/strict" +}