-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
15f0c28
commit 606427c
Showing
11 changed files
with
327 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
# dependencies | ||
/node_modules | ||
**/node_modules | ||
/.pnp | ||
.pnp.js | ||
.yarn/install-state.gz | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
"use server"; | ||
|
||
import type { AdminSession } from "@/type/AdminSession"; | ||
import { type SessionOptions, getIronSession } from "iron-session"; | ||
import { cookies } from "next/headers"; | ||
|
||
/** | ||
* Options used to store the session in the cookies | ||
*/ | ||
const sessionOptions: SessionOptions = { | ||
password: process.env.SESSION_ENCRYPTION_KEY ?? "", | ||
cookieName: "admin-session", | ||
ttl: 60 * 60 * 24, // 1 day | ||
cookieOptions: { | ||
secure: true, | ||
}, | ||
}; | ||
|
||
/** | ||
* Get the full session from the cookies | ||
*/ | ||
async function getFullSession() { | ||
return await getIronSession<AdminSession>(cookies(), sessionOptions); | ||
} | ||
|
||
/** | ||
* Check if the current user is admin | ||
* - Admin role is used to add articles | ||
*/ | ||
export async function isAdmin() { | ||
const currentSession = await getFullSession(); | ||
return currentSession.isAdmin ?? false; | ||
} | ||
|
||
/** | ||
* Try to login as an admin | ||
* @param password | ||
*/ | ||
export async function login(password: string) { | ||
if (password !== process.env.ADMIN_PASSWORD) { | ||
throw new Error("Invalid password"); | ||
} | ||
|
||
const session = await getFullSession(); | ||
session.isAdmin = true; | ||
await session.save(); | ||
} | ||
|
||
/** | ||
* Delete the current session | ||
*/ | ||
export async function deleteSession() { | ||
const session = await getFullSession(); | ||
session.destroy(); | ||
} |
14 changes: 14 additions & 0 deletions
14
packages/example/src/context/article/dto/ArticleDocument.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import type { Hex } from "viem"; | ||
|
||
export type ArticleDocument = Readonly<{ | ||
// The id of the article (an hex string representing a byte32 hash) | ||
_id: Hex; | ||
// The content id linked to this article | ||
contentId: Hex; | ||
// The title of the article | ||
title: string; | ||
// The description of the article | ||
description?: string; | ||
// The link to access this article | ||
link: string; | ||
}>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import type { Hex } from "viem"; | ||
|
||
export type ContentDocument = Readonly<{ | ||
// The id of the content (an hex string, representing a bigint) | ||
_id: Hex; | ||
// The title of the content | ||
title: string; | ||
}>; |
52 changes: 52 additions & 0 deletions
52
packages/example/src/context/article/repository/ArticleRepository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import type { ArticleDocument } from "@/context/article/dto/ArticleDocument"; | ||
import { DI } from "@/context/common/di"; | ||
import { getMongoDb } from "@/context/common/mongoDb"; | ||
import type { Collection, WithId } from "mongodb"; | ||
import type { Hex } from "viem"; | ||
|
||
/** | ||
* Repository used to access the article collection | ||
*/ | ||
export class ArticleRepository { | ||
constructor(private readonly collection: Collection<ArticleDocument>) {} | ||
|
||
/** | ||
* Get an article by its id | ||
* @param id | ||
*/ | ||
public async getById( | ||
id: Hex | ||
): Promise<WithId<ArticleDocument> | undefined> { | ||
return this.collection.findOne({ _id: id }); | ||
} | ||
|
||
/** | ||
* Get multiple article by their ids | ||
* @param ids | ||
*/ | ||
public async getByIds(ids: Hex[]): Promise<WithId<ArticleDocument>[]> { | ||
return this.collection.find({ _id: { $in: ids } }).toArray(); | ||
} | ||
|
||
/** | ||
* Create a new article | ||
*/ | ||
public async create(article: ArticleDocument) { | ||
const insertResult = await this.collection.insertOne(article); | ||
if (!insertResult.acknowledged) { | ||
throw new Error("Failed to insert test content"); | ||
} | ||
return insertResult.insertedId; | ||
} | ||
} | ||
|
||
export const getArticleRepository = DI.registerAndExposeGetter({ | ||
id: "ArticleRepository", | ||
isAsync: true, | ||
getter: async () => { | ||
const db = await getMongoDb(); | ||
return new ArticleRepository( | ||
db.collection<ArticleDocument>("articles") | ||
); | ||
}, | ||
}); |
35 changes: 35 additions & 0 deletions
35
packages/example/src/context/article/repository/ContentRepository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import type { ContentDocument } from "@/context/article/dto/ContentDocument"; | ||
import { DI } from "@/context/common/di"; | ||
import { getMongoDb } from "@/context/common/mongoDb"; | ||
import type { Collection } from "mongodb"; | ||
import type { Hex } from "viem"; | ||
|
||
/** | ||
* Repository used to access the content collection | ||
*/ | ||
export class ContentRepository { | ||
constructor(private readonly collection: Collection<ContentDocument>) {} | ||
|
||
/** | ||
* Get a content by its id | ||
* @param id | ||
*/ | ||
public async getById(id: Hex) { | ||
const content = await this.collection.findOne({ _id: id }); | ||
if (!content) { | ||
throw new Error(`Content ${id} not found`); | ||
} | ||
return content; | ||
} | ||
} | ||
|
||
export const getContentRepository = DI.registerAndExposeGetter({ | ||
id: "ContentRepository", | ||
isAsync: true, | ||
getter: async () => { | ||
const db = await getMongoDb(); | ||
return new ContentRepository( | ||
db.collection<ContentDocument>("contents") | ||
); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
type ServiceIdentifier = string; | ||
type ServiceGetter<T> = () => T | Promise<T>; | ||
type ServiceEntry<T> = { | ||
getter: ServiceGetter<T>; | ||
instance?: T; | ||
}; | ||
|
||
type ConditionalServiceGetter< | ||
Return, | ||
Getter extends ServiceGetter<Return> = ServiceGetter<Return>, | ||
> = ReturnType<Getter> extends Promise<Return> | ||
? () => Promise<Return> | ||
: () => Return; | ||
|
||
/** | ||
* Our service register | ||
*/ | ||
const registry = new Map<ServiceIdentifier, ServiceEntry<unknown>>(); | ||
|
||
/** | ||
* Function to register a new service | ||
*/ | ||
const register = <T>(id: ServiceIdentifier, getter: ServiceGetter<T>) => { | ||
if (registry.has(id)) { | ||
throw new Error(`Service ${id} already registered.`); | ||
} | ||
|
||
registry.set(id, { getter }); | ||
}; | ||
|
||
/** | ||
* Register a new dependency in our DI Container and return a direct getter | ||
* @param p | ||
*/ | ||
function registerAndExposeGetter<ValueType>(p: { | ||
id: ServiceIdentifier; | ||
getter: () => ValueType; | ||
isAsync?: false; | ||
}): () => ValueType; | ||
|
||
/** | ||
* Register a new dependency in our DI Container and return an async getter | ||
* @param p | ||
*/ | ||
function registerAndExposeGetter<ValueType>(p: { | ||
id: ServiceIdentifier; | ||
getter: () => Promise<ValueType>; | ||
isAsync: true; | ||
}): () => Promise<ValueType>; | ||
|
||
/** | ||
* Register a new dependency in our DI Container | ||
* @param id | ||
* @param getter | ||
* @param isAsync | ||
*/ | ||
function registerAndExposeGetter< | ||
ValueType, | ||
BuilderType extends ServiceGetter<ValueType> = ServiceGetter<ValueType>, | ||
GetterType extends ConditionalServiceGetter< | ||
ValueType, | ||
BuilderType | ||
> = ConditionalServiceGetter<ValueType, BuilderType>, | ||
>({ | ||
id, | ||
getter, | ||
isAsync, | ||
}: { | ||
id: ServiceIdentifier; | ||
getter: ServiceGetter<ValueType>; | ||
isAsync?: boolean; | ||
}): ConditionalServiceGetter<ValueType, BuilderType> { | ||
register(id, getter); | ||
return ( | ||
isAsync ? () => getAsync<ValueType>(id) : () => get<ValueType>(id) | ||
) as GetterType; | ||
} | ||
|
||
/** | ||
* Get a dependency from our DI Container | ||
* @param id | ||
*/ | ||
async function getAsync<T>(id: ServiceIdentifier): Promise<T> { | ||
const entry = registry.get(id); | ||
|
||
// If we don't have an instance, and no getter, throw | ||
if (!entry) { | ||
throw new Error("Service not found."); | ||
} | ||
|
||
let { instance } = entry; | ||
|
||
// If we don't have an instance yet, try to build it | ||
if (!instance) { | ||
instance = await entry.getter(); | ||
registry.set(id, { getter: entry.getter, instance }); | ||
} | ||
return instance as T; | ||
} | ||
|
||
/** | ||
* Get a dependency from our DI Container | ||
* @param id | ||
*/ | ||
function get<T>(id: ServiceIdentifier): T { | ||
const entry = registry.get(id); | ||
|
||
// If we don't have an instance, and no getter, throw | ||
if (!entry) { | ||
throw new Error("Service not found."); | ||
} | ||
|
||
let { instance } = entry; | ||
|
||
// If we don't have an instance yet, try to build it | ||
if (!instance) { | ||
instance = entry.getter(); | ||
if (instance instanceof Promise) { | ||
throw new Error( | ||
"Service needs to be built asynchronously. Use getAsync to build and get the service." | ||
); | ||
} | ||
registry.set(id, { getter: entry.getter, instance }); | ||
} | ||
|
||
return instance as T; | ||
} | ||
|
||
/** | ||
* Our DI Container | ||
*/ | ||
export const DI = { | ||
registerAndExposeGetter, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
"use server"; | ||
|
||
import { DI } from "@/context/common/di"; | ||
import { MongoClient } from "mongodb"; | ||
|
||
// Get the mongo db client | ||
export const getMongoDb = DI.registerAndExposeGetter({ | ||
id: "Mongo", | ||
isAsync: true, | ||
getter: async () => { | ||
// Get the mongo client | ||
// TODO: Should use Config.MONGODB_FRAK_POC_URI instead, but next isn't happy about it | ||
const client = new MongoClient( | ||
process.env.MONGODB_FRAK_POC_URI as string | ||
); | ||
// Connect to the database | ||
await client.connect(); | ||
// and then connect to the poc database | ||
return client.db("poc"); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export type AdminSession = { | ||
isAdmin: true; | ||
}; |