Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

Commit

Permalink
Rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinrouillard committed Oct 28, 2022
0 parents commit b118280
Show file tree
Hide file tree
Showing 24 changed files with 2,434 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
.env
.git
30 changes: 30 additions & 0 deletions .github/workflows/production.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: production-build
concurrency:
group: api
cancel-in-progress: true

on:
push:
branches:
- "main"

jobs:
build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}

- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest,ghcr.io/${{ github.repository }}:${{ github.sha }}
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
dist

node_modules

.env
.env.*
!.env.example

volumes

yarn-error.log
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 180,
"tabWidth": 2
}
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM node:alpine AS builder

RUN apk update
RUN apk add python3 build-base

WORKDIR /app
COPY . .

RUN yarn
RUN yarn build

FROM node:alpine

WORKDIR /app

COPY --from=builder /app/node_modules node_modules
COPY --from=builder /app/dist dist
COPY --from=builder /app/templates templates
COPY --from=builder /app/package.json ./

ENTRYPOINT yarn start
29 changes: 29 additions & 0 deletions development/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
services:
postgres:
container_name: hosting-psql
image: postgres
ports:
- 5432:5432
environment:
- POSTGRES_PASSWORD=postgres
volumes:
- ./volumes/postgres:/var/lib/postgresql/data
keydb:
container_name: hosting-keydb
hostname: keydb
image: eqalpha/keydb
ports:
- 6379:6379
volumes:
- ./volumes/keydb:/data
rabbit:
container_name: hosting-rabbit
image: rabbitmq:3-management
environment:
- "RABBITMQ_DEFAULT_USER=rabbit"
- "RABBITMQ_DEFAULT_PASS=docker"
ports:
- 5672:5672
- 15672:15672
volumes:
- ./volumes/rabbitmq:/var/lib/rabbitmq/mnesia
39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "api",
"version": "1.0.0",
"description": "API",
"main": "dist/server.js",
"repository": "[email protected]:dustinrouillard/api",
"author": "Dustin Rouillard <[email protected]>",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "NODE_ENV=development ts-node-esm --files -r dotenv/config src/server.ts",
"prepare": "prisma generate",
"start": "node dist/server.js",
"build": "tsc"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.198.0",
"@fastify/multipart": "^7.3.0",
"@prisma/client": "4.3.1",
"amqplib": "^0.10.3",
"deep-object-diff": "^1.1.7",
"envsafe": "^2.0.3",
"erlpack": "^0.1.4",
"fastify": "^4.6.0",
"fastify-plugin": "^4.2.1",
"file-type": "^18.0.0",
"otplib": "^12.0.1",
"pika-id": "^1.0.4",
"redis": "^4.3.1"
},
"devDependencies": {
"@types/amqplib": "^0.8.2",
"@types/node": "^18.7.18",
"dotenv": "^16.0.2",
"prisma": "^4.3.1",
"ts-node": "^10.9.1",
"typescript": "^4.8.3"
}
}
27 changes: 27 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model spotify_devices {
id Int @id @default(autoincrement())
name String?
type String?
spotify_history spotify_history[]
}

model spotify_history {
id String @id
type String @default("track")
name String
artists Json[]
length Int
image String
device Int @default(autoincrement())
listened_at DateTime @default(now()) @db.Timestamptz(6)
spotify_devices spotify_devices @relation(fields: [device], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
5 changes: 5 additions & 0 deletions src/connectivity/keydb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createClient } from 'redis';
import { env } from '../env.js';

export const keydb = createClient({ url: env.KEYDB_URI });
keydb.connect();
12 changes: 12 additions & 0 deletions src/connectivity/minio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { S3Client } from '@aws-sdk/client-s3';
import { env } from '../env.js';

export const minio = new S3Client({
endpoint: env.MINIO_ENDPOINT,
forcePathStyle: true,
region: 'none',
credentials: {
accessKeyId: env.MINIO_CLIENT_ID,
secretAccessKey: env.MINIO_CLIENT_SECRET
}
});
2 changes: 2 additions & 0 deletions src/connectivity/postgres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
13 changes: 13 additions & 0 deletions src/connectivity/rabbit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Channel, connect, Connection } from 'amqplib';
import { env } from '../env.js';

export enum RabbitOp {
SpotifyUpdate
}

export let rabbitClient: Connection;
export let rabbitChannel: Channel;
(async () => {
rabbitClient = await connect(env.RABBIT_URI);
rabbitChannel = await rabbitClient.createChannel();
})();
44 changes: 44 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { str, envsafe, port } from 'envsafe';

export const env = envsafe({
HOST: str({
default: '0.0.0.0'
}),
PORT: port({
default: 8080
}),
KEYDB_URI: str({
default: 'redis://10.7.20.3:6379'
}),
RABBIT_URI: str({
default: 'amqp://rabbit:[email protected]:5672'
}),
SPOTIFY_REDIRECT: str({
default: 'http://rest.dstn.to/v1/spotify/setup',
devDefault: 'http://127.0.0.1:8080/v1/spotify/setup'
}),
SPOTIFY_CLIENT_ID: str({
desc: 'Spotify Client ID'
}),
SPOTIFY_CLIENT_SECRET: str({
desc: 'Spotify Client Secret'
}),
FILES_DOMAIN: str({
default: 'https://files.dstn.to'
}),
IMAGES_DOMAIN: str({
default: 'https://dustin.pics'
}),
MINIO_ENDPOINT: str({
default: 'https://dcs.rouillard.cloud'
}),
MINIO_BUCKET: str({
default: 'cdn'
}),
MINIO_CLIENT_ID: str({
desc: 'Minio Client ID'
}),
MINIO_CLIENT_SECRET: str({
desc: 'Minio Client Secret'
})
});
97 changes: 97 additions & 0 deletions src/methods/files/upload-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { createHash } from 'crypto';
import { fileTypeFromBuffer } from 'file-type';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { Multipart } from '@fastify/multipart';
import { FastifyReply, FastifyRequest } from 'fastify';

