This commit is contained in:
Korbs 2024-07-16 02:05:26 -04:00
commit ea1438363b
22 changed files with 529 additions and 0 deletions

3
.env.sample Normal file
View file

@ -0,0 +1,3 @@
PUBLIC_APPWRITE_ENDPOINT="https://example.com/v1"
PUBLIC_APPWRITE_PROJECT_ID=""
APPWRITE_KEY=""

20
.gitignore vendored Normal file
View file

@ -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

12
astro.config.mjs Normal file
View file

@ -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
}
});

BIN
bun.lockb Executable file

Binary file not shown.

22
package.json Normal file
View file

@ -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"
}
}

18
src/env.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
user?: import("node-appwrite").Models.User <import("node-appwrite").Models.Preferences<{}>>;
users?: import("node-appwrite").Service.UserList <import("node-appwrite").Service.getPrefs<{}>>;
}
}
interface ImportMetaEnv {
readonly PUBLIC_APPWRITE_ENDPOINT: string;
readonly PUBLIC_APPWRITE_PROJECT_ID: string;
readonly APPWRITE_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View file

@ -0,0 +1,13 @@
<svg width="160" height="29" viewBox="0 0 160 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M46.7348 23.5568C49.3517 23.5568 50.6746 22.2116 51.2497 21.2957H51.5085C51.6236 22.2688 52.3137 23.1847 53.6653 23.1847H56.2247V20.3226H55.5633C55.1032 20.3226 54.8731 20.0651 54.8731 19.6644V8.18757H51.4798V10.0193H51.221C50.5595 9.10343 49.1792 7.8155 46.6486 7.8155C42.6226 7.8155 39.6318 11.1355 39.6318 15.6861C39.6318 20.2368 42.6801 23.5568 46.7348 23.5568ZM47.3387 20.294C44.9519 20.294 43.0827 18.5482 43.0827 15.7148C43.0827 12.9386 44.8944 11.0496 47.31 11.0496C49.6106 11.0496 51.5373 12.7382 51.5373 15.7148C51.5373 18.262 49.8981 20.294 47.3387 20.294Z" fill="#EDEDF0"/>
<path d="M58.2281 29H61.6215V21.2957H61.8803C62.513 22.2116 63.8645 23.5568 66.539 23.5568C70.565 23.5568 73.4982 20.1796 73.4982 15.6861C73.4982 11.1641 70.3637 7.8155 66.3089 7.8155C63.7208 7.8155 62.4554 9.21791 61.8515 9.99066H61.5927V8.18757H58.2281V29ZM65.82 20.3799C63.4907 20.3799 61.564 18.6627 61.564 15.6861C61.564 13.1389 63.2031 10.9924 65.7625 10.9924C68.1494 10.9924 70.0186 12.8527 70.0186 15.6861C70.0186 18.4623 68.2069 20.3799 65.82 20.3799Z" fill="#EDEDF0"/>
<path d="M75.2492 29H78.6425V21.2957H78.9013C79.534 22.2116 80.8856 23.5568 83.56 23.5568C87.586 23.5568 90.2396 20.1796 90.2396 15.6861C90.2396 11.1641 87.3847 7.8155 83.3299 7.8155C80.7418 7.8155 79.4765 9.21791 78.8726 9.99066H78.6137V8.18757H75.2492V29ZM82.841 20.3799C80.5117 20.3799 78.585 18.6627 78.585 15.6861C78.585 13.1389 80.2242 10.9924 82.7835 10.9924C85.1704 10.9924 87.0396 12.8527 87.0396 15.6861C87.0396 18.4623 85.2279 20.3799 82.841 20.3799Z" fill="#EDEDF0"/>
<path d="M94.7253 23.5329H99.5277L102.26 11.7699H102.432L105.164 23.5329H109.938L113.76 8.53582H110.34L107.608 20.3275H107.35L104.618 8.53582H100.103L97.3422 20.3275H97.0834L94.3802 8.53582H90.7568L94.7253 23.5329Z" fill="#EDEDF0"/>
<path d="M115.48 23.5329H118.873V16.1202C118.873 13.2868 120.196 11.541 122.669 11.541H124.164V8.16376H123.043C121.116 8.16376 119.649 9.4803 119.074 10.7396H118.844V8.53582H115.48V23.5329Z" fill="#EDEDF0"/>
<path d="M141.004 23.5329H143.649V20.5278H141.032C139.997 20.5278 139.566 20.0699 139.566 19.0109V11.5124H143.822V8.53582H139.566V4.32861H136.345V8.53582H133.527V11.5124H136.144V19.0395C136.144 22.2164 138.07 23.5329 141.004 23.5329Z" fill="#EDEDF0"/>
<path d="M152.753 23.5568C155.888 23.5568 158.648 22.0113 159.626 18.8916L156.52 18.1475C155.974 19.8075 154.392 20.6661 152.724 20.6661C150.251 20.6661 148.612 19.0634 148.583 16.5447H160V15.6003C160 11.1355 157.21 7.8155 152.609 7.8155C148.555 7.8155 145.075 10.9924 145.075 15.7148C145.075 20.294 148.152 23.5568 152.753 23.5568ZM148.612 14.0834C148.813 12.2803 150.452 10.7634 152.609 10.7634C154.68 10.7634 156.376 12.0513 156.549 14.0834H148.612Z" fill="#EDEDF0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M132.018 23.5329H128.625V11.5124H125.979V8.53582H132.018V23.5329Z" fill="#EDEDF0"/>
<path d="M130.069 6.45465C131.306 6.45465 132.226 5.53879 132.226 4.33673C132.226 3.16329 131.306 2.24744 130.069 2.24744C128.833 2.24744 127.912 3.16329 127.912 4.33673C127.912 5.53879 128.833 6.45465 130.069 6.45465Z" fill="#EDEDF0"/>
<path d="M29.6277 19.8556V26.4741H13.0326C8.19779 26.4741 3.97629 23.8122 1.71769 19.8556C1.38936 19.2803 1.10198 18.6769 0.860926 18.0505C0.387713 16.8231 0.0902487 15.506 0 14.1317V12.3423C0.0195935 12.0361 0.050468 11.7322 0.0908425 11.432C0.173373 10.8159 0.298058 10.213 0.461931 9.62693C2.01219 4.07098 7.05306 0 13.0326 0C19.0122 0 24.0525 4.07098 25.6027 9.62693H18.5069C17.342 7.81586 15.3257 6.61852 13.0326 6.61852C10.7396 6.61852 8.72325 7.81586 7.55833 9.62693C7.20328 10.1775 6.92778 10.7846 6.74728 11.432C6.58697 12.006 6.50147 12.6113 6.50147 13.237C6.50147 15.1341 7.28877 16.8441 8.55107 18.0505C9.72074 19.1702 11.2977 19.8556 13.0326 19.8556H29.6277Z" fill="#FD366E"/>
<path d="M29.6277 11.432V18.0505H17.5142C18.7765 16.8442 19.5638 15.1342 19.5638 13.2371C19.5638 12.6113 19.4783 12.006 19.3179 11.432H29.6277Z" fill="#FD366E"/>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

1
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1 @@
<slot />

11
src/middleware.ts Normal file
View file

@ -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();
});

