Skip to content

Commit

Permalink
Add redis support to nextjs fullstack example
Browse files Browse the repository at this point in the history
  • Loading branch information
steos committed Jul 20, 2023
1 parent ad9824f commit 255ca84
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 96 deletions.
9 changes: 9 additions & 0 deletions examples/fullstack-simple-nextjs/app/api/Datastore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface Datastore {
setUser(name: string, value: string): Promise<void>;
getUser(name: string): Promise<string | null>;
hasUser(name: string): Promise<boolean>;
getLogin(name: string): Promise<string | null>;
setLogin(name: string, value: string): Promise<void>;
hasLogin(name: string): Promise<boolean>;
removeLogin(name: string): Promise<void>;
}
78 changes: 78 additions & 0 deletions examples/fullstack-simple-nextjs/app/api/InMemoryStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { readFile, writeFile } from "fs/promises";
import { Datastore } from "./Datastore";

type LoginState = { value: string; timestamp: number };

export default class InMemoryStore implements Datastore {
constructor(
private users: Record<string, string> = {},
private logins: Record<string, LoginState> = {},
private listeners: (() => Promise<void>)[] = []
) {}
addListener(listener: () => Promise<void>) {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index !== -1) {
this.listeners.splice(index, 1);
}
};
}
_notifyListeners() {
return Promise.all(this.listeners.map((f) => f()));
}
static empty() {
return new InMemoryStore({}, {});
}
stringify() {
return JSON.stringify(
{
logins: this.logins,
users: this.users,
},
null,
2
);
}
async getUser(name: string) {
return this.users[name];
}
async hasUser(name: string) {
return this.users[name] != null;
}
async getLogin(name: string) {
const hasLogin = await this.hasLogin(name);
return hasLogin ? this.logins[name].value : null;
}
async hasLogin(name: string) {
const login = this.logins[name];
if (login == null) return false;
const now = new Date().getTime();
const elapsed = now - login.timestamp;
return elapsed < 2000;
}
async setUser(name: string, value: string) {
this.users[name] = value;
await this._notifyListeners();
}
async setLogin(name: string, value: string) {
this.logins[name] = { value, timestamp: new Date().getTime() };
await this._notifyListeners();
}
async removeLogin(name: string) {
delete this.logins[name];
await this._notifyListeners();
}
}

export async function readDatabaseFile(filePath: string) {
const json = await readFile(filePath, "utf-8");
const data = JSON.parse(json);
const db = new InMemoryStore(data.serverSetup, data.users, data.logins);
return db;
}

export function writeDatabaseFile(filePath: string, db: InMemoryStore) {
const data = db.stringify();
return writeFile(filePath, data);
}
47 changes: 47 additions & 0 deletions examples/fullstack-simple-nextjs/app/api/RedisStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as redis from "redis";
import { Datastore } from "./Datastore";

export default class RedisStore implements Datastore {
private readonly client: ReturnType<typeof redis.createClient>;
constructor(url: string) {
this.client = redis.createClient({ url });
}

onError(handler: (err: unknown) => void) {
this.client.on("error", handler);
}

connect() {
return this.client.connect();
}

async getUser(name: string) {
return this.client.get(`user:${name}`);
}

async hasUser(name: string) {
const user = await this.getUser(name);
return user != null;
}

getLogin(name: string) {
return this.client.get(`login:${name}`);
}

async hasLogin(name: string) {
const login = await this.getLogin(name);
return login != null;
}

async setUser(name: string, value: string) {
await this.client.set(`user:${name}`, value);
}

async setLogin(name: string, value: string) {
await this.client.set(`login:${name}`, value, { EX: 2 });
}

async removeLogin(name: string) {
await this.client.del(`login:${name}`);
}
}
126 changes: 37 additions & 89 deletions examples/fullstack-simple-nextjs/app/api/db.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,14 @@
import * as opaque from "@serenity-kit/opaque";
import { readFile, writeFile } from "fs/promises";
import { Datastore } from "./Datastore";
import InMemoryStore, {
readDatabaseFile,
writeDatabaseFile,
} from "./InMemoryStore";
import RedisStore from "./RedisStore";
import { REDIS_URL, ENABLE_REDIS } from "./env";

type LoginState = { value: string; timestamp: number };

class Database {
constructor(
private serverSetup: string,
private users: Record<string, string> = {},
private logins: Record<string, LoginState> = {},
private listeners: (() => Promise<void>)[] = []
) {}
addListener(listener: () => Promise<void>) {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index !== -1) {
this.listeners.splice(index, 1);
}
};
}
_notifyListeners() {
return Promise.all(this.listeners.map((f) => f()));
}
static empty(serverSetup: string) {
return new Database(serverSetup, {}, {});
}
stringify() {
return JSON.stringify(
{
serverSetup: this.serverSetup,
logins: this.logins,
users: this.users,
},
null,
2
);
}
getUser(name: string) {
return this.users[name];
}
hasUser(name: string) {
return this.users[name] != null;
}
getLogin(name: string) {
return this.hasLogin(name) ? this.logins[name].value : null;
}
hasLogin(name: string) {
const login = this.logins[name];
if (login == null) return null;
const now = new Date().getTime();
const elapsed = now - login.timestamp;
return elapsed < 2000;
}
async setUser(name: string, value: string) {
this.users[name] = value;
await this._notifyListeners();
}
async setLogin(name: string, value: string) {
this.logins[name] = { value, timestamp: new Date().getTime() };
await this._notifyListeners();
}
async removeLogin(name: string) {
delete this.logins[name];
await this._notifyListeners();
}
getServerSetup() {
return this.serverSetup;
}
}

