Compare commits

27 Commits

Author SHA1 Message Date
deno-deploy[bot]
4fb87cc127 [Deno Deploy] Update .github/workflows/deploy.yml 2025-04-04 06:50:43 +00:00
deno-deploy[bot]
def0f2bd9c [Deno Deploy] Add .github/workflows/deploy.yml 2025-04-04 06:40:33 +00:00
Lynixenn
68aa7c20f3 removed files 2025-03-30 21:57:57 +02:00
Lynixenn
6e87e5003e Utils added extra 2025-03-30 21:57:15 +02:00
Esad Mustafoski
0ecd265ade Update deno.json
Quick fix
2025-03-29 00:36:11 +01:00
Lynixenn
895a40fe67 Added database creation upon start of API, only does this when the Database doesn't exist. 2025-03-28 23:51:03 +01:00
Lynixenn
cfcfd4ba5e Fixed Login and Register user not returning, Fixed error upon logging/registering 2025-03-25 18:41:39 +01:00
Lynixenn
9e7039c72c Now returns userId upon Register 2025-03-24 21:21:45 +01:00
Esad Mustafoski
029fc10de7 Fix Error in create_db 2025-03-21 08:32:28 +01:00
Esad Mustafoski
8b594990ee Update create_db.ts 2025-03-20 11:56:23 +01:00
Esad Mustafoski
65ced2f3d3 Update helpers.ts to match comment structure 2025-03-18 08:31:14 +01:00
Esad Mustafoski
497d5aa28f Generated API Docs using AI, Might edit later if there is an issue or it's hard to read. Finished API Functions 2025-03-17 21:39:26 +01:00
Esad Mustafoski
8b992017ba Modulized the project so it's easier to look over items, Unfinished for now, added extra functions for chat, API is not finished for the chat. and will not work. Files saved as a "Work in progress" type of commit. \n Document folders added, but nothing yet done inside of them. \n Some comments have been removed as they were bloated and made it hard to read the code, the functions themselves are self-explanatory and will be documented in the "docs" folders, containing the documentation in a markdown format 2025-03-14 06:37:08 +01:00
Esad Mustafoski
aeaae9fc58 Huge changes! Added some API features, but not fully. 2025-03-14 06:33:12 +01:00
Esad Mustafoski
4c80caa52a Attempted adding of 'Salting' for the password, a method which adds random numbers or letters to make rainbowtable password cracking impossible, not tested yet 2025-02-02 12:33:47 +01:00
Lynixenn
3c6fdd0b59 Fixed account_created format 2024-12-17 22:21:53 +01:00
Lynixenn
4dfa695458 Added body response for Successful login 2024-12-17 19:04:47 +01:00
Lynixenn
010e518828 Login and Register API added, further info shared between Owners 2024-12-17 19:01:14 +01:00
Esad Mustafoski
b0f8a51cc8 Shortened database/utils.ts by adding a 'Query Database' helper function to reduce boilerplate/copied code, also added functions form apping the Post, Account and Comment row. api/main.ts now has more unfinished endpoints and will now work using functions instead of doing everything inside of the routes, created a helpers.ts file to make it a bit more modular and easier to look over. 2024-11-13 23:29:59 +01:00
Esad Mustafoski
7a6152be51 Combined functions, Changed some items 2024-11-07 19:29:35 +01:00
Lynixen
f45945474d Changed functions in database/utils.ts 2024-11-06 21:35:41 +01:00
Esad Mustafoski
064b864246 Removed file 2024-11-06 04:51:35 +01:00
Esad Mustafoski
4d06db689d Removed file 2024-11-06 04:51:09 +01:00
Esad Mustafoski
c786ddd895 Merged some functions in database/utils.ts to attempt to shorten code length, Also changed tests to work with the new datatypes 2024-11-06 04:49:18 +01:00
Esad Mustafoski
108b3f0d50 Updated readme to be more current 2024-11-05 02:32:49 +01:00
Esad Mustafoski
abe39c0797 Documentation Update 2024-11-05 02:22:11 +01:00
Esad Mustafoski
3d31061087 Added API and Tests, Tests generated using AI and will be replaced sometime 2024-11-05 02:05:39 +01:00
29 changed files with 2497 additions and 161 deletions

41
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Deploy
on:
push:
branches: api-dev
pull_request:
branches: api-dev
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
permissions:
id-token: write # Needed for auth with Deno Deploy
contents: read # Needed to clone the repository
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Install Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install step
run: "npm install"
- name: Upload to Deno Deploy
uses: denoland/deployctl@v1
with:
project: "esp-projekt"
entrypoint: "api/main.ts"
root: ""

3
.gitignore vendored
View File