16
src/oauth.ts Normal file
View file

@ -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();
}

45
src/pages/account.astro Normal file
View file

@ -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);
---
<p>Email: {AN ? <span>None</span> : <span>{user.email}</span>} <span><button><a style="color: white; text-decoration: none;" href="/settings/update-username">Change</a></button></span></p>
<p>ID: {user.$id}</p>
<!-- <p>Phone: {user.phone}</p> -->
<p>Name: {AN ? <span>Anonymous</span> : <span>{user.name}</span>} <span><button><a style="color: white; text-decoration: none;" href="/settings/update-username">Change</a></button></span></p>
<p>Created at: {user.$createdAt}</p>
<p>Last login: {user.accessedAt}</p>
<p>Last updated: {user.$updatedAt}</p>
<p>Labels:
{user.labels.map((data) =>
<ul>
<li>{data}</li>
</ul>
)}
</p>
Theme: {user.prefs.darkTheme}
{EmailIsVerified ?
<p>Your account is verified</p>
:
<p style="color: rgb(255, 113, 113)">Your account is not verified<!-- [<a href="/api/email/send-verification">Send Verification Email</a>] --></p>
}
{AN ?
<p>This is an anonmyous account.</p>
:
<p style="color: rgb(255, 113, 113)">Not an anonmyous account.</p>
}
<a href="/signout">Sign Out</a>