async function readDatabaseFile(filePath: string) {
const json = await readFile(filePath, "utf-8");
const data = JSON.parse(json);
const db = new Database(data.serverSetup, data.users, data.logins);
return db;
}

function writeDatabaseFile(filePath: string, db: Database) {
const data = db.stringify();
return writeFile(filePath, data);
}

const SERVER_SETUP = process.env.OPAQUE_SERVER_SETUP;
if (!SERVER_SETUP) {
throw new Error("OPAQUE_SERVER_SETUP env variable value is missing");
}

const db = opaque.ready.then(async () => {
console.log("initializing db");
async function setupInMemoryStore(): Promise<Datastore> {
console.log("initializing InMemoryStore");
const file = "data.json";
const db = await readDatabaseFile(file).catch((err) => {
if ("code" in err && err.code == "ENOENT") {
Expand All @@ -98,10 +19,37 @@ const db = opaque.ready.then(async () => {
);
console.error(err);
}
return Database.empty(SERVER_SETUP);
return InMemoryStore.empty();
});
db.addListener(() => writeDatabaseFile(file, db));
return db;
}

async function setupRedis(): Promise<Datastore> {
try {
const redis = new RedisStore(REDIS_URL);
redis.onError((err) => {
console.error("Redis Error:", err instanceof Error ? err.message : err);
process.exit(1);
});
await redis.connect();
console.log("connected to redis at", REDIS_URL);
return redis;
} catch (err) {
console.error(
"Redis Setup Error:",
err instanceof Error ? err.message : err
);
process.exit(1);
}
}

const db = opaque.ready.then(() => {
if (ENABLE_REDIS) {
return setupRedis();
} else {
return setupInMemoryStore();
}
});

export default db;
17 changes: 17 additions & 0 deletions examples/fullstack-simple-nextjs/app/api/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function requireEnv(key: string) {
const value = process.env[key];
if (value == null) throw new Error(`missing value for env variable "${key}"`);
return value;
}

export function hasEnv(key: string) {
return process.env[key] != null;
}

export function getEnv(key: string, defaultValue: string) {
return process.env[key] ?? defaultValue;
}

export const SERVER_SETUP = requireEnv("OPAQUE_SERVER_SETUP");
export const ENABLE_REDIS = hasEnv("ENABLE_REDIS");
export const REDIS_URL = getEnv("REDIS_URL", "redis://127.0.0.1:6379");
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
);

const db = await database;
const serverLoginState = userIdentifier && db.getLogin(userIdentifier);
const serverLoginState = await db.getLogin(userIdentifier);

if (!serverLoginState)
return NextResponse.json({ error: "login not started" }, { status: 400 });
Expand Down
8 changes: 5 additions & 3 deletions examples/fullstack-simple-nextjs/app/api/login/start/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as opaque from "@serenity-kit/opaque";
import { NextRequest, NextResponse } from "next/server";
import database from "../../db";
import { SERVER_SETUP } from "../../env";

export async function POST(request: NextRequest) {
const { userIdentifier, startLoginRequest } = await request.json();
Expand All @@ -17,19 +18,20 @@ export async function POST(request: NextRequest) {
);

const db = await database;
const registrationRecord = userIdentifier && db.getUser(userIdentifier);
const registrationRecord = await db.getUser(userIdentifier);

if (!registrationRecord)
return NextResponse.json({ error: "user not registered" }, { status: 400 });

if (db.hasLogin(userIdentifier))
const hasLogin = await db.hasLogin(userIdentifier);
if (hasLogin)
return NextResponse.json(
{ error: "login already started" },
{ status: 400 }
);

const { serverLoginState, loginResponse } = opaque.server.startLogin({
serverSetup: db.getServerSetup(),
serverSetup: SERVER_SETUP,
userIdentifier,
registrationRecord,
startLoginRequest,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as opaque from "@serenity-kit/opaque";
import { NextRequest, NextResponse } from "next/server";
import database from "../../db";
import { SERVER_SETUP } from "../../env";

export async function POST(req: NextRequest) {
const { userIdentifier, registrationRequest } = await req.json();
Expand All @@ -17,15 +18,15 @@ export async function POST(req: NextRequest) {
);

const db = await database;

if (db.hasUser(userIdentifier))
const hasUser = await db.hasUser(userIdentifier);
if (hasUser)
return NextResponse.json(
{ error: "user already registered" },
{ status: 400 }
);

const { registrationResponse } = opaque.server.createRegistrationResponse({
serverSetup: db.getServerSetup(),
serverSetup: SERVER_SETUP,
userIdentifier,
registrationRequest,
});
Expand Down
1 change: 1 addition & 0 deletions examples/fullstack-simple-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"postcss": "8.4.24",
"react": "18.2.0",
"react-dom": "18.2.0",
"redis": "^4.6.7",
"tailwindcss": "3.3.2",
"typescript": "5.1.3"
}
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 255ca84

Please sign in to comment.