@@ -12,6 +12,9 @@ dist
dist-ssr
.vite
*.local
*.sqlite
*.idea
.env
# Editor directories and files
.vscode/*

View File

@@ -46,25 +46,28 @@ $ deno task build
# Resources
## Libs/Frameworks used
## Frameworks used
<ul>
<li><a href="https://deno.land/"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/84/Deno.svg/600px-Deno.svg.png" width="20" height="20"> Deno</a></li>
<li><a href="https://vitejs.dev/"><img src="https://vitejs.dev/logo.svg" width="20" height="20"> Vite</a></li>
<li><a href="https://www.typescriptlang.org/"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Typescript_logo_2020.svg/512px-Typescript_logo_2020.svg.png?20221110153201" width="20" height="20"> TypeScript</a></li>
<li><a href="https://oakserver.github.io/oak/"><img src="https://oakserver.org/oak_logo.svg?__frsh_c=2e345d91800d1e0a52aa35efcaab769fa1768888" width="20" height="20"> Oak</a></li>
<li><a href="https://deno.land/x/denodb"><img src="https://github.com/eveningkid/denodb/blob/master/design/logo.png?raw=true" width="20" height="20"> DenoDB</a></li>
<li><a href="https://tailwindcss.com/"><img src="https://tailwindcss.com/_next/static/media/tailwindcss-mark.3c5441fc7a190fb1800d4a5c7f07ba4b1345a9c8.svg" width="20" height="20"> TailwindCSS</a></li>
<li><a href="https://vuejs.org/"><img src="https://upload.wikimedia.org/wikipedia/commons/9/95/Vue.js_Logo_2.svg" width="20" height="20">VueJs</a></li>
<li><a href="https://www.sqlite.org/"><img src="https://www.vectorlogo.zone/logos/sqlite/sqlite-icon.svg" height="20", width="20">SQLite</a></li>
</ul>
## Tools used
<ul>
<li><a href="https://code.visualstudio.com/"><img src="https://upload.wikimedia.org/wikipedia/commons/9/9a/Visual_Studio_Code_1.35_icon.svg" width="20" height="20"> Visual Studio Code</a></li>
<li><a href="https://www.jetbrains.com/webstorm/"><img src="https://upload.wikimedia.org/wikipedia/commons/c/c0/WebStorm_Icon.svg" width="20" height="20"> JetBrains WebStorm</a></li>
<li><a href="https://www.jetbrains.com/datagrip/"><img src="https://seeklogo.com/images/D/datagrip-logo-295CA63255-seeklogo.com.png" width="20" height="20"> JetBrains DataGrip</a></li>
<li><a href="https://neovim.io/"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/07/Neovim-mark-flat.svg/640px-Neovim-mark-flat.svg.png" width="20" height="20"> Neovim</a></li>
</ul>
---
### Honorable mentions
<ul>
<li><a href="https://oakserver.github.io/oak/"><img src="https://oakserver.org/oak_logo.svg?__frsh_c=2e345d91800d1e0a52aa35efcaab769fa1768888" width="20" height="20"> Oak</a></li>
</ul>

View File

@@ -1,6 +0,0 @@
INSERT INTO posts (posts_uuid, user_id, created_at, post_text, likes, comments) VALUES
('1a2b3c4d', 1, '2024-11-01 10:00:00', 'Post1', 5, 2),
('2b3c4d5e', 1, '2024-11-02 11:30:00', 'Post2', 10, 3),
('3c4d5e6f', 2, '2024-11-03 12:45:00', 'Post3', 0, 0),
('4d5e6f7g', 2, '2024-11-04 14:20:00', 'Post4', 15, 5),
('5e6f7g8h', 3, '2024-11-05 09:15:00', 'Post5', 8, 1);

499
api/doc/API.md Normal file
View File

@@ -0,0 +1,499 @@
# This was generated using AI!!! Will edit if errors are found.
# API Documentation
## Index
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/` | Returns basic information and directions for using the API endpoints. |
| GET | `/api` | Redirects to the full API documentation. |
---
## Account Endpoints
| Method | Endpoint | Description |
|--------|-------------------------------------------|----------------------------------------------------------------|
| POST | `/api/account/login` | Logs in a user with username and password. |
| POST | `/api/account/register` | Registers a new user with required details. |
| POST | `/api/account/logout` | Logs a user out. (TODO) |
| POST | `/api/account/password/forgot` | Initiates password recovery. (TODO) |
| POST | `/api/account/password/reset` | Resets the password. (TODO) |
| POST | `/api/account/password/change` | Changes the password. (TODO) |
| POST | `/api/account/email/change-email` | Changes the user's email address. (TODO) |
| POST | `/api/account/email/verify-email` | Verifies a user's email. (TODO) |
| POST | `/api/account/delete-account` | Deletes the user account. (TODO) |
| POST | `/api/account/block` | Blocks a user account. (TODO) |
---
## Auth Endpoints
| Method | Endpoint | Description |
|--------|----------------------|--------------------------------------------------|
| GET | `/api/auth` | Auth endpoint (placeholder). |
| GET | `/api/auth/verify` | Verifies an authentication token. (TODO) |
| GET | `/api/auth/refresh` | Refreshes an authentication token. (TODO) |
---
## User Endpoints
| Method | Endpoint | Description |
|--------|---------------|------------------------------------------------------------|
| GET | `/api/users` | Retrieves a list of all users from the database. |
---
## Chat Endpoints
| Method | Endpoint | Description |
|--------|----------------------------------|-----------------------------------------------------------------|
| GET | `/api/chats` | Retrieves chats for a user (requires an Authorization header). |
| GET | `/api/chat/:id` | Retrieves details of a specific chat using its ID. |
| POST | `/api/chat/create` | Creates a new chat with at least 2 participants. |
| POST | `/api/chat/:id/message` | Sends a message to the specified chat. |
| DELETE | `/api/chat/:id` | Deletes a specific chat by its ID. |
---
## Post Endpoints
| Method | Endpoint | Description |
|--------|----------------------------------|-------------------------------------------------------------------|
| GET | `/api/posts` | Retrieves all posts. |
| GET | `/api/post/:id` | Retrieves a specific post by its ID. |
| POST | `/api/post/create` | Creates a new post. |
| PUT | `/api/post/:id` | Updates an existing post (requires at least one update field). |
| DELETE | `/api/post/:id` | Deletes a specific post by its ID. |
| POST | `/api/post/:id/like` | Likes a specific post. |
---
## Comment Endpoints
| Method | Endpoint | Description |
|--------|-----------------------------------|--------------------------------------------------------------------|
| GET | `/api/post/:id/comments` | Retrieves all comments associated with a specific post. |
| POST | `/api/post/:id/comment` | Adds a new comment to a specified post. |
| PUT | `/api/comment/:id` | Updates an existing comment using its ID. |
| DELETE | `/api/comment/:id` | Deletes a specific comment by its ID. |
| POST | `/api/comment/:id/like` | Likes a specific comment. |
> Note: Replace any `:id` placeholder in the URLs with the actual identifier when making requests.
> Note: Endpoints marked as TODO are placeholders for future implementation.
---
## Base URL
Assuming the server is running on port 8000, the base URL is:
```
http://localhost:8000
```
---
## Index & Documentation Routes
- **GET /**
A simple informational endpoint.
- **Response:** `"For endpoints, use /api/{name}"`
**Example:**
```bash
curl http://localhost:8000/
```
- **GET /api**
Directs users for API documentation.
- **Response:** `"For API Documentation, visit /docs"`
**Example:**
```bash
curl http://localhost:8000/api
```
---
## Account Endpoints
### 1. POST /api/account/login
Log in a user with their username and password.
- **Request Body:**
```json
{
"username": "johndoe",
"password": "secret123"
}
```
- **Response:** On success returns status 200 with body `"Success"`. If unsuccessful, appropriate error messages are returned.
**Example:**
```bash
curl -X POST http://localhost:8000/api/account/login \
-H "Content-Type: application/json" \
-d '{"username": "johndoe", "password": "secret123"}'
```
### 2. POST /api/account/register
Register a new user with all required fields.
- **Request Body:**
```json
{
"username": "johndoe",
"password": "secret123",
"userGroup": "default",
"displayname": "John Doe",
"user_email": "john@example.com",
"firstname": "John",
"surname": "Doe"
}
```
- **Response:** On success returns a status of 200 and a message indicating the user ID registration.
**Example:**
```bash
curl -X POST http://localhost:8000/api/account/register \
-H "Content-Type: application/json" \
-d '{
"username": "johndoe",
"password": "secret123",
"userGroup": "default",
"displayname": "John Doe",
"user_email": "john@example.com",
"firstname": "John",
"surname": "Doe"
}'
```
### Other Account Endpoints
- POST `/api/account/logout`
- POST `/api/account/password/forgot`
- POST `/api/account/password/reset`
- POST `/api/account/password/change`
- POST `/api/account/email/change-email`
- POST `/api/account/email/verify-email`
- POST `/api/account/delete-account`
- POST `/api/account/block`
These endpoints are marked as TODO and currently have no implemented logic.
---
## Auth Endpoints
- GET `/api/auth`
- GET `/api/auth/verify`
- GET `/api/auth/refresh`
These endpoints are also marked as TODO.
---
## User Endpoints
### GET /api/users
Retrieve all users from the database.
- **Response:** JSON array of user objects.
**Example:**
```bash
curl http://localhost:8000/api/users
```
---
## Chat Endpoints
### 1. GET /api/chats
Retrieve all chats for a user. Requires an "Authorization" header and an optional query parameter for userId.
- **Headers:**
- Authorization: Bearer <token> (only required to simulate authentication)
- **Query Parameter:**
- userId (optional; defaults to "1" if not provided)
- **Response:** JSON array of chats for the specified user.
**Example:**
```bash
curl "http://localhost:8000/api/chats?userId=2" \
-H "Authorization: Bearer mytoken123"
```
### 2. GET /api/chat/:id
Retrieve details of a specific chat identified by its ID.
- **Route Parameter:**
- id: Chat ID
- **Response:** JSON object containing chat details.
**Example:**
```bash
curl http://localhost:8000/api/chat/10
```
### 3. POST /api/chat/create
Create a new chat with at least two participants.
- **Request Body:**
```json
{
"participants": [1, 2],
"chatName": "Group Chat"
}
```
- **Response:** Returns status 201 and a JSON object with the newly created "chatId".
**Example:**
```bash
curl -X POST http://localhost:8000/api/chat/create \
-H "Content-Type: application/json" \
-d '{"participants": [1, 2], "chatName": "Group Chat"}'
```
### 4. POST /api/chat/:id/message
Send a message to a specific chat.
- **Route Parameter:**
- id: Chat ID
- **Request Body:**
```json
{
"senderId": 1,
"content": "Hello, world!"
}
```
- **Response:** Returns status 201 and a JSON object with the "messageId" of the new message.
**Example:**
```bash
curl -X POST http://localhost:8000/api/chat/10/message \
-H "Content-Type: application/json" \
-d '{"senderId": 1, "content": "Hello, world!"}'
```
### 5. DELETE /api/chat/:id
Delete a specific chat.
- **Route Parameter:**
- id: Chat ID
- **Response:** Returns status 200 with a confirmation message.
**Example:**
```bash
curl -X DELETE http://localhost:8000/api/chat/10
```
---
## Post Endpoints
### 1. GET /api/posts
Retrieve all posts.
- **Response:** JSON array of posts.
**Example:**
```bash
curl http://localhost:8000/api/posts
```
### 2. GET /api/post/:id
Retrieve a specific post by its ID.
- **Route Parameter:**
- id: Post ID
- **Response:** JSON object representing the post.
**Example:**
```bash
curl http://localhost:8000/api/post/15
```
### 3. POST /api/post/create
Create a new post.
- **Request Body:**
```json
{
"userId": 1,
"postText": "This is a new post",
"postType": "text"
}
```
- **Response:** Returns status 201 with the "postId" of the created post.
**Example:**
```bash
curl -X POST http://localhost:8000/api/post/create \
-H "Content-Type: application/json" \
-d '{"userId": 1, "postText": "This is a new post", "postType": "text"}'
```
### 4. PUT /api/post/:id
Update an existing post. At least one of `postText` or `postType` must be provided.
- **Route Parameter:**
- id: Post ID
- **Request Body:**
```json
{
"postText": "Updated post text",
"postType": "text"
}
```
- **Response:** Returns status 200 with a confirmation message.
**Example:**
```bash
curl -X PUT http://localhost:8000/api/post/15 \
-H "Content-Type: application/json" \
-d '{"postText": "Updated post text", "postType": "text"}'
```
### 5. DELETE /api/post/:id
Delete a specific post.
- **Route Parameter:**
- id: Post ID
- **Response:** Returns status 200 with a message confirming deletion.
**Example:**
```bash
curl -X DELETE http://localhost:8000/api/post/15
```
### 6. POST /api/post/:id/like
Like a specific post.
- **Route Parameter:**
- id: Post ID
- **Request Body:**
```json
{
"userId": 1
}
```
- **Response:** Returns status 200 with a confirmation message.
**Example:**
```bash
curl -X POST http://localhost:8000/api/post/15/like \
-H "Content-Type: application/json" \
-d '{"userId": 1}'
```
---
## Comment Endpoints
### 1. GET /api/post/:id/comments
Retrieve all comments for a specific post.
- **Route Parameter:**
- id: Post ID
- **Response:** JSON array of comments.
**Example:**
```bash
curl http://localhost:8000/api/post/15/comments
```
### 2. POST /api/post/:id/comment
Create a new comment on a specific post.
- **Route Parameter:**
- id: Post ID
- **Request Body:**
```json
{
"userId": 1,
"text": "This is a comment"
}
```
- **Response:** Returns status 201 with the newly created "commentId".
**Example:**
```bash
curl -X POST http://localhost:8000/api/post/15/comment \
-H "Content-Type: application/json" \
-d '{"userId": 1, "text": "This is a comment"}'
```
### 3. PUT /api/comment/:id
Update an existing comment.
- **Route Parameter:**
- id: Comment ID
- **Request Body:**
```json
{
"text": "Updated comment text"
}
```
- **Response:** Returns status 200 with a confirmation message.
**Example:**
```bash
curl -X PUT http://localhost:8000/api/comment/20 \
-H "Content-Type: application/json" \
-d '{"text": "Updated comment text"}'
```
### 4. DELETE /api/comment/:id
Delete a specific comment.
- **Route Parameter:**
- id: Comment ID
- **Response:** Returns status 200 with a confirmation message.
**Example:**
```bash
curl -X DELETE http://localhost:8000/api/comment/20
```
### 5. POST /api/comment/:id/like
Like a specific comment.
- **Route Parameter:**
- id: Comment ID
- **Request Body:**
```json
{
"userId": 1
}
```
- **Response:** Returns status 200 with a confirmation message.
**Example:**
```bash
curl -X POST http://localhost:8000/api/comment/20/like \
-H "Content-Type: application/json" \
-d '{"userId": 1}'
```

73
api/helpers.ts Normal file
View File

@@ -0,0 +1,73 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @file api/helpers.ts
* @description Helper functions for the API
*/
// +++ IMPORTS ------------------------------------------------------ //
import { Context } from "https://deno.land/x/oak/mod.ts";
import { encodeHex } from "jsr:@std/encoding/hex";
// import { hash } from "node:crypto";
export type ApiResponse = {
status: number;
body: unknown;
};
// +++ FUNCTIONS ---------------------------------------------------- //
/**
* @description Sends a response to the client
* @usage sendResponse(ctx, { status: 200, body: { message: "Success" } })
* Status is the HTTP Status code
* Body is the response body/message/data.
*/
const sendResponse = (ctx: Context, { status, body }: ApiResponse): void => {
ctx.response.status = status;
ctx.response.body = body as any;
};
/**
* @usage errorResponse(ctx, 401, "Unauthorized")
* @see sendResponse
*/
const errorResponse = (ctx: Context, status: number, message: string): void => {
sendResponse(ctx, { status, body: { error: message } });
};
/**
* @description password "Salter", used to salt the passwords before the hash, this salt will be
* returned seperately to save the salt in the DB
* @param password The password to salt
* @returns {saltedPassword: string, salt: string} Password with the salt + Salt seperately, both strings
*/
const saltPassword = async (
password: string,
): Promise<{ saltedPassword: string; salt: string }> => {
const saltBytes = new Uint8Array(16); // 16 bytes = 128 bits for randomness
await crypto.getRandomValues(saltBytes);
const salt = encodeHex(saltBytes);
const saltedPassword = `${password}${salt}`;
return {
saltedPassword,
salt,
};
};
/**
* @description Hashing Function for Passwords/etc
* @param password The password to hash
* @returns {hash: string} The hashed password as a string
*/
const hashPassword = async (password: string): Promise<string> => {
const to_hash = password;
const buffer = new TextEncoder().encode(to_hash);
const hash_buffer = await crypto.subtle.digest("SHA-256", buffer);
const hash = await encodeHex(hash_buffer);
return hash;
};
export { errorResponse, hashPassword, saltPassword, sendResponse };

142
api/helpers/chat_api.ts Normal file
View File

