lok-onion
v1.0.3
Published
A simple webframework for onion sites.
Readme
Løk
A simple webframework for onion sites.
Installation
npm install lok-onionExample
/* @jsxImportSource lokjs */
//
// This line sets the jsx import source so the typescript compiler
// knows to use the custom jsx functions from lokjs instead of React.
//
// You can also set the "jsxImportSource" field in your
// tsconfig.json to "lokjs" instead.
import { Lok, Button, UserSessionBase, redirect} from "lokjs";
// Stored for every user on the client as a cookie.
// Note that users can choose to reset their state whenever they want.
// The user session interface should extend UserSessionBase, and should be
// JSON serializable.
interface UserSession extends UserSessionBase {
count: number;
}
const lok = Lok.initialize<UserSession>(
// A secret used for signing data.
"my-secret-key",
// A function that creates a new session, will be called for new sessions
// or when a user resets their session.
() => ({ count: 0 }),
);
// An SSF (Server-Side Function) is used to run javascript on the server
// whenever a user interacts with the page. It can be used to modify the
// user's session, or run code that you don't want to expose to the client.
//
// Since løk is designed for onion sites, no javascript is ran on the client
// by default. Meaning you have to run session modification on the server.
//
// An SSF has access to:
// - The user's session, through `userSession`, of type `UserSession` in this example
// - Any field values passed by the client, through `userArguments`.
// This is an object that can contain any fields and values, so make sure to
// validate and sanitize it before using it.
// - The express response object, through `response`, which can be used to set
// cookies or send responses directly. You won't need to use this in most cases.
// - Any server arguments passed when calling the SSF, through `serverArguments`.
// This object can contain any JSON serializable value, clients can see the value
// but can't modify it. You can trust this value.
const addToCountSsf = lok.createSsf<number>(async ({ userSession, serverArguments }) => {
userSession.count += serverArguments;
// After modifying the session, we redirect the user back to the index page.
return redirect(
// The url to redirect to.
"/",
// True implies the modifications we made to the userSession will be saved.
// If you error halfway through modifying the session, you can pass false here
// instead to revert the session to its previous state.
true,
// An optional message that will be stored in the session and can be displayed to the user.
// This will always be stored, even if you pass false as the second argument.
{ type: "info", text: `Added ${serverArguments} to count` }
);
});
const setCountSsf = lok.createSsf<{count: number, message: string}, undefined>(async ({ userSession, serverArguments }) => {
userSession.count = serverArguments.count;
return redirect("/", true, { type: "info", text: serverArguments.message });
});
// This SSF contains a bit more logic, to validate the user input.
const userSetCountSsf = lok.createSsf(async ({ userSession, userArguments }) => {
if (userArguments.value === undefined) {
return redirect("/", false, { type: "error", text: "Value is required" });
}
const value = parseInt(userArguments.value, 10);
if (isNaN(value)) {
return redirect("/", false, { type: "error", text: "Value must be a number" });
}
userSession.count = value;
return redirect("/", true, { type: "info", text: `Count set to ${value}` });
});
// This is the component that will be rendered for the "/" route.
// Each page receives the user's session as a property.
const HomePage = ({ userSession }: { userSession: UserSession }) => {
return (<>
<style>
{`
html {
font-family: Arial, sans-serif;
}
.popup {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 5px;
font-weight: bold;
z-index: 1000;
}
.fadeout {
animation: fadeout 3s forwards;
}
@keyframes fadeout {
0% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
}
.info {
background-color: lightblue;
color: white;
}
.error {
background-color: lightcoral;
color: white;
}
`}
</style>
{/* Show a popup if there's a message in the user session */}
{userSession.message && (
<p class={`popup fadeout ${userSession.message.type}`}>{userSession.message.type.toUpperCase()}: {userSession.message.text}</p>
)}
<h1>Welcome to løk!</h1>
<p>Count: {userSession.count}</p>
{/* onClickSsf specifies which SSF to call when the button is clicked */}
{/* The ssfArguments property will be passed to the SSF as the
serverArguments parameter, use undefined if you don't want to pass anything. */}
<Button onClickSsf={addToCountSsf} ssfArguments={1}>+</Button>
<Button onClickSsf={addToCountSsf} ssfArguments={-1}>-</Button>
<input type="text" name="value" placeholder="Set count" />
<Button onClickSsf={userSetCountSsf} ssfArguments={undefined}>Set</Button>
<Button onClickSsf={setCountSsf} ssfArguments={{ message: "Count was reset", count: 0 }} disabled={userSession.count === 0}>Reset</Button>
<Button onClickSsf={setCountSsf} ssfArguments={{ message: "Doubled!", count: userSession.count * 2 }} disabled={userSession.count === 0}>Double</Button>
</>);
};
// Using the `route` method, we can specify which component should be rendered for which route.
// Note that components won't generate at the top level. Each component is wrapped in a form.
lok.route("/", HomePage);
// Using the `listen` method, we can start the server and listen for incoming requests.
const PORT = 3000;
lok.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});