import { env } from '../../env.js';
import { minio } from '../../connectivity/minio.js';

const mimeConversations: { [key: string]: string } = {
'text/plain': 'txt'
};

interface FileType {
mime: string;
extension: string;
}

const AlllowedTypes = ['image/gif', 'image/png', 'image/svg', 'image/webp', 'image/jpeg'];
const AlllowedFileTypes = [
...AlllowedTypes,
'application/octet-stream',
'application/ogg',
'application/pdf',
'application/rtf',
'application/x-sh',
'application/x-tar',
'application/zip',
'audio/mpeg',
'audio/ogg',
'audio/opus',
'audio/wav',
'audio/webm',
'font/otf',
'font/ttf',
'font/woff',
'font/woff2',
'image/webp',
'text/css',
'text/csv',
'text/javascript',
'text/plain',
'video/mov',
'video/mp4',
'video/mpeg',
'video/ogg',
'video/webm'
];

async function GetType(file: Multipart, buffer: Buffer): Promise<FileType> {
const preType = file.mimetype;

let bufferType;
if (Object.keys(mimeConversations).includes(preType)) {
bufferType = { mime: preType, ext: mimeConversations[preType] };
} else bufferType = await fileTypeFromBuffer(buffer);

return {
mime: bufferType?.mime || preType,
extension: bufferType?.ext || mimeConversations[preType]
};
}

export async function uploadFile(req: FastifyRequest<{ Params: { type: string } }>, res: FastifyReply) {
const file = await req.file();

const { type } = req.params;

if (!['images', 'files'].includes(type)) return res.status(400).send({ code: 'invalid_upload_type', message: 'Invalid upload type' });

if (!file) return res.status(400).send({ code: 'file_required', message: 'File to upload is required in the multipart formdata' });

const buffer = await file?.toBuffer();
const file_type = await GetType(file, buffer);

if (!file.mimetype || !(type == 'images' ? AlllowedTypes : AlllowedFileTypes).includes(file.mimetype)) throw { code: 'invalid_file_type', data: file.mimetype };

const hash = createHash('sha1').update(buffer).digest('hex').substring(0, 16);

const folder = type == 'images' ? 'i' : 'u';
const file_path = `${hash}${file_type.extension ? `.${file_type.extension}` : ''}`;

try {
await minio.send(
new PutObjectCommand({
Bucket: env.MINIO_BUCKET,
Key: `${folder}/${file_path}`,
Body: buffer,
ContentType: file_type.mime
})
);
} catch (error) {
return res.status(400).send({ code: 'failed_to_put_object', message: 'Failed to put object to s3 bucket' });
}

return res.status(200).send({ success: true, data: { url: type == 'images' ? `${env.IMAGES_DOMAIN}/${file_path}` : `${env.FILES_DOMAIN}/${file_path}` } });
}
17 changes: 17 additions & 0 deletions src/methods/spotify/authorize-spotify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FastifyReply, FastifyRequest } from 'fastify';
import { stringify } from 'querystring';
import { keydb } from '../../connectivity/keydb.js';
import { env } from '../../env.js';

export async function authorizeSpotify(req: FastifyRequest, res: FastifyReply) {
if (await keydb.exists('spotify/refresh_token')) return res.status(400).send({ code: 'already_authorized', message: 'Spotify is already authorized.' });

const query = stringify({
response_type: 'code',
client_id: env.SPOTIFY_CLIENT_ID,
scope: encodeURIComponent('user-read-playback-state user-read-currently-playing'),
redirect_uri: `${env.SPOTIFY_REDIRECT}`
});

return res.status(200).send({ url: `https://accounts.spotify.com/authorize?${query}` });
}
8 changes: 8 additions & 0 deletions src/methods/spotify/get-current-playing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FastifyReply, FastifyRequest } from 'fastify';
import { keydb } from '../../connectivity/keydb.js';

export async function getCurrentPlaying(req: FastifyRequest, res: FastifyReply) {
const current = await keydb.get('spotify/current');
if (!current) return res.status(200).send({ success: true, data: { playing: false } });
return res.status(200).send({ success: true, data: { playing: true, ...JSON.parse(current) } });
}
8 changes: 8 additions & 0 deletions src/methods/spotify/get-recents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FastifyReply, FastifyRequest } from 'fastify';
import { prisma } from '../../connectivity/postgres.js';

export async function getRecentPlays(req: FastifyRequest<{ Querystring: { limit: number } }>, res: FastifyReply) {
const entries = await prisma.spotify_history.findMany({ orderBy: { listened_at: 'desc' }, take: req.query.limit || 10 });

return res.status(200).send({ success: true, data: { recents: entries, count: entries.length, limit: req.query.limit } });
}
Loading

0 comments on commit b118280

Please sign in to comment.