@@ -0,0 +1,142 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description API file for Comments
*/
// +++ IMPORTS ------------------------------------------------------ //
import * as db_utils from "../../database/utils.ts";
import * as helper_utils from "../helpers.ts";
// +++ FUNCTIONS ----------------------------------------------------- //
async function api_getChats(ctx: any): Promise<void> {
try {
const authHeader = ctx.request.headers.get("Authorization");
if (!authHeader) {
helper_utils.errorResponse(ctx, 401, "Authentication Required");
return;
}
// Get userId from query parameter (for testing) or use 1 as default (avoid errors because of userid)
// Assumes 1 is a test account
const userId = ctx.request.url.searchParams.get("userId") || "1";
const chats = await db_utils.getUserChats(userId);
ctx.response.body = chats;
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error retrieving chats");
console.log(error);
}
}
async function api_getChatById(ctx: any): Promise<void> {
try {
const chatId = ctx.params.id;
if (!chatId) {
helper_utils.errorResponse(ctx, 400, "Chat ID required");
return;
}
const chat = await db_utils.getChatById(chatId);
if (!chat) {
helper_utils.errorResponse(ctx, 404, "Chat not found");
return;
}
ctx.response.body = chat;
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error retrieving chat");
console.log(error);
}
}
async function api_createChat(ctx: any): Promise<void> {
try {
const body = ctx.request.body;
const result = await body.json();
const { participants, chatName } = result;
if (
!participants || !Array.isArray(participants) || participants.length < 2
) {
helper_utils.errorResponse(
ctx,
400,
"Two people required to create a chat",
);
return;
}
const chatId = await db_utils.createChat(participants, chatName || "");
helper_utils.sendResponse(ctx, {
status: 201,
body: { chatId },
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error creating chat");
console.log(error);
}
}
async function api_sendMessage(ctx: any): Promise<void> {
try {
const chatId = ctx.params.id;
if (!chatId) {
helper_utils.errorResponse(ctx, 400, "Chat ID required");
return;
}
const body = ctx.request.body;
const result = await body.json();
const { senderId, content } = result;
if (!senderId || !content) {
helper_utils.errorResponse(
ctx,
400,
"Sender ID and message content required",
);
return;
}
const messageId = await db_utils.addMessageToChat(
chatId,
senderId,
content,
);
helper_utils.sendResponse(ctx, {
status: 201,
body: { messageId },
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error sending message");
console.log(error);
}
}
async function api_deleteChat(ctx: any): Promise<void> {
try {
const chatId = ctx.params.id;
if (!chatId) {
helper_utils.errorResponse(ctx, 400, "Chat ID required");
return;
}
await db_utils.deleteChat(chatId);
helper_utils.sendResponse(ctx, {
status: 200,
body: { message: "Chat deleted successfully" },
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error deleting chat");
console.log(error);
}
}
export {
api_createChat,
api_deleteChat,
api_getChatById,
api_getChats,
api_sendMessage,
};

142
api/helpers/comments_api.ts Normal file
View File

@@ -0,0 +1,142 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description API file for Comments
*/
// +++ IMPORTS ------------------------------------------------------ //
import * as db_utils from "../../database/utils.ts";
import * as helper_utils from "../helpers.ts";
// +++ FUNCTIONS ----------------------------------------------------- //
async function api_getPostComments(ctx: any): Promise<void> {
try {
const postId = ctx.params.id;
if (!postId) {
helper_utils.errorResponse(ctx, 400, "Post ID required");
return;
}
const comments = await db_utils.getCommentsFromDB(Number(postId));
ctx.response.body = comments;
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error retrieving comments");
console.log(error);
}
}
async function api_createComment(ctx: any): Promise<void> {
try {
const postId = ctx.params.id;
if (!postId) {
helper_utils.errorResponse(ctx, 400, "Post ID required");
return;
}
const body = ctx.request.body;
const result = await body.json();
const { userId, text } = result;
if (!userId || !text) {
helper_utils.errorResponse(ctx, 400, "User ID and comment text required");
return;
}
// Create timestamp in the format expected by the database
const createdAt = `${Math.floor(Date.now() / 1000)}-${
new Date().toLocaleDateString("en-GB").split("/").join("-")
}`;
const commentId = await db_utils.createComment(postId, userId, createdAt, text);
helper_utils.sendResponse(ctx, {
status: 201,
body: { commentId }
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error creating comment");
console.log(error);
}
}
async function api_updateComment(ctx: any): Promise<void> {
try {
const commentId = ctx.params.id;
if (!commentId) {
helper_utils.errorResponse(ctx, 400, "Comment ID required");
return;
}
const body = ctx.request.body;
const result = await body.json();
const { text } = result;
if (!text) {
helper_utils.errorResponse(ctx, 400, "Comment text required");
return;
}
await db_utils.updateComment(commentId, text);
helper_utils.sendResponse(ctx, {
status: 200,
body: { message: "Comment updated successfully" }
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error updating comment");
console.log(error);
}
}
async function api_deleteComment(ctx: any): Promise<void> {
try {
const commentId = ctx.params.id;
if (!commentId) {
helper_utils.errorResponse(ctx, 400, "Comment ID required");
return;
}
await db_utils.deleteComment(commentId);
helper_utils.sendResponse(ctx, {
status: 200,
body: { message: "Comment deleted successfully" }
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error deleting comment");
console.log(error);
}
}
async function api_likeComment(ctx: any): Promise<void> {
try {
const commentId = ctx.params.id;
if (!commentId) {
helper_utils.errorResponse(ctx, 400, "Comment ID required");
return;
}
const body = ctx.request.body;
const result = await body.json();
const { userId } = result;
if (!userId) {
helper_utils.errorResponse(ctx, 400, "User ID required");
return;
}
await db_utils.likeComment(commentId, userId);
helper_utils.sendResponse(ctx, {
status: 200,
body: { message: "Comment liked successfully" }
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error liking comment");
console.log(error);
}
}
export {
api_createComment,
api_deleteComment,
api_getPostComments,
api_likeComment,
api_updateComment,
};

11
api/helpers/mod.ts Normal file
View File

@@ -0,0 +1,11 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description A mod file is used to export all the functions in the folder, making them easier to access.
* @file mod.ts
*/
export * from "./chat_api.ts";
export * from "./comments_api.ts";
export * from "./post_api.ts";
export * from "./user_api.ts";

147
api/helpers/post_api.ts Normal file
View File

@@ -0,0 +1,147 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description API file for Posts
*/
// +++ IMPORTS ------------------------------------------------------ //
import * as db_utils from "../../database/utils.ts";
import * as helper_utils from "../helpers.ts";
import { Context } from "https://deno.land/x/oak@v17.1.2/mod.ts";
// +++ FUNCTIONS ----------------------------------------------------- //
async function api_getPostById(ctx: any): Promise<void> {
try {
const postId = ctx.params.id;
if (!postId) {
helper_utils.errorResponse(ctx, 400, "Post ID required");
return;
}
const post = await db_utils.getPostById(postId);
if (!post) {
helper_utils.errorResponse(ctx, 404, "Post not found");
return;
}
ctx.response.body = post;
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error retrieving post");
}
}
async function api_createPost(ctx: Context): Promise<void> {
try {
const body = ctx.request.body;
const result = await body.json();
const { userId, postText, postType } = result;
if (!userId || !postText || !postType) {
helper_utils.errorResponse(
ctx,
400,
"User ID, post text, and post type required",
);
return;
}
// Create timestamp in the format expected by the database
const createdAt = `${Math.floor(Date.now() / 1000)}-${new Date()
.toLocaleDateString("en-GB")
.split("/")
.join("-")}`;
const postId = await db_utils.createPost(
userId,
createdAt,
postText,
postType,
);
helper_utils.sendResponse(ctx, {
status: 201,
body: { postId },
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error creating post");
}
}
async function api_updatePost(ctx: any): Promise<void> {
try {
const postId = ctx.params.id;
if (!postId) {
helper_utils.errorResponse(ctx, 400, "Post ID required");
return;
}
const body = ctx.request.body;
const result = await body.json();
const { postText, postType } = result;
if (!postText && !postType) {
helper_utils.errorResponse(ctx, 400, "No update data provided");
return;
}
await db_utils.updatePost(postId, postText, postType);
helper_utils.sendResponse(ctx, {
status: 200,
body: { message: "Post updated successfully" },
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error updating post");
}
}
async function api_deletePost(ctx: any): Promise<void> {
try {
const postId = ctx.params.id;
if (!postId) {
helper_utils.errorResponse(ctx, 400, "Post ID required");
return;
}
await db_utils.deletePost(postId);
helper_utils.sendResponse(ctx, {
status: 200,
body: { message: "Post deleted successfully" },
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error deleting post");
}
}
async function api_likePost(ctx: any): Promise<void> {
try {
const postId = ctx.params.id;
if (!postId) {
helper_utils.errorResponse(ctx, 400, "Post ID required");
return;
}
const body = ctx.request.body;
const result = await body.json();
const { userId } = result;
if (!userId) {
helper_utils.errorResponse(ctx, 400, "User ID required");
return;
}
await db_utils.likePost(postId, userId);
helper_utils.sendResponse(ctx, {
status: 200,
body: { message: "Post liked successfully" },
});
} catch (error) {
helper_utils.errorResponse(ctx, 500, "Error liking post");
}
}
export {
api_createPost,
api_deletePost,
api_getPostById,
api_likePost,
api_updatePost,
};

45
api/helpers/user_api.ts Normal file
View File

@@ -0,0 +1,45 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description API file for Users
*/
// +++ IMPORTS ------------------------------------------------------ //
import * as db_utils from "../../database/utils.ts";
// import * as helper_utils from "../helpers.ts";
import { Context } from "https://deno.land/x/oak@v17.1.2/mod.ts";
// +++ FUNCTIONS ----------------------------------------------------- //
/*async function api_user_getInfo(ctx: any): Promise<void> {
const id = ctx.params.id;
if (!id) {
ctx.response.status = 400;
ctx.response.body = { error: "User ID required" };
return;
}
try {
const user = await db_utils.getAllUserInfoByID(id);
if (user === null || user === undefined) {
helper_utils.errorResponse(ctx, 404, "User not found");
return;
}
ctx.response.body = user;
} catch (error) {
helper_utils.errorResponse(ctx, 500, error as string);
}
}
*/
async function api_getAllUsers(ctx: Context): Promise<void> {
const getUsers = await db_utils.getAllUsersFromDB();
ctx.response.body = getUsers;
}
export {
api_getAllUsers,
// api_user_getInfo
};

View File

@@ -1,44 +1,307 @@
// main API file. Handles all the routing/api stuff
// Due to the Language servers, the import statements are
// shown as errors, @ts-ignore is used to ignore them.
// This is a Deno file, but the Vue LSP is still
// attempting to find errors, which causes
// confusing False errors
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description Main API file, Handles all the routing/api stuff
*/
// +++ IMPORTS ------------------------------------------------------ //
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { oakCors } from "https://deno.land/x/cors/mod.ts";
import * as db_utils from "../database/utils.ts";
import {
Application,
Context,
Next,
Router,
} from "https://deno.land/x/oak@v17.1.2/mod.ts";
import { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";
import {
dirname,
fromFileUrl,
join,
} from "https://deno.land/std@0.224.0/path/mod.ts";
// +++ VARIABLES ---------------------------------------------------- //
import * as db_utils from "../database/utils.ts";
import * as helper_utils from "./helpers.ts";
import {
// --- Chat --- //
api_createChat,
api_createComment,
api_createPost,
api_deleteChat,
api_deleteComment,
api_deletePost,
// --- User --- //
api_getAllUsers,
api_getChatById,
api_getChats,
// --- Post --- //
api_getPostById,
// --- Comment --- //
api_getPostComments,
api_likeComment,
api_likePost,
api_sendMessage,
api_updateComment,
api_updatePost,
} from "./helpers/mod.ts";
// +++ VARIABLES / TYPES --------------------------------------------- //
const router = new Router();
const app = new Application();
// unused for now
type ApiResponse = {
status: number;
body: unknown;
};
// database creation if missing, runs here because this is the main file executed by the API.
db_utils.ensureDatabaseExists();
// +++ ROUTER ------------------------------------------------------- //
// Creates the routes for the API server.
// Example: localhost:8000/api will show "testAPIPoint"
// in the HTML
// in the HTML page.
// Docs Routes
router
.get("/", (ctx) => {
ctx.response.body = "ESP API Site";
})
.get("/api", (ctx) => {
ctx.response.body = "testAPIPoint";
})
.get("/api/users", (ctx) => {
ctx.response.body = "Info from all users here"; //getAllUsers();
})
.get("/api/posts", async (ctx) => {
const getPosts = await db_utils.getPostsFromDB();
const countedPosts = await db_utils.countPosts();
ctx.response.body = { getPosts, countedPosts };
.get("/", (ctx: any) => {
ctx.response.body = "For endpoints, use /api/{name}";
})
.get("/api", (ctx: any) => {
ctx.response.body = "For API Documentation, visit /docs";
});
// -- Account routes --
router
.post("/api/account/login", api_login)
.post("/api/account/register", api_register)
.post("/api/account/logout", () => {}) // TODO
.post("/api/account/password/forgot", () => {}) // TODO
.post("/api/account/password/reset", () => {}) // TODO
.post("/api/account/password/change", () => {}) // TODO
.post("/api/account/email/change-email", () => {}) // TODO
.post("/api/account/email/verify-email", () => {}) // TODO
.post("/api/account/delete-account", () => {}) // TODO
.post("/api/account/block", () => {}); // TODO
// -- Auth Routes -- //
router
.get("/api/auth", () => {}) // TODO
.get("/api/auth/verify", () => {}) // TODO
.get("/api/auth/refresh", () => {}); // TODO
// -- User routes -- //
router.get("/api/users", api_getAllUsers);
// .get("/api/user/:id/info", api_user_getInfo);
// -- Chat routes -- //
router
.get("/api/chats", api_getChats)
.get("/api/chat/:id", api_getChatById)
.post("/api/chat/create", api_createChat)
.post("/api/chat/:id/message", api_sendMessage)
.delete("/api/chat/:id", api_deleteChat);
// -- Post routes -- //
router
.get("/api/posts", api_posts_getAll)
.get("/api/post/:id", api_getPostById)
.post("/api/post/create", api_createPost)
.put("/api/post/:id", api_updatePost)
.delete("/api/post/:id", api_deletePost)
.post("/api/post/:id/like", api_likePost);
// -- Comment Routes -- //
router
.get("/api/post/:id/comments", api_getPostComments)
.post("/api/post/:id/comment", api_createComment)
.put("/api/comment/:id", api_updateComment)
.delete("/api/comment/:id", api_deleteComment)
.post("/api/comment/:id/like", api_likeComment);
// +++ FUNCTIONS ----------------------------------------------------- //
// ABANDONED FUNCTIONS //
async function _authenticator(ctx: Context, next: Next): Promise<void> {
const authHeader = ctx.request.headers.get("Authorization");
if (!authHeader) {
ctx.response.status = 401;
ctx.response.body = { error: "No header" };
return;
}
// Bearer check
// Bearer is often used for authentication in API's and is a standard, I check it using RegEx (Regular Expressions)
const match = authHeader.match(/^Bearer (.+)$/);
if (!match) {
ctx.response.status = 401;
ctx.response.body = { error: "Invalid format" };
return;
}
const token = match[1];
try {
// Token logic missing, unattempted.
await next();
} catch (error) {
ctx.response.status = 401;
ctx.response.body = { error: "Invalid token" };
}
}
async function _tokenChecker(ctx: Context, next: Next): Promise<void> {
// logic below (TODO):
/**
* 1. check if the token is valid
* 2. if valid, set the user in the context
* 3. if not valid, return 401
* 4. if: token missing, expired, blacklisted, !DB, !user or !correct user, return 401 with associated error
* eg: wrong user: 401 -> "Token not associated with this user"
*/
}
// API: Posts //
async function api_posts_getAll(ctx: Context): Promise<void> {
const posts = await db_utils.getPostsFromDB();
ctx.response.body = posts;
}
// API: login/register //
async function api_register(ctx: Context): Promise<void> {
try {
const body = ctx.request.body;
const result = await body.json();
const {
username,
password,
userGroup,
displayname,
user_email,
firstname,
surname,
} = result;
// Claude 3-5 Sonnet was used for the first Date formatting
const account_created = `${Math.floor(Date.now() / 1000)}-${
new Date()
.toLocaleDateString("en-GB")
.split("/")
.join("-")
}`;
if (
!username ||
!password ||
!userGroup ||
!displayname ||
!user_email ||
!firstname ||
!surname
) {
helper_utils.errorResponse(ctx, 400, "Missing required fields");
return;
}
// First salt the password
const { saltedPassword, salt } = await helper_utils.saltPassword(password);
// Then hash the salted password
const hash = await helper_utils.hashPassword(saltedPassword);
const userId = db_utils.registerUser(
username,
hash,
salt,
userGroup,
displayname,
user_email,
firstname,
surname,
account_created,
);
const user = await db_utils.getUserByUsername(username);
const responseBody: any = {
success: true,
message: "Register successful",
};
if (user.user_id !== undefined) {
responseBody.userId = user.user_id;
}
helper_utils.sendResponse(ctx, {
status: 200,
body: responseBody,
});
} catch (error) {
console.log(error);
helper_utils.errorResponse(ctx, 500, "Invalid request");
return;
}
}
async function api_login(ctx: Context): Promise<string> {
try {
const body = ctx.request.body;
const result = await body.json();
const { username, password } = result;
if (!username || !password) {
helper_utils.errorResponse(ctx, 400, "Missing required fields");
return "Error";
}
const user = await db_utils.getUserByUsername(username);
if (!user) {
helper_utils.errorResponse(ctx, 404, "User not found");
return "Error";
}
const storedSalt = user.password_salt;
const saltedPassword = `${password}${storedSalt}`;
const hash = await helper_utils.hashPassword(saltedPassword);
// Compare the hashed password with the stored hash
if (user.password !== hash) {
helper_utils.errorResponse(ctx, 401, "Invalid password");
return "Error";
}
// Return success with the user ID if it exists
const responseBody: any = {
success: true,
message: "Login successful",
};
// Only add userId if it exists
if (user.user_id !== undefined) {
responseBody.userId = user.user_id;
}
helper_utils.sendResponse(ctx, {
status: 200,
body: responseBody,
});
app.use(oakCors());
return "Success";
} catch (error) {
console.log(error);
helper_utils.errorResponse(ctx, 500, "Invalid request");
return "Error";
}
}
// +++ APP ---------------------------------------------------------- //
app.use(
oakCors({
origin: "*",
credentials: true,
}),
);
app.use(router.routes());
app.use(router.allowedMethods());
// @ts-ignore: start app
export { app };
await app.listen({ port: 8000 });

107
database/create_db.ts Normal file
View File

@@ -0,0 +1,107 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description This file is responsible for creating the database and the tables
*/
// +++ IMPORTS ------------------------------------------------------ //
import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";
import {
dirname,
fromFileUrl,
join,
} from "https://deno.land/std@0.224.0/path/mod.ts";
// +++ VARIABLES ---------------------------------------------------- //
const _dirname: string = dirname(fromFileUrl(import.meta.url));
const dbPath: string = join(_dirname, "../database/esp-projekt.sqlite");
const db = new DB(dbPath);
export function createDatabase(): void {
db.execute(`
CREATE TABLE IF NOT EXISTS accounts (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_group TEXT,
bio TEXT,
displayname TEXT,
username TEXT,
user_email TEXT,
password TEXT,
password_salt TEXT,
firstname TEXT,
surname TEXT,
account_created TEXT,
blocked_users TEXT,
followers TEXT,
following TEXT,
contacts TEXT
);
CREATE TABLE IF NOT EXISTS posts (
posts_uuid INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
created_at TEXT,
post_text TEXT,
post_type TEXT,
likes INTEGER,
comments INTEGER
);
CREATE TABLE IF NOT EXISTS comments (
comment_id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER,
author_user_id INTEGER,
date_created_at TEXT,
text TEXT,
likes INTEGER
);
CREATE TABLE IF NOT EXISTS messages (
message_id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER,
sender_id INTEGER,
content TEXT,
timestamp TEXT,
FOREIGN KEY (chat_id) REFERENCES chats (chat_id),
FOREIGN KEY (sender_id) REFERENCES accounts (user_id)
);
CREATE TABLE IF NOT EXISTS chats (
chat_id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_name TEXT,
participants TEXT,
created_at TEXT
)
`);
}
// Sample data generated using AI, does not work yet and will be adjusted
export function insertSampleData(): void {
db.query(
`INSERT INTO accounts (user_group, bio, displayname, username, user_email, password, password_salt, firstname, surname, account_created, blocked_users, followers, following, contacts) VALUES
('admin', 'Admin bio', 'Admin User', 'admin', 'admin@example.com', 'pw1', 'salt1', 'Admin', 'User', '2024-01-01', '[]', '[]', '[]', '[]'),
('user', 'I love coding!', 'John Dev', 'johndev', 'john@example.com', 'pw2', 'salt2', 'John', 'Smith', '2024-01-02', '[]', '[]', '[3,4]', '[1,2]'),
('user', 'Photography enthusiast', 'Alice', 'alice_photo', 'alice@example.com', 'pw3', 'salt3', 'Alice', 'Johnson', '2024-01-03', '[]', '[5]', '[1]', '[2]')
`,
);
db.query(
`INSERT INTO posts (user_id, created_at, post_text, post_type, likes, comments) VALUES
(1, '2024-01-15 10:00:00', 'First post about programming!', 'text', 5, 2),
(1, '2024-01-15 11:30:00', 'Check out this new feature', 'text', 10, 3),
(2, '2024-01-16 09:15:00', 'Just learned about TypeScript', 'text', 8, 1),
(3, '2024-01-16 14:20:00', 'Posted my new photo collection', 'image', 15, 4)
`,
);
db.query(
`INSERT INTO comments (post_id, author_user_id, date_created_at, text, likes) VALUES
(1, 2, '2024-01-15 10:05:00', 'Great post!', 3),
(1, 3, '2024-01-15 10:10:00', 'Very informative', 2),
(2, 3, '2024-01-15 11:35:00', 'Nice feature', 4),
(3, 1, '2024-01-16 09:20:00', 'TypeScript is awesome', 5),
(4, 2, '2024-01-16 14:25:00', 'Beautiful photos!', 6)
`,
);
}

View File

Binary file not shown.

View File

@@ -0,0 +1,61 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description This file is responsible for making Interfaces accessible by every file, deduplicating it.
* @file interfaces.ts
*/
interface Post {
posts_uuid: number;
user_id: number;
created_at: string;
post_text: string;
post_type: string;
likes: number;
comments: number;
}
interface Accounts {
user_id: number;
user_group: string;
bio: string;
displayname: string;
username: string;
user_email: string;
password: string;
password_salt: string;
firstname: string;
surname: string;
account_created: string;
blocked_users: string;
followers: string;
following: string;
contacts: string;
}
interface Comments {
comment_id: number;
post_id: number;
author_user_id: number;
date_created_at: string;
text: string;
likes: number;
}
interface Chat {
chat_id: number;
chat_name: string;
participants: string;
created_at: string;
}
interface Message {
message_id: number;
chat_id: number;
sender_id: number;
content: string;
timestamp: string;
}
export type { Accounts, Chat, Comments, Message, Post };

View File

@@ -0,0 +1,122 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description This file makes accessing the Database easier by creating a map for each type of data
* @file maphelper.ts
*/
// +++ IMPORTS ------------------------------------------------------ //
import { Row } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";
import {
Accounts,
Chat,
Comments,
Message,
Post,
} from "../helpers/interfaces.ts";
function mapPostRow(row: Row): Post {
const [
posts_uuid,
user_id,
created_at,
post_text,
post_type,
likes,
comments,
] = row;
return {
posts_uuid: Number(posts_uuid),
user_id: Number(user_id),
created_at: String(created_at),
post_text: String(post_text),
post_type: String(post_type),
likes: Number(likes),
comments: Number(comments),
};
}
function mapCommentRow(row: Row): Comments {
const [
comment_id,
post_id,
author_user_id,
date_created_at,
text,
likes,
] = row;
return {
comment_id: Number(comment_id),
post_id: Number(post_id),
author_user_id: Number(author_user_id),
date_created_at: String(date_created_at),
text: String(text),
likes: Number(likes),
};
}
function mapChatRow(row: Row): Chat {
const [chat_id, chat_name, participants, created_at] = row;
return {
chat_id: Number(chat_id),
chat_name: String(chat_name),
participants: String(participants),
created_at: String(created_at),
};
}
function mapMessageRow(row: Row): Message {
const [message_id, chat_id, sender_id, content, timestamp] = row;
return {
message_id: Number(message_id),
chat_id: Number(chat_id),
sender_id: Number(sender_id),
content: String(content),
timestamp: String(timestamp),
};
}
function mapAccountRow(row: Row): Accounts {
const [
user_id,
user_group,
bio,
displayname,
username,
user_email,
password,
password_salt,
firstname,
surname,
account_created,
blocked_users,
followers,
following,
contacts,
] = row;
return {
user_id: Number(user_id),
user_group: String(user_group),
bio: String(bio),
displayname: String(displayname),
username: String(username),
user_email: String(user_email),
password: String(password),
password_salt: String(password_salt),
firstname: String(firstname),
surname: String(surname),
account_created: String(account_created),
blocked_users: String(blocked_users),
followers: String(followers),
following: String(following),
contacts: String(contacts),
};
}
export {
mapAccountRow,
mapChatRow,
mapCommentRow,
mapMessageRow,
mapPostRow
};

View File

@@ -0,0 +1,86 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description This file is responsible for creating Functions to easily access the Database, Specifically for Chats
* @file chatUtil.ts
*/
// +++ IMPORTS ------------------------------------------------------ //
import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";
import { Chat, Message } from "../interfaces.ts";
import { mapChatRow, mapMessageRow, queryDatabase } from "./mod.ts";
async function getUserChats(db: DB, userId: string): Promise<Chat[]> {
const query = `SELECT * FROM chats WHERE participants LIKE '%${userId}%'`;
return await queryDatabase<Chat>(db, query, [], mapChatRow);
}
async function getChatById(db: DB, chatId: string): Promise<Chat | null> {
const query = `SELECT * FROM chats WHERE chat_id = ?`;
const chats = await queryDatabase<Chat>(db, query, [chatId], mapChatRow);
return chats.length > 0 ? chats[0] : null;
}
async function getChatMessages(db: DB, chatId: string): Promise<Message[]> {
const query =
`SELECT * FROM messages WHERE chat_id = ? ORDER BY timestamp ASC`;
return await queryDatabase<Message>(db, query, [chatId], mapMessageRow);
}
async function createChat(
db: DB,
participants: string[],
chatName: string,
): Promise<string> {
const participantsJson = JSON.stringify(participants);
const timestamp = `${Math.floor(Date.now() / 1000)}-${
new Date().toLocaleDateString("en-GB").split("/").join("-")
}`;
const query = `
INSERT INTO chats (chat_name, participants, created_at)
VALUES (?, ?, ?)
`;
db.query(query, [chatName, participantsJson, timestamp]);
return db.lastInsertRowId.toString();
}
async function addMessageToChat(
db: DB,
chatId: string,
senderId: string,
content: string,
): Promise<string> {
const timestamp = `${Math.floor(Date.now() / 1000)}-${
new Date().toLocaleDateString("en-GB").split("/").join("-")
}`;
const query = `
INSERT INTO messages (chat_id, sender_id, content, timestamp)
VALUES (?, ?, ?, ?)
`;
db.query(query, [chatId, senderId, content, timestamp]);
return db.lastInsertRowId.toString();
}
async function deleteChat(db: DB, chatId: string): Promise<void> {
// First delete all messages in the chat
const deleteMessagesQuery = `DELETE FROM messages WHERE chat_id = ?`;
db.query(deleteMessagesQuery, [chatId]);
// Then delete the chat itself
const deleteChatQuery = `DELETE FROM chats WHERE chat_id = ?`;
db.query(deleteChatQuery, [chatId]);
}
export {
getUserChats,
getChatById,
getChatMessages,
createChat,
addMessageToChat,
deleteChat
};

View File

@@ -0,0 +1,81 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description This file is responsible for creating Functions to easily access the Database, Specifically for Comments
* @file commentUtil.ts
*/
import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";
import { Comments } from "../interfaces.ts";
import { mapCommentRow, queryDatabase } from "./mod.ts";
async function getCommentsFromDB(
db: DB,
post_id?: number,
): Promise<Comments[]> {
const query = post_id
? `SELECT * FROM comments WHERE post_id = ?`
: `SELECT * FROM comments`;
const params = post_id ? [post_id] : [];
return await queryDatabase<Comments>(db, query, params, mapCommentRow);
}
function createComment(
db: DB,
postId: string,
userId: string,
createdAt: string,
text: string,
): string {
const query = `
INSERT INTO comments (post_id, author_user_id, date_created_at, text, likes)
VALUES (?, ?, ?, ?, 0)
`;
db.query(query, [postId, userId, createdAt, text]);
const updatePostQuery =
`UPDATE posts SET comments = comments + 1 WHERE posts_uuid = ?`;
db.query(updatePostQuery, [postId]);
return db.lastInsertRowId.toString();
}
async function updateComment(
db: DB,
commentId: string,
text: string,
): Promise<void> {
const query = `UPDATE comments SET text = ? WHERE comment_id = ?`;
db.query(query, [text, commentId]);
}
async function deleteComment(db: DB, commentId: string): Promise<void> {
const getPostIdQuery = `SELECT post_id FROM comments WHERE comment_id = ?`;
const result = db.query(getPostIdQuery, [commentId]);
const postId: any = result[0][0];
const deleteCommentQuery = `DELETE FROM comments WHERE comment_id = ?`;
db.query(deleteCommentQuery, [commentId]);
const updatePostQuery =
`UPDATE posts SET comments = comments - 1 WHERE posts_uuid = ?`;
db.query(updatePostQuery, [postId]);
}
async function likeComment(
db: DB,
commentId: string,
userId: string,
): Promise<void> {
const query = `UPDATE comments SET likes = likes + 1 WHERE comment_id = ?`;
db.query(query, [commentId]);
}
export {
createComment,
deleteComment,
getCommentsFromDB,
likeComment,
updateComment,
};

View File

@@ -0,0 +1,46 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description Core Utility for querying the Database and inserting Sample Data
* @file core_utils.ts
*/
// +++ IMPORTS ------------------------------------------------------ //
import { DB, Row } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";
import * as db_create from "../../create_db.ts";
// +++ Helper ------------------------------------------------------ //
// "T" is a generic type, it can be anything and makes the function "flexible"(?)
async function queryDatabase<T>(
db: DB,
query: string,
params: any[],
mapRow: (row: Row) => T,
): Promise<T[]> {
const results: T[] = [];
try {
const rows = await db.query(query, params);
for (const row of rows) {
results.push(mapRow(row));
}
} catch (error) {
console.error("Database query error:", error);
}
return results;
}
// +++ FUNCTIONS --------------------------------------------------- //
/**
* See:
* @file ./create_db.ts
*/
function insertSamples(): void {
db_create.insertSampleData();
}
function createDatabaseIfNotExist(): void {
db_create.createDatabase();
}
export { queryDatabase, insertSamples, createDatabaseIfNotExist };

View File

@@ -0,0 +1,13 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description A mod file is used to export all the functions in the folder, making them easier to access.
* @file mod.ts
*/
export * from "./chat_utils.ts";
export * from "./comment_utils.ts";
export * from "./post_utils.ts";
export * from "./user_utils.ts";
export * from "./core_utils.ts"
export * from "../maphelper.ts";

View File

@@ -0,0 +1,128 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description This file is responsible for creating Functions to easily access the Database, Specifically for Posts
* @file postUtil.ts
*/
// +++ IMPORTS ------------------------------------------------------ //
import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";
import { Post } from "../interfaces.ts";
import { mapPostRow, queryDatabase } from "./mod.ts";
async function getPostsFromDB(db: DB, user_uuid?: string): Promise<Post[]> {
const query = user_uuid
? `SELECT * FROM posts WHERE user_id = ?`
: `SELECT * FROM posts`;
const params = user_uuid ? [user_uuid] : [];
return await queryDatabase<Post>(db, query, params, mapPostRow);
}
async function getPostById(db: DB, postId: string): Promise<Post | null> {
const query = `SELECT * FROM posts WHERE posts_uuid = ?`;
const posts = await queryDatabase<Post>(db, query, [postId], mapPostRow);
return posts.length > 0 ? posts[0] : null;
}
async function createPost(
db: DB,
userId: string,
createdAt: string,
postText: string,
postType: string,
): Promise<string> {
const query = `
INSERT INTO posts (user_id, created_at, post_text, post_type, likes, comments)
VALUES (?, ?, ?, ?, 0, 0)
`;
db.query(query, [userId, createdAt, postText, postType]);
return db.lastInsertRowId.toString();
}
async function updatePost(
db: DB,
postId: string,
postText?: string,
postType?: string,
): Promise<void> {
let query = `UPDATE posts SET `;
const params: any[] = [];
if (postText) {
query += `post_text = ?`;
params.push(postText);
if (postType) {
query += `, post_type = ?`;
params.push(postType);
}
} else if (postType) {
query += `post_type = ?`;
params.push(postType);
}
query += ` WHERE posts_uuid = ?`;
params.push(postId);
db.query(query, params);
}
// This function deletes the comments on the post first, then
// deletes the post to avoid errors
async function deletePost(db: DB, postId: string): Promise<void> {
const deleteCommentsQuery = `DELETE FROM comments WHERE post_id = ?`;
db.query(deleteCommentsQuery, [postId]);
const deletePostQuery = `DELETE FROM posts WHERE posts_uuid = ?`;
db.query(deletePostQuery, [postId]);
}
// This is simplified and doesn't work as it would in a real application
// or website like twitter, this only exists as a test
async function likePost(db: DB, postId: string, userId: string): Promise<void> {
const query = `UPDATE posts SET likes = likes + 1 WHERE posts_uuid = ?`;
db.query(query, [postId]);
}
/**
* @param posts_to_filter The Posts in an array to filter
* @param post_types The types of Posts to filter for
* @returns Array of Posts
*/
// Filter functions merged to one
function filterPosts(posts_to_filter: Post[], post_types: string[]): Post[] {
if (post_types.length === 0) {
return posts_to_filter;
}
// Set is a type that we can specify the values to
// example: const set = new Set<number>();
// set.add(1); Will work
// set.add("2"); Will not work, it will error because
// it is a string.
const includedPostIds = new Set<number>();
const result: Post[] = [];
for (const type of post_types) {
for (const post of posts_to_filter) {
if (!includedPostIds.has(post.posts_uuid) && post.post_type === type) {
result.push(post);
includedPostIds.add(post.posts_uuid);
}
}
}
return result;
}
export {
getPostsFromDB,
getPostById,
createPost,
updatePost,
deletePost,
likePost,
filterPosts,
};

View File

@@ -0,0 +1,103 @@
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description This file is responsible for creating Functions to easily access the Database, Specifically for Users
* @file userUtil.ts
*/
// +++ IMPORTS ------------------------------------------------------ //
import { DB, Row } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";
import { mapAccountRow, queryDatabase } from "./mod.ts";
import { Accounts } from "../interfaces.ts";
/**
* @param user The username of the User to add
* @param password The hashed password of the User to add
* @param salt The salt used for the password
* @returns "noUser" if user exists, "newUser" if registration successful
*/
function registerUser(
db: DB,
user: string,
password: string,
salt: string,
userGroup: string,
displayname: string,
user_email: string,
firstname: string,
surname: string,
account_created: string,
): any {
const query_user_exists =
`SELECT * FROM accounts WHERE displayname = '${user}'`;
if (!query_user_exists) {
return "noUser";
}
const query_add_user = `
INSERT INTO accounts (
username,
password,
password_salt,
user_group,
displayname,
user_email,
firstname,
surname,
account_created,
bio,
blocked_users,
followers,
following,
contacts
) VALUES (
'${user}',
'${password}',
'${salt}',
'${userGroup}',
'${displayname}',
'${user_email}',
'${firstname}',
'${surname}',
'${account_created}',
'',
'[]',
'[]',
'[]',
'[]'
)`;
db.query(query_add_user);
const userId = db.query(
`SELECT user_id FROM accounts WHERE displayname = '${user}'`,
);
console.log(`New user: ${user}`);
return userId;
}
/**
* @returns Array of all Users in the Database
*/
async function getAllUsersFromDB(db: DB): Promise<Accounts[]> {
const query = `SELECT * FROM accounts`;
return await queryDatabase<Accounts>(db, query, [], mapAccountRow);
}
/**
* @param username
* @returns Returns the Accounts for the User with the given username
*/
async function getUserByUsername(db: DB, username: string): Promise<Accounts> {
const query = `SELECT * FROM accounts WHERE username = '${username}'`;
const params: string[] = [];
const result = await queryDatabase<Accounts>(
db,
query,
params,
mapAccountRow,
);
return result[0];
}
export { getAllUsersFromDB, getUserByUsername, registerUser };

View File

@@ -1,138 +1,203 @@
import { DB } from "https://deno.land/x/sqlite/mod.ts";
import { dirname, fromFileUrl, join } from "https://deno.land/std/path/mod.ts";
/// <reference lib="deno.ns" />
/**
* @author Esad Mustafoski
* @description This file is responsible for creating Functions to easily access the Database, Intended for use in the API
*/
// __dirname Is never getting used again, It's only needed because the DB Import
// from SQLite doesn't like relative paths, so I use this as
// +++ IMPORTS ------------------------------------------------------ //
import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";
import {
dirname,
fromFileUrl,
join,
} from "https://deno.land/std@0.224.0/path/mod.ts";
import * as db_create from "./create_db.ts";
// Import all internal utilities with renamed imports to avoid naming conflicts
import {
addMessageToChat as addMessageToChatInternal,
createChat as createChatInternal,
// getCommentsForComments as getCommentsForCommentsInternal,
// Accidentally deleted function...
createComment as createCommentInternal,
createPost as createPostInternal,
deleteChat as deleteChatInternal,
deleteComment as deleteCommentInternal,
deletePost as deletePostInternal,
filterPosts,
// --- Account Functions --- //
getAllUsersFromDB as getAllUsersFromDBInternal,
getChatById as getChatByIdInternal,
getChatMessages as getChatMessagesInternal,
// --- Comment Functions --- //
getCommentsFromDB as getCommentsFromDBInternal,
getPostById as getPostByIdInternal,
// --- Post Functions --- //
getPostsFromDB as getPostsFromDBInternal,
getUserByUsername as getUserByUsernameInternal,
// --- Chat Functions --- //
getUserChats as getUserChatsInternal,
likeComment as likeCommentInternal,
likePost as likePostInternal,
// --- Mapper Functions --- //
queryDatabase as queryDatabaseInternal,
// getAllUserInfoByID as getAllUserInfoByIDInternal,
// Accidentally deleted function...
registerUser as registerUserInternal,
updateComment as updateCommentInternal,
updatePost as updatePostInternal,
} from "./helpers/utils/mod.ts";
// +++ VARIABLES ---------------------------------------------------- //
// _dirname Is never getting used again, It's only needed because the DB Import
// from SQLite doesn't like relative paths, so I use this as
// A Workaround
const __dirname:string = dirname(fromFileUrl(import.meta.url));
const dbPath:string = join(__dirname, "../database/esp-projekt.sqlite");
const _dirname: string = dirname(fromFileUrl(import.meta.url));
const dbPath: string = join(_dirname, "../database/esp-projekt.sqlite");
const db = new DB(dbPath);
// Interfaces
interface Post {
posts_uuid: number;
user_id: number;
created_at: string;
post_text: string;
likes: number;
comments: number;
// +++ INTERFACES --------------------------------------------------- //
// Only re-export interfaces that are needed by external code
export type {
Accounts,
Chat,
Comments,
Message,
Post,
} from "./helpers/interfaces.ts";
// +++ HELPER FUNCTIONS --------------------------------------------- //
export function queryDatabase<T>(
query: string,
params: any[],
mapRow: (row: any) => T,
): Promise<T[]> {
return queryDatabaseInternal(db, query, params, mapRow);
}
async function getPostsFromDB() {
let dataresult: Array<Post> = [];
// +++ DATABASE INITIALIZATION -------------------------------------- //
export async function ensureDatabaseExists(): Promise<void> {
try {
// Check if the database directory exists, create it if not
const dbDir = dirname(dbPath);
try {
const rows = await db.query("SELECT * FROM posts");
// Assuming `db.query` returns an array of arrays or tuples
for (const row of rows) {
const [posts_uuid, user_id, created_at, post_text, likes, comments] = row;
dataresult.push({
posts_uuid: Number(posts_uuid), // Convert to string if necessary
user_id: Number(user_id),
created_at: String(created_at), // Convert to Date if necessary
post_text: String(post_text),
likes: Number(likes), // Convert to number if necessary
comments: Number(comments), // Convert to number if necessary
});
}
await Deno.stat(dbDir);
} catch (error) {
console.error("Error fetching posts", error);
if (error instanceof Deno.errors.NotFound) {
// Create the database directory
await Deno.mkdir(dbDir, { recursive: true });
console.log(`Created database directory: ${dbDir}`);
} else {
throw error;
}
}
return dataresult;
}
// Test Function, not useful
async function countPosts(): Promise<number> {
let count = 0;
// Check if the database file exists
try {
for (const [c] of await db.query("SELECT COUNT(*) FROM posts")) {
count = c as number;
}
await Deno.stat(dbPath);
console.log("Database file already exists");
} catch (error) {
console.error("Error counting posts:", error);
if (error instanceof Deno.errors.NotFound) {
createDatabaseIfNotExist();
insertSamples();
// Nothing, file will be created below
} else {
throw error;
}
}
console.log("Total posts:", count);
return count;
} catch (error) {
console.error("Error ensuring database exists:", error);
throw error;
}
}
function getCommentsForPost(postid: bigint) {
export function createDatabaseIfNotExist(): void {
db_create.createDatabase();
}
function getCommentsForComments(commentid: bigint) {
export function insertSamples(): void {
db_create.insertSampleData();
}
function getAllUsers() {
const users = [];
for (
const [
user_id,
user_group,
bio,
displayname,
username,
user_email,
password,
firstname,
surname,
account_created,
blocked_users,
followers,
following,
contacts,
] of db.query("SELECT * FROM Accounts")
) {
users.push({
user_id,
user_group,
bio,
displayname,
username,
user_email,
password,
firstname,
surname,
account_created,
blocked_users,
followers,
following,
contacts,
});
}
return users;
}
// +++ ACCOUNT FUNCTIONS -------------------------------------------- //
export const getAllUsersFromDB = () => getAllUsersFromDBInternal(db);
export const getUserByUsername = (username: string) =>
getUserByUsernameInternal(db, username);
// export const getAllUserInfoByID = (user_id: string) => getAllUserInfoByIDInternal(db, user_id);
export const registerUser = (
user: string,
password: string,
salt: string,
userGroup: string,
displayname: string,
user_email: string,
firstname: string,
surname: string,
account_created: string,
) =>
registerUserInternal(
db,
user,
password,
salt,
userGroup,
displayname,
user_email,
firstname,
surname,
account_created,
);
function getUserByID(userid: bigint) {
}
// +++ POST FUNCTIONS ----------------------------------------------- //
export const getPostsFromDB = (user_uuid?: string) =>
getPostsFromDBInternal(db, user_uuid);
export const getPostById = (postId: string) => getPostByIdInternal(db, postId);
export const createPost = (
userId: string,
createdAt: string,
postText: string,
postType: string,
) => createPostInternal(db, userId, createdAt, postText, postType);
export const updatePost = (
postId: string,
postText?: string,
postType?: string,
) => updatePostInternal(db, postId, postText, postType);
export const deletePost = (postId: string) => deletePostInternal(db, postId);
export const likePost = (postId: string, userId: string) =>
likePostInternal(db, postId, userId);
function getAllPostsFromUser() {
}
// +++ COMMENT FUNCTIONS -------------------------------------------- //
export const getCommentsFromDB = (post_id?: number) =>
getCommentsFromDBInternal(db, post_id);
// export const getCommentsForComments = (comment_id: number) => getCommentsForCommentsInternal(db, comment_id);
export const createComment = (
postId: string,
userId: string,
createdAt: string,
text: string,
) => createCommentInternal(db, postId, userId, createdAt, text);
export const updateComment = (commentId: string, text: string) =>
updateCommentInternal(db, commentId, text);
export const deleteComment = (commentId: string) =>
deleteCommentInternal(db, commentId);
export const likeComment = (commentId: string, userId: string) =>
likeCommentInternal(db, commentId, userId);
// Filter Functions
function filterForImagePosts() {
return [];
}
// +++ CHAT FUNCTIONS ----------------------------------------------- //
export const getUserChats = (userId: string) =>
getUserChatsInternal(db, userId);
export const getChatById = (chatId: string) => getChatByIdInternal(db, chatId);
export const getChatMessages = (chatId: string) =>
getChatMessagesInternal(db, chatId);
export const createChat = (participants: string[], chatName: string) =>
createChatInternal(db, participants, chatName);
export const addMessageToChat = (
chatId: string,
senderId: string,
content: string,
) => addMessageToChatInternal(db, chatId, senderId, content);
export const deleteChat = (chatId: string) => deleteChatInternal(db, chatId);
function filterForVideoPosts() {
return [];
}
function filterForTextPosts() {
return [];
}
// Export all Functions to make this a module
export {
getPostsFromDB,
countPosts,
getCommentsForPost,
getCommentsForComments,
getAllUsers,
getUserByID,
getAllPostsFromUser,
filterForImagePosts,
filterForVideoPosts,
filterForTextPosts
};
// +++ UTILITY FUNCTIONS -------------------------------------------- //
export { filterPosts };

View File

@@ -1,6 +1,6 @@
{
"tasks": {
"dev": "deno task dev:api && deno task dev:vite",
"dev": "deno task dev:api & deno task dev:vite",
"dev:api": "deno run -A api/main.ts",
"dev:vite": "deno run -A npm:vite",
"build": "deno run -A --node-modules-dir npm:vite build",

168
deno.lock generated
View File

@@ -6,6 +6,7 @@
"jsr:@std/bytes@1": "1.0.2",
"jsr:@std/bytes@^1.0.2": "1.0.2",
"jsr:@std/crypto@1": "1.0.3",
"jsr:@std/encoding@*": "1.0.5",
"jsr:@std/encoding@1": "1.0.5",
"jsr:@std/encoding@^1.0.5": "1.0.5",
"jsr:@std/http@1": "1.0.9",
@@ -1112,14 +1113,101 @@
}
},
"redirects": {
"https://deno.land/std/assert/mod.ts": "https://deno.land/std@0.224.0/assert/mod.ts",
"https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts",
"https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.224.0/testing/asserts.ts",
"https://deno.land/x/cors/mod.ts": "https://deno.land/x/cors@v1.2.2/mod.ts",
"https://deno.land/x/oak/mod.ts": "https://deno.land/x/oak@v17.1.2/mod.ts",
"https://deno.land/x/sqlite/mod.ts": "https://deno.land/x/sqlite@v3.9.1/mod.ts"
"https://deno.land/x/sqlite/mod.ts": "https://deno.land/x/sqlite@v3.9.1/mod.ts",
"https://deno.land/x/superoak/mod.ts": "https://deno.land/x/superoak@4.8.1/mod.ts"
},
"remote": {
"https://deno.land/std@0.115.0/_wasm_crypto/crypto.js": "1c565287b35f6eb1aa58499d0f4fbac99a9c30eb9a735c512d193a6493499e84",
"https://deno.land/std@0.115.0/_wasm_crypto/crypto.wasm.js": "e93d38b215c05c552669e9565654599b13e7898ebd3f10ac91e39413efda4b84",
"https://deno.land/std@0.115.0/_wasm_crypto/mod.ts": "9afe300945fe7e5bcec231b52b0016d8442d026b823f619bb5939b2cf66ff21b",
"https://deno.land/std@0.115.0/crypto/mod.ts": "8e1ec0ff94a4f08e3c4f72de5b88781566481602614e62291aa7ae7444ba11f0",
"https://deno.land/std@0.115.0/encoding/base64.ts": "0b58bd6477214838bf711eef43eac21e47ba9e5c81b2ce185fe25d9ecab3ebb3",
"https://deno.land/std@0.115.1/async/deadline.ts": "1d6ac7aeaee22f75eb86e4e105d6161118aad7b41ae2dd14f4cfd3bf97472b93",
"https://deno.land/std@0.115.1/async/debounce.ts": "b2f693e4baa16b62793fd618de6c003b63228db50ecfe3bd51fc5f6dc0bc264b",
"https://deno.land/std@0.115.1/async/deferred.ts": "ab60d46ba561abb3b13c0c8085d05797a384b9f182935f051dc67136817acdee",
"https://deno.land/std@0.115.1/async/delay.ts": "f2d8ccaa8ebc26594bd8b0989edfd8a96257a714c1dee2fb54d986e5bdd840ac",
"https://deno.land/std@0.115.1/async/mod.ts": "78425176fabea7bd1046ce3819fd69ce40da85c83e0f174d17e8e224a91f7d10",
"https://deno.land/std@0.115.1/async/mux_async_iterator.ts": "62abff3af9ff619e8f2adc96fc70d4ca020fa48a50c23c13f12d02ed2b760dbe",
"https://deno.land/std@0.115.1/async/pool.ts": "353ce4f91865da203a097aa6f33de8966340c91b6f4a055611c8c5d534afd12f",
"https://deno.land/std@0.115.1/async/tee.ts": "3e9f2ef6b36e55188de16a667c702ace4ad0cf84e3720379160e062bf27348ad",
"https://deno.land/std@0.115.1/fmt/colors.ts": "8368ddf2d48dfe413ffd04cdbb7ae6a1009cf0dccc9c7ff1d76259d9c61a0621",
"https://deno.land/std@0.115.1/http/http_status.ts": "2ff185827bff21c7be2807fcb09a6a2166464ba57fcd94afe805abab8e09070a",
"https://deno.land/std@0.115.1/http/server.ts": "46f616eac1ca0ea7b9fce97102d185a3d97ae7d7d3bbd635b74cefe05ed1cb37",
"https://deno.land/std@0.115.1/testing/_diff.ts": "e6a10d2aca8d6c27a9c5b8a2dbbf64353874730af539707b5b39d4128140642d",
"https://deno.land/std@0.115.1/testing/asserts.ts": "a1fef0239a2c343b0baa49c77dcdd7412613c46f3aba2887c331a2d7ed1f645e",
"https://deno.land/std@0.213.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.213.0/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840",
"https://deno.land/std@0.213.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
"https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
"https://deno.land/std@0.213.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f",
"https://deno.land/std@0.213.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1",
"https://deno.land/std@0.213.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
"https://deno.land/std@0.213.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
"https://deno.land/std@0.213.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769",
"https://deno.land/std@0.213.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c",
"https://deno.land/std@0.213.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219",
"https://deno.land/std@0.213.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444",
"https://deno.land/std@0.213.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
"https://deno.land/std@0.213.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005",
"https://deno.land/std@0.213.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0",
"https://deno.land/std@0.213.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1",
"https://deno.land/std@0.213.0/assert/assert_not_equals.ts": "f3edda73043bc2c9fae6cbfaa957d5c69bbe76f5291a5b0466ed132c8789df4c",
"https://deno.land/std@0.213.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931",
"https://deno.land/std@0.213.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f",
"https://deno.land/std@0.213.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be",
"https://deno.land/std@0.213.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49",
"https://deno.land/std@0.213.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54",
"https://deno.land/std@0.213.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366",
"https://deno.land/std@0.213.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7",
"https://deno.land/std@0.213.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7",
"https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
"https://deno.land/std@0.213.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
"https://deno.land/std@0.213.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c",
"https://deno.land/std@0.213.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b",
"https://deno.land/std@0.213.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd",
"https://deno.land/std@0.213.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145",
"https://deno.land/std@0.213.0/async/delay.ts": "8e1d18fe8b28ff95885e2bc54eccec1713f57f756053576d8228e6ca110793ad",
"https://deno.land/std@0.213.0/fmt/colors.ts": "aeaee795471b56fc62a3cb2e174ed33e91551b535f44677f6320336aabb54fbb",
"https://deno.land/std@0.213.0/http/server.ts": "6dce295abc169d0956ae00432441331b3425afad4d79e8b3475739be2f04d614",
"https://deno.land/std@0.213.0/http/status.ts": "ed61b4882af2514a81aefd3245e8df4c47b9a8e54929a903577643d2d1ebf514",
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
"https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8",
"https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2",
"https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c",
@@ -1197,6 +1285,7 @@
"https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972",
"https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e",
"https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c",
"https://deno.land/std@0.224.0/testing/asserts.ts": "d0cdbabadc49cc4247a50732ee0df1403fdcd0f95360294ad448ae8c240f3f5c",
"https://deno.land/x/cors@v1.2.2/abcCors.ts": "cdf83a7eaa69a1bf3ab910d18b9422217902fac47601adcaf0afac5a61845d48",
"https://deno.land/x/cors@v1.2.2/attainCors.ts": "7d6aba0f942495cc31119604e0895c9bb8edd8f8baa7fe78e6c655bd0b4cbf59",
"https://deno.land/x/cors@v1.2.2/cors.ts": "0e2d9167e3685f9bcf48f565e312b6e1883fa458f7337e5ce7bc2e3b29767980",
@@ -1205,6 +1294,7 @@
"https://deno.land/x/cors@v1.2.2/oakCors.ts": "1348dc7673c61b85d2e80559a7b44f8e0246eaa6bcc6ec744fafe5d9b13b5c71",
"https://deno.land/x/cors@v1.2.2/opineCors.ts": "fb5790115c26b7061d84b8d6c17d258a1e241bcab75b0bc3ca1fdb2e57bc5072",
"https://deno.land/x/cors@v1.2.2/types.ts": "97546633ccc7f0df7a29bacba5d91dc6f61decdd1b65258300244dba905d34b8",
"https://deno.land/x/free_port@v1.2.0/mod.ts": "512646732aaea41fbfd1f210f3ae82660f38251777d189d290da331d0235a58e",
"https://deno.land/x/oak@v17.1.2/application.ts": "2bcc73b3f22a193554c9958f7080ea635db25594d25ff7944021fca5bf74adba",
"https://deno.land/x/oak@v17.1.2/body.ts": "0eb7ab9df44d1b79933463d596b5e1a4f0991c94cff591861e58a413bda3f3db",
"https://deno.land/x/oak@v17.1.2/context.ts": "345cfdaa5a2310558ee0863f2fba5f9ba648188412b16ce342c33266c085f5d3",
@@ -1234,6 +1324,10 @@
"https://deno.land/x/oak@v17.1.2/utils/resolve_path.ts": "aa39d54a003b38fee55f340a0cba3f93a7af85b8ddd5fbfb049a98fc0109b36d",
"https://deno.land/x/oak@v17.1.2/utils/streams.ts": "3da73b94681f8d27a82cc67df3f91090ec0bd6c3e9ab957af588d41ab585d923",
"https://deno.land/x/oak@v17.1.2/utils/type_guards.ts": "a8dbb5ab7424f0355b121537d2454f927e0ca9949262fb67ac4fbefbd5880313",
"https://deno.land/x/opine@1.9.1/src/methods.ts": "0481daecc6068d24e9e5391818baddf555ab803d39a465dcd259161f8bd8ee49",
"https://deno.land/x/opine@1.9.1/src/utils/mergeDescriptors.ts": "1fe498d4a1a8dcfd3570f9ca5e0647590d86d029b3c340bfcfdb57002851e41b",
"https://deno.land/x/opine@2.3.4/src/methods.ts": "0481daecc6068d24e9e5391818baddf555ab803d39a465dcd259161f8bd8ee49",
"https://deno.land/x/opine@2.3.4/src/utils/mergeDescriptors.ts": "1fe498d4a1a8dcfd3570f9ca5e0647590d86d029b3c340bfcfdb57002851e41b",
"https://deno.land/x/sqlite@v3.2.0/build/sqlite.js": "16fe819f3b40c0d2e100014ec922b7dcef32bc9a0c799a9ecd4f1ae104217c88",
"https://deno.land/x/sqlite@v3.2.0/build/vfs.js": "baff72655c0916c906327fe6703c6a47daa1346e55c2eaa2629bcd879a673c8d",
"https://deno.land/x/sqlite@v3.2.0/mod.ts": "0b3e066f61a149d5aa99a50e2c41c687eaa0713350d3e9bfbe4025173ec1c9a9",
@@ -1250,7 +1344,77 @@
"https://deno.land/x/sqlite@v3.9.1/src/error.ts": "f7a15cb00d7c3797da1aefee3cf86d23e0ae92e73f0ba3165496c3816ab9503a",
"https://deno.land/x/sqlite@v3.9.1/src/function.ts": "bc778cab7a6d771f690afa27264c524d22fcb96f1bb61959ade7922c15a4ab8d",
"https://deno.land/x/sqlite@v3.9.1/src/query.ts": "d58abda928f6582d77bad685ecf551b1be8a15e8e38403e293ec38522e030cad",
"https://deno.land/x/sqlite@v3.9.1/src/wasm.ts": "e79d0baa6e42423257fb3c7cc98091c54399254867e0f34a09b5bdef37bd9487"
"https://deno.land/x/sqlite@v3.9.1/src/wasm.ts": "e79d0baa6e42423257fb3c7cc98091c54399254867e0f34a09b5bdef37bd9487",
"https://deno.land/x/superdeno@4.7.0/deps.ts": "af7a0bc4c15710e0d7eb3e36b8ebf0f97debe394062ddf952c9f2b8160c6a590",
"https://deno.land/x/superdeno@4.7.0/mod.ts": "fa91c501867a4302a4bc92d63cbf934fe5475ebb7bf58335338e001147263c87",
"https://deno.land/x/superdeno@4.7.0/src/close.ts": "3d8bb8c24ab62cf6ce7aa9bff8ed35c0fcba5c9e4d90d0bf93fe2868a97b25e5",
"https://deno.land/x/superdeno@4.7.0/src/superagent.ts": "8f60187f9278b154ef6bccf09a5ff7d45f81103ad0ce02d45518a6bbe63ce764",
"https://deno.land/x/superdeno@4.7.0/src/superdeno.ts": "e27b490f34e170c5f103503c722293cefc351b1ac767246dfcb95d204e4a6953",
"https://deno.land/x/superdeno@4.7.0/src/test.ts": "d14892194d6911ba40de898884a50831468f47631a09178aef8fb61aa00ec1f6",
"https://deno.land/x/superdeno@4.7.0/src/types.ts": "0fc4a0a1acf4c3acba3e3d956a9e8917ce32cd47dbb8e75ffadec21a43a8e52f",
"https://deno.land/x/superdeno@4.7.0/src/utils.ts": "458c1699f73e348745b9b4d081e005dbc12d050fd2899ebf4f919515823ba1c0",
"https://deno.land/x/superdeno@4.7.0/src/xhrSham.js": "2db048613ca2fa2aa4ca9df2074411f8403ad4d88a8fb1e87313cee40e2c4a0d",
"https://deno.land/x/superdeno@4.7.0/version.ts": "4a24eb54df1fa633019ba3a4cae82032dc13c0e16bd9813a3aad95e1d5ea91e5",
"https://deno.land/x/superdeno@4.9.0/deps.ts": "acb88a5969aae0bcc82e053cb433cd183a10cc656495caa634b6e22a79156c4e",
"https://deno.land/x/superdeno@4.9.0/mod.ts": "fa91c501867a4302a4bc92d63cbf934fe5475ebb7bf58335338e001147263c87",
"https://deno.land/x/superdeno@4.9.0/src/close.ts": "8bd4ab602ebbb048d06697d0c48c30be5f78ab9ad673850965e8014d78cca7a8",
"https://deno.land/x/superdeno@4.9.0/src/superagent.ts": "8f60187f9278b154ef6bccf09a5ff7d45f81103ad0ce02d45518a6bbe63ce764",
"https://deno.land/x/superdeno@4.9.0/src/superdeno.ts": "2e2cd4898961ac7688f0c2a4b210bf560a338f6601bd231d74bf8a0956880311",
"https://deno.land/x/superdeno@4.9.0/src/test.ts": "1ab3c8c98160af8c3b30e097809d5c57bdd38d7b42c703f3f170f8452ad06c0f",
"https://deno.land/x/superdeno@4.9.0/src/types.ts": "9a48cdfafad3cea2212e1be29cdd2055e7d3d467437c9048012797323335abbb",
"https://deno.land/x/superdeno@4.9.0/src/utils.ts": "09a2e65cc5cc2a261b885f0e66ee84e96e978181975a0728636d20e48b67bd89",
"https://deno.land/x/superdeno@4.9.0/src/xhrSham.js": "6a35aed77bbe98324fe3b4d7430463b7cd6d3b43445ffdccd1fc327dc59dd3c6",
"https://deno.land/x/superdeno@4.9.0/version.ts": "4f8ba8f2a6b201e8e96818d3ab5c43aef1db751523c4b79160500664b72f87de",
"https://deno.land/x/superoak@4.5.0/deps.ts": "17bc881921ae2d3829d6e1168f29374de287c4d460d5288e6b5a6134e7508e5b",
"https://deno.land/x/superoak@4.5.0/mod.ts": "6d4ea8a5a48c9007f2e947934889c06259d3ebb5569515bcb0432036a22449cd",
"https://deno.land/x/superoak@4.5.0/src/superoak.ts": "9c08a3211c4d1f7bb89e88fc3f242536fce654c157aa6db52d3c24f033bb3d28",
"https://deno.land/x/superoak@4.5.0/version.ts": "af59786fbab2ab31ea2aa97927e98d8778407938bad4b531805b6166c064e847",
"https://deno.land/x/superoak@4.8.1/deps.ts": "d716c0b36fdac6458f6984ce80f69d0b645c7e0ac8461024a40ead5ed3fcd08d",
"https://deno.land/x/superoak@4.8.1/mod.ts": "6d4ea8a5a48c9007f2e947934889c06259d3ebb5569515bcb0432036a22449cd",
"https://deno.land/x/superoak@4.8.1/src/superoak.ts": "9c08a3211c4d1f7bb89e88fc3f242536fce654c157aa6db52d3c24f033bb3d28",
"https://deno.land/x/superoak@4.8.1/version.ts": "b9b71ac3596ff0a6aaad2bf9df8a54fb2925abd526800879e261de9c693812bc",
"https://jspm.dev/npm:call-bind@1.0.5!cjs": "09f8399c727fc1e9d58fdafc0a729b45bf37b7ee0c11d9d0b39abe37ac42ccf5",
"https://jspm.dev/npm:call-bind@1.0.5/callBound!cjs": "55fa05e2b115eeaef9ff684e3df12de253e6644a40ad09b5722f3a9a8df8f645",
"https://jspm.dev/npm:call-bind@1/callBound!cjs": "9cf2ef160025d392767618c2f0cb72d32cf14caa3fbeb493c6df9bde9d7fca8d",
"https://jspm.dev/npm:component-emitter@1!cjs": "26c2994a5fcac1cd9156b00be96c5e2f006dd76338095a96006ac3a47c6c327d",
"https://jspm.dev/npm:component-emitter@1.3.0!cjs": "757cafefb0bf5639f3f90b2267a7d168e03631e731c2a79fca847b735695e196",
"https://jspm.dev/npm:define-data-property@1!cjs": "37b65cb06c826730306a5f766de69da37b96076c96ea11a47667e9429623f937",
"https://jspm.dev/npm:define-data-property@1.1.1!cjs": "4ac6fa4b9d7ba7ccc83ffa350c58112ee878a450a97375217f66508d5673c822",
"https://jspm.dev/npm:fast-safe-stringify@2!cjs": "d8dd0803af23f037ffb44c13e18333131af27ebe582f55fd498b6e3c8f6d5a9d",
"https://jspm.dev/npm:fast-safe-stringify@2.1.1!cjs": "8a14a2de8a07a719c74aa63ffa5ff635fc55e9ee5d5a79fbc2e087dc4aa1940e",
"https://jspm.dev/npm:function-bind@1!cjs": "73fbc50bf85e8a6ca150609e98c396301c1ae5a1603e50ce8c64e95f646e0ce0",
"https://jspm.dev/npm:function-bind@1.1.2!cjs": "bbb663bc4e50f400a8ca0de9e0bfdaaa7022695f86b2806a48dc1afc5b4195a8",
"https://jspm.dev/npm:function-bind@1.1.2/implementation!cjs": "ebdc0ec85854db19d7e21081b368891394f86e21c6d786273c327762cb46ea6a",
"https://jspm.dev/npm:get-intrinsic@1!cjs": "f6d9266edc586632e8f6d8d6c5ca28fb2c0d5ee9c9d9252df9aafd57eda9fcea",
"https://jspm.dev/npm:get-intrinsic@1.2.2!cjs": "723fcebc493a45d5af8ecb366020a6cc2ce9bd4759bad699c1172015cb193f65",
"https://jspm.dev/npm:gopd@1!cjs": "c220469947b77de2c5e4b115beda16397bf6133c5b873b8e24e85b902ee6dc82",
"https://jspm.dev/npm:gopd@1.0.1!cjs": "b38da4f4b49cfef31e3aa8d62fdd136cf0fe99a5df6c603a426f97248f3cf4ab",
"https://jspm.dev/npm:has-property-descriptors@1!cjs": "b1a828f75a22a5614b136dd3da1be98cc744a2cd6bfed9bd8c338a8d51a570d1",
"https://jspm.dev/npm:has-property-descriptors@1.0.1!cjs": "f8da64823507f597f3cb44a2f3576e350df72e1033ef5e7a5b30d771e81c0819",
"https://jspm.dev/npm:has-proto@1!cjs": "78a2914e5525d531426c5d69fd5aa23671ec359c6c527b9791327f60ad1b6682",
"https://jspm.dev/npm:has-proto@1.0.1!cjs": "0a9d605f1d310f859265780011d6343a7869cadf3a9e02fd6cc949c2924b528c",
"https://jspm.dev/npm:has-symbols@1!cjs": "48faf647d225b64fa235ccc3e5a848e72221b0230935e421066a5de39aa89c3a",
"https://jspm.dev/npm:has-symbols@1.0.3!cjs": "36965f84e4e0ea1abeddb6928d0719a2648e61ceb9825df185b40d05cddb64df",
"https://jspm.dev/npm:has-symbols@1.0.3/shams!cjs": "669673e1dc7691c0b397580760121d57f3a5c5101dd70be2e8dd7d2a044de2e9",
"https://jspm.dev/npm:hasown@2!cjs": "9a39af846b167cae93b7a40f1ba4c97255bb5b07a1481da853a29bb68d24e603",
"https://jspm.dev/npm:hasown@2.0.0!cjs": "f52fd2477e345530f759465a984023f23d8261c4a54970e619daf1da6a2e85f5",
"https://jspm.dev/npm:object-inspect@1!cjs": "dc197b471ed55ecf2eabeb8da9aaee277e97831e65192531432a4ec2346211d9",
"https://jspm.dev/npm:object-inspect@1.13.1!cjs": "cec116e5c2b7d6b75e178d2541d70475d716ad912e3d5599e5c2d97284a9cb3e",
"https://jspm.dev/npm:qs@6!cjs": "210de1e090ac836c2495c19dfea88fc74b49de1b308241f8c9490d27ab6e0195",
"https://jspm.dev/npm:qs@6.11.2!cjs": "5da52fff60f7b1a6b1c73cdea2d9fc5d5588fa6c551b2a0ea2a1ebbb2a5e559f",
"https://jspm.dev/npm:qs@6.11.2/_/e71c21de.js": "cfe49eb949fb7291803f1ed2f4c0a244b8fca3b6936f5082fc97581a0663e427",
"https://jspm.dev/npm:qs@6.11.2/lib/stringify!cjs": "35d39c5871af151efe9ccca8e4ebecbf0282f97287b5fad56ebac369f69c2581",
"https://jspm.dev/npm:set-function-length@1!cjs": "b4c766d874ba261ff0c11aa18a6bf4510ecf8da09a7219da83a62772e0bc1b41",
"https://jspm.dev/npm:set-function-length@1.1.1!cjs": "f52607660d1f50e19e645ab49e6a4adf27fa4ae909867ec9950e993c430e4ca1",
"https://jspm.dev/npm:side-channel@1!cjs": "a07dfe7165af0d7f916d089490c38839397abcd8b218e4566b270858c9a0ea04",
"https://jspm.dev/npm:side-channel@1.0.4!cjs": "db65b31b6f9e67d57f04e26d71eb5b376306f5a89ab46fae1278c3ffefb19663",
"https://jspm.dev/npm:superagent@6.1.0!cjs": "fcf1c0b17cb3ff899b59ae178fc4ab74ad3b592d7fa8b44b16394001758e3176",
"https://jspm.dev/npm:superagent@6.1.0/lib/agent-base!cjs": "cfe465965a55d80114d835143717413945d0bbc46355d0f7f8200a89902ed006",
"https://jspm.dev/npm:superagent@6.1.0/lib/is-object!cjs": "95f67ff49b42fd5e82114b9d54a4b3fe1ac98813aed7ceaf53d314983f59820a",
"https://jspm.dev/npm:superagent@6.1.0/lib/request-base!cjs": "e361c341aa75d7417c918bc8fb697d0ccf96101e039dd2f00e5e45c01c534caa",
"https://jspm.dev/npm:superagent@6.1.0/lib/response-base!cjs": "00ac549f34d73c2753caa798aa7eb781051179013e3418ff0868a1e1904a8913",
"https://jspm.dev/npm:superagent@6.1.0/lib/utils!cjs": "ea706523553983c96ef4ab2f191c61c53fb8b78ad8ff2472b48f1385e896c030",
"https://jspm.dev/superagent@6.1.0": "4b3082d71252c42abd3930d85d1f3c4b2e937e0fab2b5f1c9d19eac20dea89a9"
},
"workspace": {
"dependencies": [

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,3 +1,4 @@
/// <reference lib="deno.ns" />
import './assets/main.css';
import { createApp } from 'vue';
// @ts-ignore:

View File

@@ -1,3 +1,5 @@
/// <reference types="vite/client" />
import { createRouter, createWebHistory } from "vue-router";
// Vue components imported here.