24
src/pages/api/an.ts Normal file
View file

@ -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");
};

View file

@ -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");
};

View file

@ -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");
};

8
src/pages/index.astro Normal file
View file

@ -0,0 +1,8 @@
---
const { user } = Astro.locals;
if (user) {
return Astro.redirect("/account");
}
return Astro.redirect("/signin");
---

View file

@ -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");
}
---
<form method="POST">
<label for="name">New Name</label>
<input
id="name"
name="name"
placeholder="Your name"
autocomplete="off"
type="text"
/>
<button type="submit">Update Name</button>
</form>

105
src/pages/signin.astro Normal file
View file

@ -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");
}
---
<Layout title="Sign in | Server side rendering with Appwrite and Astro">
<div class="u-max-width-500 u-width-full-line">
<h1 class="heading-level-2 u-margin-block-start-auto">Demo sign in</h1>
<div class="u-margin-block-start-24">
<form class="form common-section" method="POST">
<ul class="form-list" style="--form-list-gap: 1.5rem;">
<li class="form-item">
<p>
This is a demo project for <a href="https://appwrite.io"
>Appwrite</a
> server side rendering. View the source code on the
<a
class="link"
href="https://github.com/appwrite/demos-for-svelte"
>GitHub repository</a
>.
</p>
</li>
<li class="form-item">
<label class="label is-required" for="email">Email</label>
<div class="input-text-wrapper">
<input
id="email"
name="email"
placeholder="Email"
type="email"
class="input-text"
autocomplete="off"
/>
</div>
</li>
<li class="form-item">
<label class="label is-required" for="password">Password</label>
<div class="input-text-wrapper" style="--amount-of-buttons: 1">
<input
id="password"
name="password"
placeholder="Password"
minlength="8"
type="password"
class="input-text"
autocomplete="off"
/>
<button
type="button"
class="show-password-button"
aria-label="show password"
><span aria-hidden="true" class="icon-eye"></span></button
>
</div>
</li>
<li class="form-item">
<button class="button is-full-width" type="submit"> Sign in</button>
</li>
<span class="with-separators eyebrow-heading-3">or</span>
<li class="form-item"></li>
</ul>
</form>
<form method="POST" action="/api/oauth/discord">
<button class="button is-discord is-full-width" type="submit">
<span class="icon-discord" aria-hidden="true"></span>
<span class="text">Sign in with discord</span></button
>
</form>
</div>
<ul class="inline-links is-center is-with-sep u-margin-block-start-32">
<li class="inline-links-item">
<span class="text"
>Don't have an account? <a class="link" href="/signup">Sign up</a
></span
>
</li>
</ul>
</div>
</Layout>

6
src/pages/signout.astro Normal file
View file

@ -0,0 +1,6 @@
---
Astro.cookies.delete('session-token')
Astro.cookies.delete('anonymous')
---
<script is:inline>location.href = '/'</script>

88
src/pages/signup.astro Normal file
View file

@ -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");
}
---
<form method="POST">
<label for="name">Name</label>
<input
id="name"
name="name"
placeholder="Your name"
autocomplete="off"
type="text"
/>
<label class="label is-required" for="email">Email</label>
<input
id="email"
name="email"
placeholder="Your email"
type="email"
autocomplete="off"
/>
<label for="password">Password</label>
<input
id="password"
name="password"
placeholder="Your password"
minlength="8"
type="password"
autocomplete="off"
/>
<button type="submit">Create Account</button>
<hr/>
<form method="POST" action="/api/oauth">
<button class="button is-discord is-full-width" type="submit">
<span class="icon-discord" aria-hidden="true"></span>
<span class="text">Sign in with discord</span></button
>
</form>
</form>
<br/>
<a href="/api/an">Create Anonymous Session</a>

45
src/server/appwrite.ts Normal file
View file

@ -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<string, string | null> {
const map = new Map<string, string | null>();
for (const cookie of cookies.split(";")) {
const [name, value] = cookie.split("=");
map.set(name.trim(), value ?? null);
}
return map;
}

3
tsconfig.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}