zyket 1.2.17 → 1.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/SECURITY.md +40 -0
  2. package/bin/cli.js +282 -202
  3. package/index.js +4 -3
  4. package/package.json +5 -1
  5. package/src/extensions/bullboard/index.js +14 -3
  6. package/src/extensions/interactive-storage/index.js +25 -9
  7. package/src/extensions/interactive-storage/routes/delete-folder.js +13 -1
  8. package/src/extensions/interactive-storage/routes/delete.js +14 -3
  9. package/src/extensions/interactive-storage/routes/download.js +22 -3
  10. package/src/extensions/interactive-storage/routes/info.js +13 -0
  11. package/src/services/auth/index.js +66 -42
  12. package/src/services/express/Express.js +32 -15
  13. package/src/services/express/RequireAdminMiddleware.js +14 -0
  14. package/src/services/express/RequireAuthMiddleware.js +46 -0
  15. package/src/services/express/index.js +7 -5
  16. package/src/services/socketio/AuthGuard.js +33 -0
  17. package/src/services/socketio/SocketIO.js +3 -1
  18. package/src/services/socketio/index.js +2 -1
  19. package/src/services/template-manager/index.js +1 -0
  20. package/src/templates/api-rest/.env.example +24 -0
  21. package/src/templates/api-rest/README.md +50 -0
  22. package/src/templates/api-rest/index.js +18 -0
  23. package/src/templates/api-rest/src/models/Task.js +18 -0
  24. package/src/templates/api-rest/src/routes/tasks/[id].js +42 -0
  25. package/src/templates/api-rest/src/routes/tasks/index.js +26 -0
  26. package/src/templates/api-rest/src/services/auth/auth.js +9 -0
  27. package/src/templates/api-rest/src/services/auth/index.js +23 -0
  28. package/src/templates/realtime-chat/.env.example +26 -0
  29. package/src/templates/realtime-chat/README.md +38 -0
  30. package/src/templates/realtime-chat/frontend/.env.example +3 -0
  31. package/src/templates/realtime-chat/frontend/index.html +12 -0
  32. package/src/templates/realtime-chat/frontend/main.jsx +18 -0
  33. package/src/templates/realtime-chat/frontend/src/hooks/useAuth.jsx +27 -0
  34. package/src/templates/realtime-chat/frontend/src/hooks/useChatSocket.jsx +29 -0
  35. package/src/templates/realtime-chat/frontend/src/middlewares/LoggedMiddleware.jsx +12 -0
  36. package/src/templates/realtime-chat/frontend/src/middlewares/NotLoggedMiddleware.jsx +12 -0
  37. package/src/templates/realtime-chat/frontend/src/store/storeAuth.jsx +11 -0
  38. package/src/templates/realtime-chat/frontend/src/views/AuthView.jsx +70 -0
  39. package/src/templates/realtime-chat/frontend/src/views/ChatView.jsx +69 -0
  40. package/src/templates/realtime-chat/frontend/styles.css +1 -0
  41. package/src/templates/realtime-chat/frontend/vite.config.js +7 -0
  42. package/src/templates/realtime-chat/index.js +14 -0
  43. package/src/templates/realtime-chat/src/guards/auth.js +3 -0
  44. package/src/templates/realtime-chat/src/handlers/connection.js +23 -0
  45. package/src/templates/realtime-chat/src/handlers/message.js +29 -0
  46. package/src/templates/realtime-chat/src/services/auth/auth.js +8 -0
  47. package/src/templates/realtime-chat/src/services/auth/index.js +19 -0
  48. package/src/templates/saas-multitenant/.env.example +22 -0
  49. package/src/templates/saas-multitenant/README.md +71 -0
  50. package/src/templates/saas-multitenant/frontend/.env.example +3 -0
  51. package/src/templates/saas-multitenant/frontend/index.html +12 -0
  52. package/src/templates/saas-multitenant/frontend/main.jsx +18 -0
  53. package/src/templates/saas-multitenant/frontend/src/hooks/useAuth.jsx +27 -0
  54. package/src/templates/saas-multitenant/frontend/src/hooks/useProjects.jsx +41 -0
  55. package/src/templates/saas-multitenant/frontend/src/middlewares/LoggedMiddleware.jsx +12 -0
  56. package/src/templates/saas-multitenant/frontend/src/middlewares/NotLoggedMiddleware.jsx +12 -0
  57. package/src/templates/saas-multitenant/frontend/src/store/storeAuth.jsx +13 -0
  58. package/src/templates/saas-multitenant/frontend/src/views/AuthView.jsx +70 -0
  59. package/src/templates/saas-multitenant/frontend/src/views/DashboardView.jsx +131 -0
  60. package/src/templates/saas-multitenant/frontend/styles.css +1 -0
  61. package/src/templates/saas-multitenant/frontend/vite.config.js +7 -0
  62. package/src/templates/saas-multitenant/index.js +14 -0
  63. package/src/templates/saas-multitenant/src/middlewares/RequireOrganization.js +22 -0
  64. package/src/templates/saas-multitenant/src/models/Project.js +17 -0
  65. package/src/templates/saas-multitenant/src/routes/admin/stats.js +15 -0
  66. package/src/templates/saas-multitenant/src/routes/projects/index.js +34 -0
  67. package/src/templates/saas-multitenant/src/services/auth/auth.js +8 -0
  68. package/src/templates/saas-multitenant/src/services/auth/index.js +43 -0
  69. package/src/utils/EnvManager.js +23 -0
@@ -0,0 +1,50 @@
1
+ # api-rest template
2
+
3
+ Backend-only REST API starter with email/password auth and a protected CRUD
4
+ resource (`Task`). No frontend, no sockets.
5
+
6
+ ## What's included
7
+ - `index.js` — boots the kernel (auth + database + express) and syncs the `Task` table.
8
+ - `src/services/auth/` — auth service (email/password, no organizations).
9
+ - `src/models/Task.js` — example Sequelize model.
10
+ - `src/routes/tasks/` — full CRUD, every endpoint protected with `RequireAuthMiddleware`.
11
+
12
+ ## Endpoints (all require a valid session)
13
+ | Method | Path | Description |
14
+ |--------|------|-------------|
15
+ | GET | `/tasks` | List tasks |
16
+ | POST | `/tasks` | Create a task `{ title, description? }` |
17
+ | GET | `/tasks/:id` | Get one task |
18
+ | PUT | `/tasks/:id` | Update a task |
19
+ | DELETE | `/tasks/:id` | Delete a task |
20
+
21
+ Auth endpoints are mounted by better-auth under `/api/auth/*`.
22
+
23
+ ## Setup
24
+ ```bash
25
+ npm install
26
+ # 1) Create the better-auth tables (uses src/services/auth/auth.js)
27
+ npx @better-auth/cli migrate
28
+ # 2) Run
29
+ node index.js
30
+ ```
31
+
32
+ The `Task` table is created automatically via `sequelize.sync()` on boot.
33
+
34
+ ## Auth quick test
35
+ ```bash
36
+ # Sign up
37
+ curl -X POST http://localhost:3000/api/auth/sign-up/email \
38
+ -H "Content-Type: application/json" \
39
+ -d '{"email":"me@example.com","password":"supersecret","name":"Me"}' -c cookies.txt
40
+
41
+ # Create a task with the session cookie
42
+ curl -X POST http://localhost:3000/tasks \
43
+ -H "Content-Type: application/json" -b cookies.txt \
44
+ -d '{"title":"My first task"}'
45
+ ```
46
+
47
+ ## Notes
48
+ - `RequireAuthMiddleware` attaches `request.user` / `request.session`. Restrict by role with `new RequireAuthMiddleware(['admin'])`.
49
+ - Email sending is stubbed (logged to console) — wire a real provider in `src/services/auth/index.js`.
50
+ - Review [SECURITY.md] in the framework for hardening (Swagger password, CORS, rate limiting).
@@ -0,0 +1,18 @@
1
+ const { Kernel } = require('zyket');
2
+
3
+ const kernel = new Kernel({
4
+ services: [
5
+ // The database and express services are auto-registered by the framework
6
+ // (they activate from your .env). We only register the auth service here.
7
+ ['auth', require('./src/services/auth'), ['@service_container']],
8
+ ],
9
+ });
10
+
11
+ kernel.boot().then(async () => {
12
+ // Create the example tables (Task). better-auth tables are created via the
13
+ // better-auth CLI migrate step (see README).
14
+ await kernel.container.get('database').sync();
15
+ kernel.container.get('logger').info('api-rest template ready — try GET /tasks');
16
+ }).catch((error) => {
17
+ console.error('Error booting kernel:', error);
18
+ });
@@ -0,0 +1,18 @@
1
+ module.exports = ({ sequelize, Sequelize }) => {
2
+ const Task = sequelize.define('Task', {
3
+ title: {
4
+ type: Sequelize.STRING,
5
+ allowNull: false,
6
+ },
7
+ description: {
8
+ type: Sequelize.TEXT,
9
+ allowNull: true,
10
+ },
11
+ completed: {
12
+ type: Sequelize.BOOLEAN,
13
+ defaultValue: false,
14
+ },
15
+ });
16
+
17
+ return Task;
18
+ };
@@ -0,0 +1,42 @@
1
+ const { Route, RequireAuthMiddleware } = require('zyket');
2
+
3
+ // Item endpoints: GET/PUT/DELETE /tasks/:id
4
+ module.exports = class TaskRoute extends Route {
5
+ middlewares = {
6
+ get: [new RequireAuthMiddleware()],
7
+ put: [new RequireAuthMiddleware()],
8
+ delete: [new RequireAuthMiddleware()],
9
+ };
10
+
11
+ async #find(container, id) {
12
+ const { Task } = container.get('database').models;
13
+ return Task.findByPk(id);
14
+ }
15
+
16
+ async get({ container, request }) {
17
+ const task = await this.#find(container, request.params.id);
18
+ if (!task) return { success: false, message: 'Task not found', status: 404 };
19
+ return { task };
20
+ }
21
+
22
+ async put({ container, request }) {
23
+ const task = await this.#find(container, request.params.id);
24
+ if (!task) return { success: false, message: 'Task not found', status: 404 };
25
+
26
+ const { title, completed, description } = request.body || {};
27
+ await task.update({
28
+ title: title ?? task.title,
29
+ description: description ?? task.description,
30
+ completed: completed ?? task.completed,
31
+ });
32
+ return { task };
33
+ }
34
+
35
+ async delete({ container, request }) {
36
+ const task = await this.#find(container, request.params.id);
37
+ if (!task) return { success: false, message: 'Task not found', status: 404 };
38
+
39
+ await task.destroy();
40
+ return { message: 'Task deleted' };
41
+ }
42
+ };
@@ -0,0 +1,26 @@
1
+ const { Route, RequireAuthMiddleware } = require('zyket');
2
+
3
+ // Collection endpoints: GET /tasks and POST /tasks
4
+ module.exports = class TasksRoute extends Route {
5
+ middlewares = {
6
+ get: [new RequireAuthMiddleware()],
7
+ post: [new RequireAuthMiddleware()],
8
+ };
9
+
10
+ async get({ container }) {
11
+ const { Task } = container.get('database').models;
12
+ const tasks = await Task.findAll({ order: [['createdAt', 'DESC']] });
13
+ return { tasks };
14
+ }
15
+
16
+ async post({ container, request }) {
17
+ const { title, description } = request.body || {};
18
+ if (!title) {
19
+ return { success: false, message: 'title is required', status: 400 };
20
+ }
21
+
22
+ const { Task } = container.get('database').models;
23
+ const task = await Task.create({ title, description });
24
+ return { task, status: 201 };
25
+ }
26
+ };
@@ -0,0 +1,9 @@
1
+ // Used by the better-auth CLI (`npx @better-auth/cli generate|migrate`) to
2
+ // discover your auth configuration.
3
+ const AuthService = require('./index.js');
4
+
5
+ const auth = new AuthService({
6
+ get: () => {},
7
+ });
8
+
9
+ module.exports = auth.auth;
@@ -0,0 +1,23 @@
1
+ const { AuthService } = require('zyket');
2
+
3
+ module.exports = class CustomAuthService extends AuthService {
4
+ // This API does not use organizations/multi-tenancy.
5
+ get organizationEnabled() {
6
+ return false;
7
+ }
8
+
9
+ // Set to true once you wire a real email provider below.
10
+ get requireEmailVerification() {
11
+ return false;
12
+ }
13
+
14
+ async sendResetPasswordEmail({ user, url }) {
15
+ // TODO: integrate your email provider. Logged for local development.
16
+ this.client && console.log(`[auth] Reset password for ${user.email}: ${url}`);
17
+ }
18
+
19
+ async sendVerificationEmail({ user, url }) {
20
+ // TODO: integrate your email provider. Logged for local development.
21
+ console.log(`[auth] Verify email for ${user.email}: ${url}`);
22
+ }
23
+ };
@@ -0,0 +1,26 @@
1
+ # --- realtime-chat template ---
2
+ DEBUG=true
3
+ PORT=3000
4
+
5
+ # HTTP (/api/auth), the socket server and the Vite frontend.
6
+ DISABLE_EXPRESS=false
7
+ DISABLE_SOCKET=false
8
+ DISABLE_VITE=false
9
+ VITE_ROOT=./frontend
10
+ VITE_PORT=5173
11
+ VITE_API_BASE=http://localhost:3000
12
+
13
+ # Set true only when frontend and backend live on different domains (needs HTTPS).
14
+ # AUTH_CROSS_DOMAIN=false
15
+
16
+ DATABASE_URL=./database.sqlite
17
+ DATABASE_DIALECT=sqlite
18
+
19
+ SOCKET_MAX_HTTP_BUFFER_SIZE=10485760
20
+
21
+ # Optional: scale Socket.IO across instances with a Redis adapter
22
+ # REDIS_URL=redis://localhost:6379
23
+
24
+ # AUTH_SECRET is generated automatically on first run.
25
+ # BETTER_AUTH_URL=http://localhost:3000
26
+ # TRUSTED_ORIGINS=http://localhost:3000
@@ -0,0 +1,38 @@
1
+ # realtime-chat template
2
+
3
+ Authenticated real-time chat over Socket.IO. Sessions come from better-auth
4
+ (cookie sent during the websocket handshake), so only logged-in users can
5
+ connect or send messages.
6
+
7
+ ## What's included
8
+ - `src/services/auth/` — email/password auth service.
9
+ - `src/guards/auth.js` — reuses the framework `AuthGuard` (session check per event).
10
+ - `src/handlers/connection.js` — enforces auth on connect, joins the `general` room.
11
+ - `src/handlers/message.js` — broadcasts `message` events to the room (guarded by `auth`).
12
+ - `frontend/` — React + Vite chat UI (login/register + live chat over Socket.IO).
13
+
14
+ ## Setup
15
+ ```bash
16
+ npm install
17
+ npx @better-auth/cli migrate
18
+ node index.js
19
+ ```
20
+ This starts the API + websocket server on `:3000` and the Vite frontend on `:5173`.
21
+ Open http://localhost:5173, register a user, and start chatting (open two
22
+ browsers/users to see real-time delivery).
23
+
24
+ ## How auth works here
25
+ 1. The frontend signs in via better-auth → receives the session cookie.
26
+ 2. The Socket.IO client connects with `withCredentials: true`; the cookie travels in the handshake.
27
+ 3. `connection.js` validates the session and disconnects unauthenticated sockets.
28
+
29
+ ## Cookies & local dev
30
+ Cookie security is now environment-aware (see the framework auth service):
31
+ - **Local dev** (default): `sameSite=lax`, not `secure` → works over `http://localhost`
32
+ because the Vite frontend (`:5173`) and API (`:3000`) are same-site.
33
+ - **Cross-domain** front/back in production: set `AUTH_CROSS_DOMAIN=true` (requires HTTPS)
34
+ to switch to `sameSite=none; secure`.
35
+
36
+ ## Notes
37
+ - Set `REDIS_URL` to broadcast across multiple instances.
38
+ - `VITE_API_BASE` (frontend `.env`) points the UI at the backend; defaults to `http://localhost:3000`.
@@ -0,0 +1,3 @@
1
+ # Vite reads env from the frontend folder. Copy to frontend/.env if your
2
+ # backend is not on http://localhost:3000.
3
+ VITE_API_BASE=http://localhost:3000
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Zyket Chat</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./main.jsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,18 @@
1
+ import { createRoot } from 'react-dom/client'
2
+ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
3
+ import './styles.css'
4
+ import AuthView from './src/views/AuthView'
5
+ import ChatView from './src/views/ChatView'
6
+ import LoggedMiddleware from './src/middlewares/LoggedMiddleware'
7
+ import NotLoggedMiddleware from './src/middlewares/NotLoggedMiddleware'
8
+
9
+ createRoot(document.getElementById('root')).render(
10
+ <BrowserRouter>
11
+ <Routes>
12
+ <Route path="/" element={<Navigate to="/chat" replace />} />
13
+ <Route path="/auth" element={<NotLoggedMiddleware><AuthView /></NotLoggedMiddleware>} />
14
+ <Route path="/chat" element={<LoggedMiddleware><ChatView /></LoggedMiddleware>} />
15
+ <Route path="*" element={<Navigate to="/chat" replace />} />
16
+ </Routes>
17
+ </BrowserRouter>
18
+ )
@@ -0,0 +1,27 @@
1
+ import { useStoreAuth } from "../store/storeAuth";
2
+
3
+ export default function useAuth() {
4
+ const context = useStoreAuth();
5
+ const sessionContext = context?.client?.useSession();
6
+ const { isPending, refetch } = sessionContext || {};
7
+ const { user, session } = sessionContext?.data || {};
8
+
9
+ const login = async (email, password) => {
10
+ const { data, error } = await context.client.signIn.email({ email, password });
11
+ if (error) throw error;
12
+ return data;
13
+ };
14
+
15
+ const register = async ({ email, password, name }) => {
16
+ const { data, error } = await context.client.signUp.email({ email, password, name });
17
+ if (error) throw error;
18
+ return data;
19
+ };
20
+
21
+ const logout = async () => {
22
+ await context.client.signOut();
23
+ window.location.href = "/auth";
24
+ };
25
+
26
+ return { ...context, login, register, logout, isPending, refetch, user, session };
27
+ }
@@ -0,0 +1,29 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { io } from "socket.io-client";
3
+ import { useStoreAuth } from "../store/storeAuth";
4
+
5
+ // Connects to the Socket.IO server with the session cookie (withCredentials).
6
+ export default function useChatSocket() {
7
+ const { apiBase } = useStoreAuth();
8
+ const socketRef = useRef(null);
9
+ const [messages, setMessages] = useState([]);
10
+ const [connected, setConnected] = useState(false);
11
+
12
+ useEffect(() => {
13
+ const socket = io(apiBase, { withCredentials: true });
14
+ socketRef.current = socket;
15
+
16
+ socket.on("connect", () => setConnected(true));
17
+ socket.on("disconnect", () => setConnected(false));
18
+ socket.on("message", (m) => setMessages((prev) => [...prev, m]));
19
+
20
+ return () => socket.disconnect();
21
+ }, [apiBase]);
22
+
23
+ const send = (text) =>
24
+ new Promise((resolve) => {
25
+ socketRef.current?.emit("message", { text }, resolve);
26
+ });
27
+
28
+ return { messages, connected, send };
29
+ }
@@ -0,0 +1,12 @@
1
+ import PropTypes from "prop-types";
2
+ import { Navigate } from "react-router-dom";
3
+ import useAuth from "../hooks/useAuth";
4
+
5
+ export default function LoggedMiddleware({ children }) {
6
+ const { user, isPending } = useAuth();
7
+ if (isPending) return null;
8
+ if (!user) return <Navigate to="/auth" replace />;
9
+ return children;
10
+ }
11
+
12
+ LoggedMiddleware.propTypes = { children: PropTypes.node.isRequired };
@@ -0,0 +1,12 @@
1
+ import PropTypes from "prop-types";
2
+ import { Navigate } from "react-router-dom";
3
+ import useAuth from "../hooks/useAuth";
4
+
5
+ export default function NotLoggedMiddleware({ children }) {
6
+ const { user, isPending } = useAuth();
7
+ if (isPending) return null;
8
+ if (user) return <Navigate to="/chat" replace />;
9
+ return children;
10
+ }
11
+
12
+ NotLoggedMiddleware.propTypes = { children: PropTypes.node.isRequired };
@@ -0,0 +1,11 @@
1
+ import { create } from "zustand";
2
+ import { createAuthClient } from "better-auth/react";
3
+
4
+ const API_BASE = import.meta.env.VITE_API_BASE || "http://localhost:3000";
5
+
6
+ export const useStoreAuth = create(() => ({
7
+ apiBase: API_BASE,
8
+ client: createAuthClient({
9
+ baseURL: `${API_BASE}/api/auth/`,
10
+ }),
11
+ }));
@@ -0,0 +1,70 @@
1
+ import { useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import useAuth from "../hooks/useAuth";
4
+
5
+ export default function AuthView() {
6
+ const { login, register } = useAuth();
7
+ const navigate = useNavigate();
8
+ const [mode, setMode] = useState("login");
9
+ const [form, setForm] = useState({ name: "", email: "", password: "" });
10
+ const [error, setError] = useState(null);
11
+ const [loading, setLoading] = useState(false);
12
+
13
+ const submit = async (e) => {
14
+ e.preventDefault();
15
+ setError(null);
16
+ setLoading(true);
17
+ try {
18
+ if (mode === "login") await login(form.email, form.password);
19
+ else await register(form);
20
+ navigate("/chat", { replace: true });
21
+ } catch (err) {
22
+ setError(err?.message || "Authentication failed");
23
+ } finally {
24
+ setLoading(false);
25
+ }
26
+ };
27
+
28
+ const field = (key, type = "text", placeholder = "") => (
29
+ <input
30
+ type={type}
31
+ placeholder={placeholder}
32
+ value={form[key]}
33
+ onChange={(e) => setForm({ ...form, [key]: e.target.value })}
34
+ className="w-full px-4 py-2 rounded-lg bg-zinc-800 border border-zinc-700 text-white outline-none focus:border-orange-500"
35
+ required
36
+ />
37
+ );
38
+
39
+ return (
40
+ <div className="min-h-screen bg-zinc-950 text-white flex items-center justify-center p-4">
41
+ <form onSubmit={submit} className="w-full max-w-sm space-y-4 bg-zinc-900 p-8 rounded-2xl border border-zinc-800">
42
+ <h1 className="text-2xl font-bold text-center">
43
+ {mode === "login" ? "Sign in" : "Create account"}
44
+ </h1>
45
+
46
+ {mode === "register" && field("name", "text", "Name")}
47
+ {field("email", "email", "Email")}
48
+ {field("password", "password", "Password")}
49
+
50
+ {error && <p className="text-sm text-red-400">{error}</p>}
51
+
52
+ <button
53
+ type="submit"
54
+ disabled={loading}
55
+ className="w-full py-2 rounded-lg bg-orange-500 text-black font-semibold disabled:opacity-50"
56
+ >
57
+ {loading ? "..." : mode === "login" ? "Sign in" : "Sign up"}
58
+ </button>
59
+
60
+ <button
61
+ type="button"
62
+ onClick={() => setMode(mode === "login" ? "register" : "login")}
63
+ className="w-full text-sm text-zinc-400 hover:text-white"
64
+ >
65
+ {mode === "login" ? "Need an account? Sign up" : "Have an account? Sign in"}
66
+ </button>
67
+ </form>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,69 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import useAuth from "../hooks/useAuth";
3
+ import useChatSocket from "../hooks/useChatSocket";
4
+
5
+ export default function ChatView() {
6
+ const { user, logout } = useAuth();
7
+ const { messages, connected, send } = useChatSocket();
8
+ const [text, setText] = useState("");
9
+ const bottomRef = useRef(null);
10
+
11
+ useEffect(() => {
12
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
13
+ }, [messages]);
14
+
15
+ const onSend = async (e) => {
16
+ e.preventDefault();
17
+ const value = text.trim();
18
+ if (!value) return;
19
+ setText("");
20
+ await send(value);
21
+ };
22
+
23
+ return (
24
+ <div className="min-h-screen bg-zinc-950 text-white flex flex-col">
25
+ <header className="flex items-center justify-between px-6 py-4 border-b border-zinc-800">
26
+ <div className="flex items-center gap-3">
27
+ <span className="font-bold">#general</span>
28
+ <span className={`text-xs ${connected ? "text-green-400" : "text-zinc-500"}`}>
29
+ {connected ? "● online" : "○ connecting…"}
30
+ </span>
31
+ </div>
32
+ <div className="flex items-center gap-3 text-sm">
33
+ <span className="text-zinc-400">{user?.name || user?.email}</span>
34
+ <button onClick={logout} className="text-zinc-400 hover:text-white">Logout</button>
35
+ </div>
36
+ </header>
37
+
38
+ <main className="flex-1 overflow-auto px-6 py-4 space-y-3">
39
+ {messages.length === 0 && (
40
+ <p className="text-zinc-600 text-sm">No messages yet. Say hi 👋</p>
41
+ )}
42
+ {messages.map((m) => {
43
+ const mine = m.user?.id === user?.id;
44
+ return (
45
+ <div key={m.id} className={`flex ${mine ? "justify-end" : "justify-start"}`}>
46
+ <div className={`max-w-md px-4 py-2 rounded-2xl ${mine ? "bg-orange-500 text-black" : "bg-zinc-800"}`}>
47
+ {!mine && <p className="text-xs text-zinc-400 mb-0.5">{m.user?.name}</p>}
48
+ <p className="text-sm break-words">{m.text}</p>
49
+ </div>
50
+ </div>
51
+ );
52
+ })}
53
+ <div ref={bottomRef} />
54
+ </main>
55
+
56
+ <form onSubmit={onSend} className="flex gap-2 px-6 py-4 border-t border-zinc-800">
57
+ <input
58
+ value={text}
59
+ onChange={(e) => setText(e.target.value)}
60
+ placeholder="Type a message…"
61
+ className="flex-1 px-4 py-2 rounded-lg bg-zinc-800 border border-zinc-700 outline-none focus:border-orange-500"
62
+ />
63
+ <button type="submit" className="px-5 py-2 rounded-lg bg-orange-500 text-black font-semibold">
64
+ Send
65
+ </button>
66
+ </form>
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), tailwindcss()],
7
+ })
@@ -0,0 +1,14 @@
1
+ const { Kernel } = require('zyket');
2
+
3
+ const kernel = new Kernel({
4
+ services: [
5
+ ['auth', require('./src/services/auth'), ['@service_container']],
6
+ ],
7
+ });
8
+
9
+ kernel.boot().then(async () => {
10
+ await kernel.container.get('database').sync();
11
+ kernel.container.get('logger').info('realtime-chat template ready (ws + http)');
12
+ }).catch((error) => {
13
+ console.error('Error booting kernel:', error);
14
+ });
@@ -0,0 +1,3 @@
1
+ // Reuse the framework's session-based socket guard.
2
+ // Referenced from handlers via `guards = ["auth"];`
3
+ module.exports = require('zyket').AuthGuard;
@@ -0,0 +1,23 @@
1
+ const { Handler } = require('zyket');
2
+
3
+ // The connection handler enforces auth on connect: throwing here disconnects
4
+ // the socket (see the SocketIO service). Authenticated sockets join the room.
5
+ module.exports = class ConnectionHandler extends Handler {
6
+ async handle({ container, socket }) {
7
+ const session = await container.get('auth').getSession(socket.handshake.headers);
8
+
9
+ if (!session || !session.user) {
10
+ throw new Error('Unauthorized'); // disconnects the socket
11
+ }
12
+
13
+ socket.data.user = session.user;
14
+ socket.data.session = session.session;
15
+ socket.join('general');
16
+
17
+ container.get('logger').info(`Chat: ${session.user.email} connected (${socket.id})`);
18
+ socket.emit('welcome', {
19
+ room: 'general',
20
+ user: { id: session.user.id, name: session.user.name },
21
+ });
22
+ }
23
+ };
@@ -0,0 +1,29 @@
1
+ const { Handler } = require('zyket');
2
+
3
+ // Handles `socket.emit("message", { text })`. The `auth` guard re-validates the
4
+ // session before every message (defense in depth) and re-attaches the user.
5
+ module.exports = class MessageHandler extends Handler {
6
+ guards = ['auth'];
7
+
8
+ async handle({ container, socket, data, io }) {
9
+ const user = socket.data.user;
10
+ const text = String(data?.text || '').trim().slice(0, 2000);
11
+
12
+ if (!text) {
13
+ return { error: 'Empty message' };
14
+ }
15
+
16
+ const message = {
17
+ id: `${Date.now()}-${socket.id}`,
18
+ text,
19
+ user: { id: user.id, name: user.name },
20
+ at: new Date().toISOString(),
21
+ };
22
+
23
+ // Broadcast to everyone in the room (including sender).
24
+ io.to('general').emit('message', message);
25
+
26
+ container.get('logger').debug(`Chat message from ${user.email}`);
27
+ return { ok: true, message };
28
+ }
29
+ };
@@ -0,0 +1,8 @@
1
+ // Used by the better-auth CLI (`npx @better-auth/cli generate|migrate`).
2
+ const AuthService = require('./index.js');
3
+
4
+ const auth = new AuthService({
5
+ get: () => {},
6
+ });
7
+
8
+ module.exports = auth.auth;
@@ -0,0 +1,19 @@
1
+ const { AuthService } = require('zyket');
2
+
3
+ module.exports = class CustomAuthService extends AuthService {
4
+ get organizationEnabled() {
5
+ return false;
6
+ }
7
+
8
+ get requireEmailVerification() {
9
+ return false;
10
+ }
11
+
12
+ async sendResetPasswordEmail({ user, url }) {
13
+ console.log(`[auth] Reset password for ${user.email}: ${url}`);
14
+ }
15
+
16
+ async sendVerificationEmail({ user, url }) {
17
+ console.log(`[auth] Verify email for ${user.email}: ${url}`);
18
+ }
19
+ };