merge from local

This commit is contained in:
SrGooglo 2024-03-06 19:43:09 +00:00
parent b6a4942fd3
commit 6aba03e310
205 changed files with 2817 additions and 872 deletions

View File

@ -26,6 +26,7 @@ global["aliases"] = {
"src": global["__src"], "src": global["__src"],
// expose shared resources // expose shared resources
"@db_models": path.resolve(__dirname, "db_models"),
"@shared-utils": path.resolve(__dirname, "utils"), "@shared-utils": path.resolve(__dirname, "utils"),
"@shared-classes": path.resolve(__dirname, "classes"), "@shared-classes": path.resolve(__dirname, "classes"),
"@shared-lib": path.resolve(__dirname, "lib"), "@shared-lib": path.resolve(__dirname, "lib"),
@ -103,7 +104,7 @@ function registerAliases() {
async function injectEnvFromInfisical() { async function injectEnvFromInfisical() {
const envMode = global.FORCE_ENV ?? global.isProduction ? "prod" : "dev" const envMode = global.FORCE_ENV ?? global.isProduction ? "prod" : "dev"
console.log(`🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`) console.log(`[BOOT] 🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`)
const client = new InfisicalClient({ const client = new InfisicalClient({
accessToken: process.env.INFISICAL_TOKEN, accessToken: process.env.INFISICAL_TOKEN,
@ -130,7 +131,7 @@ async function Boot(main) {
} }
if (process.env.INFISICAL_TOKEN) { if (process.env.INFISICAL_TOKEN) {
console.log(`INFISICAL_TOKEN found, injecting env variables from INFISICAL...`) console.log(`[BOOT] INFISICAL_TOKEN found, injecting env variables from INFISICAL...`)
await injectEnvFromInfisical() await injectEnvFromInfisical()
} }
@ -138,7 +139,7 @@ async function Boot(main) {
await instance.initialize() await instance.initialize()
if (process.send) { if (process.env.lb_service && process.send) {
process.send({ process.send({
status: "ready" status: "ready"
}) })
@ -147,7 +148,7 @@ async function Boot(main) {
return instance return instance
} }
console.log(`Booting in [${global.isProduction ? "production" : "development"}] mode...`) console.log(`[BOOT] Booting in [${global.isProduction ? "production" : "development"}] mode...`)
// Apply patches // Apply patches
registerPatches() registerPatches()

View File

@ -1,5 +1,5 @@
import jwt from "jsonwebtoken" import jwt from "jsonwebtoken"
import { Session, RegenerationToken, User } from "../../classes/DbModels" import { Session, RegenerationToken, User } from "../../db_models"
export default class Token { export default class Token {
static get strategy() { static get strategy() {

View File

@ -1,4 +1,4 @@
import { SyncEntry } from "@shared-classes/DbModels" import { SyncEntry } from "../../db_models"
import crypto from "crypto" import crypto from "crypto"

View File

@ -0,0 +1,17 @@
export default {
name: "APRSession",
collection: "apr_sessions",
schema: {
user_id: { type: String, required: true },
created_at: { type: Date, required: true },
expires_at: { type: Date, required: true },
code: { type: String, required: true },
ip_address: { type: String, required: true },
client: { type: String, required: true },
status: { type: String, required: true },
}
}

View File

@ -0,0 +1,13 @@
export default {
name: "MFASession",
collection: "mfa_sessions",
schema: {
type: { type: String, required: true },
user_id: { type: String, required: true },
code: { type: String, required: true },
created_at: { type: Date, required: true },
expires_at: { type: Date, required: true },
}
}

View File

@ -0,0 +1,13 @@
export default {
name: "OperationLog",
collection: "operation_logs",
schema: {
type: { type: String, required: true },
user_id: { type: String },
comments: { type: Array, default: [] },
date: { type: Date, required: true },
ip_address: { type: String },
client: { type: String },
}
}

View File

@ -0,0 +1,8 @@
export default {
name: "UserConfig",
collection: "user_config",
schema: {
user_id: { type: String, required: true },
values: { type: Object, default: {} },
}
}

View File

@ -11,6 +11,8 @@ import Spinnies from "spinnies"
import chokidar from "chokidar" import chokidar from "chokidar"
import { dots as DefaultSpinner } from "spinnies/spinners.json" import { dots as DefaultSpinner } from "spinnies/spinners.json"
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
import getInternalIp from "./lib/getInternalIp" import getInternalIp from "./lib/getInternalIp"
import comtyAscii from "./ascii" import comtyAscii from "./ascii"
import pkg from "./package.json" import pkg from "./package.json"
@ -50,11 +52,12 @@ let services = null
const spinnies = new Spinnies() const spinnies = new Spinnies()
const instancePool = [] const ipcRouter = global.ipcRouter = new IPCRouter()
const instancePool = global.instancePool = []
const serviceFileReference = {} const serviceFileReference = {}
const serviceWatcher = Observable.from({}) const serviceRegistry = global.serviceRegistry = Observable.from({})
Observable.observe(serviceWatcher, (changes) => { Observable.observe(serviceRegistry, (changes) => {
const { type, path, value } = changes[0] const { type, path, value } = changes[0]
switch (type) { switch (type) {
@ -62,7 +65,7 @@ Observable.observe(serviceWatcher, (changes) => {
//console.log(`Updated service | ${path} > ${value}`) //console.log(`Updated service | ${path} > ${value}`)
//check if all services all ready //check if all services all ready
if (Object.values(serviceWatcher).every((service) => service.ready)) { if (Object.values(serviceRegistry).every((service) => service.ready)) {
handleAllReady() handleAllReady()
} }
@ -97,9 +100,9 @@ const relp_commands = [
aliases: ["s", "sel"], aliases: ["s", "sel"],
fn: (cb, service) => { fn: (cb, service) => {
if (!isNaN(parseInt(service))) { if (!isNaN(parseInt(service))) {
service = serviceWatcher[Object.keys(serviceWatcher)[service]] service = serviceRegistry[Object.keys(serviceRegistry)[service]]
} else { } else {
service = serviceWatcher[service] service = serviceRegistry[service]
} }
if (!service) { if (!service) {
@ -166,7 +169,7 @@ async function handleAllReady() {
// SERVICE WATCHER FUNCTIONS // SERVICE WATCHER FUNCTIONS
async function handleNewServiceStarting(id) { async function handleNewServiceStarting(id) {
if (serviceWatcher[id].ready === false) { if (serviceRegistry[id].ready === false) {
spinnies.add(id, { spinnies.add(id, {
text: `📦 [${id}] Loading service...`, text: `📦 [${id}] Loading service...`,
spinner: DefaultSpinner spinner: DefaultSpinner
@ -175,25 +178,25 @@ async function handleNewServiceStarting(id) {
} }
async function handleServiceStarted(id) { async function handleServiceStarted(id) {
if (serviceWatcher[id].ready === false) { if (serviceRegistry[id].ready === false) {
if (spinnies.pick(id)) { if (spinnies.pick(id)) {
spinnies.succeed(id, { text: `[${id}][${serviceWatcher[id].index}] Ready` }) spinnies.succeed(id, { text: `[${id}][${serviceRegistry[id].index}] Ready` })
} }
} }
serviceWatcher[id].ready = true serviceRegistry[id].ready = true
} }
async function handleServiceExit(id, code, err) { async function handleServiceExit(id, code, err) {
//console.log(`🛑 Service ${id} exited with code ${code}`, err) //console.log(`🛑 Service ${id} exited with code ${code}`, err)
if (serviceWatcher[id].ready === false) { if (serviceRegistry[id].ready === false) {
if (spinnies.pick(id)) { if (spinnies.pick(id)) {
spinnies.fail(id, { text: `[${id}][${serviceWatcher[id].index}] Failed with code ${code}` }) spinnies.fail(id, { text: `[${id}][${serviceRegistry[id].index}] Failed with code ${code}` })
} }
} }
serviceWatcher[id].ready = false serviceRegistry[id].ready = false
} }
// PROCESS HANDLERS // PROCESS HANDLERS
@ -225,16 +228,24 @@ async function handleIPCData(id, data) {
function spawnService({ id, service, cwd }) { function spawnService({ id, service, cwd }) {
handleNewServiceStarting(id) handleNewServiceStarting(id)
const instanceEnv = {
...process.env,
lb_service: {
id: service.id,
index: service.index,
},
}
let instance = ChildProcess.fork(bootloaderBin, [service], { let instance = ChildProcess.fork(bootloaderBin, [service], {
detached: false, detached: false,
silent: true, silent: true,
cwd: cwd, cwd: cwd,
env: { env: instanceEnv,
...process.env
}
}) })
instance.reload = () => { instance.reload = () => {
ipcRouter.unregister({ id, instance })
instance.kill() instance.kill()
instance = spawnService({ id, service, cwd }) instance = spawnService({ id, service, cwd })
@ -278,6 +289,8 @@ function spawnService({ id, service, cwd }) {
return handleServiceExit(id, code, err) return handleServiceExit(id, code, err)
}) })
ipcRouter.register({ id, instance })
return instance return instance
} }
@ -314,7 +327,7 @@ async function main() {
serviceFileReference[instanceFile] = id serviceFileReference[instanceFile] = id
serviceWatcher[id] = { serviceRegistry[id] = {
index: services.indexOf(service), index: services.indexOf(service),
id: id, id: id,
version: version, version: version,
@ -328,16 +341,18 @@ async function main() {
// create a new process of node for each service // create a new process of node for each service
for await (let service of services) { for await (let service of services) {
const { id, version, cwd } = serviceWatcher[serviceFileReference[path.basename(service)]] const { id, version, cwd } = serviceRegistry[serviceFileReference[path.basename(service)]]
const instance = spawnService({ id, service, cwd }) const instance = spawnService({ id, service, cwd })
// push to pool const serviceInstance = {
instancePool.push({
id, id,
version, version,
instance instance
}) }
// push to pool
instancePool.push(serviceInstance)
// if is NODE_ENV to development, start a file watcher for hot-reload // if is NODE_ENV to development, start a file watcher for hot-reload
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {

View File

@ -1,8 +1,8 @@
import { authorizedServerTokens } from "../../classes/DbModels" import { authorizedServerTokens } from "../../db_models"
import SecureEntry from "../../classes/SecureEntry" import SecureEntry from "../../classes/SecureEntry"
import AuthToken from "../../classes/AuthToken" import AuthToken from "../../classes/AuthToken"
export default async (req, res, next) => { export default async (req, res) => {
function reject(description) { function reject(description) {
return res.status(401).json({ error: `${description ?? "Invalid session"}` }) return res.status(401).json({ error: `${description ?? "Invalid session"}` })
} }
@ -35,7 +35,7 @@ export default async (req, res, next) => {
user: validation.user user: validation.user
} }
return next() return
} }
case "Server": { case "Server": {
const [client_id, token] = tokenAuthHeader[1].split(":") const [client_id, token] = tokenAuthHeader[1].split(":")
@ -65,7 +65,7 @@ export default async (req, res, next) => {
roles: ["server"], roles: ["server"],
} }
return next() return
} }
default: { default: {
return reject("Invalid token type") return reject("Invalid token type")

View File

@ -22,7 +22,6 @@
"clui": "^0.3.6", "clui": "^0.3.6",
"dotenv": "^16.4.4", "dotenv": "^16.4.4",
"http-proxy-middleware": "^2.0.6", "http-proxy-middleware": "^2.0.6",
"hyper-express": "^6.14.12",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"linebridge": "^0.16.0", "linebridge": "^0.16.0",
"module-alias": "^2.2.3", "module-alias": "^2.2.3",

View File

@ -0,0 +1,7 @@
export default class Account {
static usernameMeetPolicy = require("./methods/usernameMeetPolicy").default
static passwordMeetPolicy = require("./methods/passwordMeetPolicy").default
static loginStrategy = require("./methods/loginStrategy").default
static changePassword = require("./methods/changePassword").default
static create = require("./methods/create").default
}

View File

@ -0,0 +1,39 @@
import bcrypt from "bcrypt"
import { User, OperationLog } from "@db_models"
import Account from "@classes/account"
export default async ({ user_id, old_hash, old_password, new_password, log_comment }, req) => {
let user = await User.findById(user_id).select("+password")
user = await Account.loginStrategy({ password: old_password, hash: old_hash }, user)
await Account.passwordMeetPolicy(new_password)
user.password = bcrypt.hashSync(new_password, parseInt(process.env.BCRYPT_ROUNDS ?? 3))
await user.save()
const operation = {
type: "password:changed",
user_id: user._id.toString(),
date: Date.now(),
comments: []
}
if (log_comment) {
operation.comments.push(log_comment)
}
if (typeof req === "object") {
operation.ip_address = req.headers["x-forwarded-for"]?.split(",")[0] ?? req.socket?.remoteAddress ?? req.ip
operation.client = req.headers["user-agent"]
}
const log = new OperationLog(operation)
await log.save()
ipc.invoke("ems", "password:changed", operation)
return user
}

View File

@ -0,0 +1,53 @@
import bcrypt from "bcrypt"
import { User } from "@db_models"
import requiredFields from "@shared-utils/requiredFields"
import Account from "@classes/account"
export default async (payload) => {
requiredFields(["username", "password", "email"], payload)
let { username, password, email, fullName, roles, avatar, acceptTos } = payload
if (ToBoolean(acceptTos) !== true) {
throw new OperationError(400, "You must accept the terms of service in order to create an account.")
}
await Account.usernameMeetPolicy(username)
// check if username is already taken
const existentUser = await User.findOne({ username: username })
if (existentUser) {
throw new OperationError(400, "User already exists")
}
// check if the email is already in use
const existentEmail = await User.findOne({ email: email })
if (existentEmail) {
throw new OperationError(400, "Email already in use")
}
await Account.passwordMeetPolicy(password)
// hash the password
const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3))
let user = new User({
username: username,
password: hash,
email: email,
fullName: fullName,
avatar: avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
roles: roles,
createdAt: new Date().getTime(),
acceptTos: acceptTos,
})
await user.save()
// TODO: dispatch event bus
//global.eventBus.emit("user.create", user)
return user
}

View File

@ -0,0 +1,28 @@
import bcrypt from "bcrypt"
import { User } from "@db_models"
export default async ({ username, password, hash }, user) => {
if (typeof user === "undefined") {
let isEmail = username.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
let query = isEmail ? { email: username } : { username: username }
user = await User.findOne(query).select("+password")
}
if (!user) {
throw new OperationError(401, "User not found")
}
if (typeof hash !== "undefined") {
if (user.password !== hash) {
throw new OperationError(401, "Invalid credentials")
}
} else {
if (!bcrypt.compareSync(password, user.password)) {
throw new OperationError(401, "Invalid credentials")
}
}
return user
}

View File

@ -0,0 +1,11 @@
export default async (password) => {
if (password.length < 8) {
throw new OperationError(400, "Password must be at least 8 characters")
}
if (password.length > 64) {
throw new OperationError(400, "Password cannot be longer than 64 characters")
}
return true
}

View File

@ -0,0 +1,26 @@
export default async (username) => {
if (username.length < 3) {
throw new OperationError(400, "Username must be at least 3 characters")
}
if (username.length > 64) {
throw new OperationError(400, "Username cannot be longer than 64 characters")
}
// if username has capital letters, throw error
if (username !== username.toLowerCase()) {
throw new OperationError(400, "Username must be lowercase")
}
// make sure the username has no spaces
if (username.includes(" ")) {
throw new OperationError(400, "Username cannot contain spaces")
}
// make sure the username has no valid characters. Only letters, numbers, and underscores
if (!/^[a-z0-9_]+$/.test(username)) {
throw new OperationError(400, "Username can only contain letters, numbers, and underscores")
}
return true
}

View File

@ -1,4 +1,4 @@
import { Session } from "@shared-classes/DbModels" import { Session } from "@db_models"
export default { export default {
middlewares: ["withAuthentication"], middlewares: ["withAuthentication"],

View File

@ -0,0 +1,23 @@
import Account from "@classes/account"
import requiredFields from "@shared-utils/requiredFields"
export default {
middlewares: ["withAuthentication"],
fn: async (req) => {
requiredFields(["old_password", "new_password"], req.body)
await Account.changePassword(
{
user_id: req.auth.session.user_id,
old_password: req.body.old_password,
new_password: req.body.new_password,
log_comment: "Changed from password change request"
},
req
)
return {
message: "Password changed"
}
}
}

View File

@ -1,36 +1,97 @@
import AuthToken from "@shared-classes/AuthToken" import AuthToken from "@shared-classes/AuthToken"
import { User } from "@shared-classes/DbModels" import { UserConfig, MFASession } from "@db_models"
import requiredFields from "@shared-utils/requiredFields" import requiredFields from "@shared-utils/requiredFields"
import bcrypt from "bcrypt"
import Account from "@classes/account"
export default async (req, res) => { export default async (req, res) => {
requiredFields(["username", "password"], req.body) requiredFields(["username", "password"], req.body)
const { username, password } = req.body const user = await Account.loginStrategy({
username: req.body.username,
password: req.body.password,
})
let isEmail = username.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) const userConfig = await UserConfig.findOne({ user_id: user._id.toString() }).catch(() => {
return {}
})
let query = isEmail ? { email: username } : { username: username } if (userConfig && userConfig.values) {
if (userConfig.values.mfa_enabled) {
let codeVerified = false
const user = await User.findOne(query).select("+password") // search if is already a mfa session
let mfaSession = await MFASession.findOne({ user_id: user._id })
if (!user) { if (mfaSession) {
throw new OperationError(401, "User not found") if (!req.body.mfa_code) {
await mfaSession.delete()
} else {
if (mfaSession.expires_at < new Date().getTime()) {
await mfaSession.delete()
throw new OperationError(401, "MFA code expired, login again...")
}
if (mfaSession.code == req.body.mfa_code) {
codeVerified = true
await mfaSession.delete()
} else {
throw new OperationError(401, "Invalid MFA code, try again...")
}
}
}
if (!codeVerified) {
const mfa = {
type: "email",
user_id: user._id,
code: Math.floor(Math.random() * 9000) + 1000,
created_at: new Date().getTime(),
// expires in 1 hour
expires_at: new Date().getTime() + 60 * 60 * 1000,
ip_address: req.headers["x-forwarded-for"]?.split(",")[0] ?? req.socket?.remoteAddress ?? req.ip,
client: req.headers["user-agent"],
}
// create a new mfa session
mfaSession = new MFASession(mfa)
await mfaSession.save()
ipc.invoke("ems", "mfa:send", mfa)
return {
message: `MFA required, using [${mfa.type}] method.`,
mfa_required: true,
}
}
}
} }
if (!bcrypt.compareSync(password, user.password)) { const authData = {
return res.status(401).json({ date: new Date().getTime(),
message: "Invalid credentials",
})
}
const token = await AuthToken.createAuth({
username: user.username, username: user.username,
user_id: user._id.toString(), user_id: user._id.toString(),
ip_address: req.headers["x-forwarded-for"]?.split(",")[0] ?? req.socket?.remoteAddress ?? req.ip, ip_address: req.headers["x-forwarded-for"]?.split(",")[0] ?? req.socket?.remoteAddress ?? req.ip,
client: req.headers["user-agent"], client: req.headers["user-agent"],
//signLocation: global.signLocation, }
})
const token = await AuthToken.createAuth(authData)
// emit to ems to notify user for the new login, in the background
try {
global.ipc.call("ems", "new:login", authData).catch((error) => {
// whoipsi dupsi
console.error(error)
})
} catch (error) {
// whoipsi dupsi
console.error(error)
}
return { token: token } return { token: token }
} }

View File

@ -1,4 +1,4 @@
import { User } from "@shared-classes/DbModels" import { User } from "@db_models"
export default async (req) => { export default async (req) => {
const { username, email } = req.query const { username, email } = req.query

View File

@ -0,0 +1,45 @@
import { User, APRSession } from "@db_models"
import requiredFields from "@shared-utils/requiredFields"
import Account from "@classes/account"
export default async (req) => {
requiredFields(["new_password"], req.body)
const { apr_code } = req.params
const apr = await APRSession.findOne({ code: apr_code })
if (!apr) {
throw new OperationError(400, "Request not found")
}
if (apr.expires_at < new Date().getTime()) {
throw new OperationError(400, "Request expired")
}
if (apr.status !== "sended") {
throw new OperationError(400, "Request already completed")
}
const user = await User.findById(apr.user_id).select("+password")
await Account.changePassword(
{
user_id: apr.user_id,
old_hash: user.password,
new_password: req.body.new_password,
log_comment: "Changed from APR request"
},
req,
)
apr.status = "completed"
apr.completed_at = Date.now()
await apr.save()
return {
message: "Password changed",
}
}

View File

@ -0,0 +1,49 @@
import { User, APRSession } from "@db_models"
import requiredFields from "@shared-utils/requiredFields"
export default async (req) => {
requiredFields(["email"], req.body)
const { email } = req.body
const user = await User.findOne({ email })
if (!user) {
throw new OperationError(400, "User not found")
}
const apr = new APRSession({
user_id: user._id.toString(),
created_at: new Date().getTime(),
expires_at: new Date().getTime() + 60 * 60 * 1000,
code: nanoid(),
ip_address: req.headers["x-forwarded-for"]?.split(",")[0] ?? req.socket?.remoteAddress ?? req.ip,
client: req.headers["user-agent"],
status: "sended",
})
await apr.save()
await ipc.call("ems", "apr:send", {
user_id: user._id.toString(),
username: user.username,
email: user.email,
code: apr.code,
apr_link: `https://comty.app/forgot/apr/${apr.code}`,
created_at: apr.created_at,
expires_at: apr.expires_at,
client: apr.client,
ip_address: apr.ip_address,
})
return {
message: `Email sent to ${email}`,
sent: true,
}
}

View File

@ -1,72 +1,7 @@
import { User } from "@shared-classes/DbModels" import Account from "@classes/account"
import bcrypt from "bcrypt"
import requiredFields from "@shared-utils/requiredFields"
export default async (req) => { export default async (req) => {
requiredFields(["username", "password", "email"], req.body) const result = await Account.create(req.body)
let { username, password, email, fullName, roles, avatar, acceptTos } = req.body return result
if (ToBoolean(acceptTos) !== true) {
throw new OperationError(400, "You must accept the terms of service in order to create an account.")
}
if (username.length < 3) {
throw new OperationError(400, "Username must be at least 3 characters")
}
if (username.length > 64) {
throw new OperationError(400, "Username cannot be longer than 64 characters")
}
// if username has capital letters, throw error
if (username !== username.toLowerCase()) {
throw new OperationError(400, "Username must be lowercase")
}
// make sure the username has no spaces
if (username.includes(" ")) {
throw new OperationError(400, "Username cannot contain spaces")
}
// make sure the username has no valid characters. Only letters, numbers, and underscores
if (!/^[a-z0-9_]+$/.test(username)) {
throw new OperationError(400, "Username can only contain letters, numbers, and underscores")
}
// check if username is already taken
const existentUser = await User.findOne({ username: username })
if (existentUser) {
throw new OperationError(400, "User already exists")
}
// check if the email is already in use
const existentEmail = await User.findOne({ email: email })
if (existentEmail) {
throw new OperationError(400, "Email already in use")
}
// hash the password
const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3))
let user = new User({
username: username,
password: hash,
email: email,
fullName: fullName,
avatar: avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
roles: roles,
createdAt: new Date().getTime(),
acceptTos: acceptTos,
})
await user.save()
// TODO: dispatch event bus
//global.eventBus.emit("user.create", user)
return user
} }

View File

@ -1,5 +1,8 @@
import { Server } from "linebridge/src/server" import { Server } from "linebridge/src/server"
import nodemailer from "nodemailer" import nodemailer from "nodemailer"
import DbManager from "@shared-classes/DbManager"
import SharedMiddlewares from "@shared-middlewares"
export default class API extends Server { export default class API extends Server {
static refName = "ems" static refName = "ems"
@ -7,7 +10,12 @@ export default class API extends Server {
static routesPath = `${__dirname}/routes` static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3007 static listen_port = process.env.HTTP_LISTEN_PORT ?? 3007
middlewares = {
...SharedMiddlewares
}
contexts = { contexts = {
db: new DbManager(),
mailTransporter: nodemailer.createTransport({ mailTransporter: nodemailer.createTransport({
host: process.env.SMTP_HOSTNAME, host: process.env.SMTP_HOSTNAME,
port: process.env.SMTP_PORT ?? 587, port: process.env.SMTP_PORT ?? 587,
@ -18,6 +26,17 @@ export default class API extends Server {
}, },
}), }),
} }
ipcEvents = {
"new:login": require("./ipcEvents/newLogin").default,
"mfa:send": require("./ipcEvents/mfaSend").default,
"apr:send": require("./ipcEvents/aprSend").default,
"password:changed": require("./ipcEvents/passwordChanged").default,
}
async onInitialize() {
await this.contexts.db.initialize()
}
} }
Boot(API) Boot(API)

View File

@ -0,0 +1,26 @@
import templates from "../templates"
export default async (ctx, data) => {
const { user_id, username, email, apr_link, created_at, expires_at, ip_address, client } = data
if (!user_id || !username || !email || !apr_link || !created_at || !expires_at || !ip_address || !client) {
throw new OperationError(400, "Bad request")
}
const result = await ctx.mailTransporter.sendMail({
from: process.env.SMTP_USERNAME,
to: email,
subject: "Password reset",
html: templates.password_recovery({
username: username,
apr_link: apr_link,
date: new Date(created_at),
ip: ip_address,
client: client,
}),
})
return result
}

View File

@ -0,0 +1,27 @@
// TODO: Support SMS 2fa
import { User } from "@db_models"
import templates from "../templates"
export default async (ctx, data) => {
const user = await User.findById(data.user_id)
if (!user) {
throw new OperationError(404, "User not found")
}
const result = await ctx.mailTransporter.sendMail({
from: process.env.SMTP_USERNAME,
to: user.email,
subject: "Verification code",
html: templates.mfa_code({
mfa_code: data.code,
username: user.username,
date: new Date(data.created_at),
expires_at: new Date(data.expires_at),
ip: data.ip_address,
client: data.client,
}),
})
return result
}

View File

@ -0,0 +1,24 @@
import { User } from "@db_models"
import templates from "../templates"
export default async (ctx, data) => {
const user = await User.findById(data.user_id)
if (!user) {
throw new OperationError(404, "User not found")
}
const result = await ctx.mailTransporter.sendMail({
from: process.env.SMTP_USERNAME,
to: user.email,
subject: "New login",
html: templates.new_login({
date: new Date(data.date),
ip: data.ip_address,
client: data.client,
username: user.username
}),
})
return result
}

View File

@ -0,0 +1,24 @@
import { User } from "@db_models"
import templates from "../templates"
export default async (ctx, data) => {
const user = await User.findById(data.user_id)
if (!user) {
throw new OperationError(404, "User not found")
}
const result = await ctx.mailTransporter.sendMail({
from: process.env.SMTP_USERNAME,
to: user.email,
subject: "Your password has been changed",
html: templates.password_changed({
username: user.username,
date: new Date(data.date),
ip: data.ip_address,
client: data.client,
}),
})
return result
}

View File

@ -4,7 +4,12 @@
"version": "0.1.0", "version": "0.1.0",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"proxy": {
"path": "/ems",
"port": 3007
},
"dependencies": { "dependencies": {
"handlebars": "^4.7.8",
"nodemailer": "^6.9.11", "nodemailer": "^6.9.11",
"web-push": "^3.6.7" "web-push": "^3.6.7"
} }

View File

@ -1,5 +1,6 @@
export default { export default {
useContext: ["mailTransporter"], useContext: ["mailTransporter"],
middlewares: ["withAuthentication"],
fn: async (req, res) => { fn: async (req, res) => {
req.body = await req.urlencoded() req.body = await req.urlencoded()

View File

@ -0,0 +1,21 @@
import templates from "../../../../templates"
const testData = {
email: "test@example",
ip: "127.0.0.1",
client: "Firefox",
username: "test",
date: new Date(),
apr_link: "https://comty.app",
mfa_code: "000-000-000",
}
export default (req, res) => {
const { email } = req.params
if (!templates[email]) {
throw new OperationError(404, "Template not found")
}
return res.html(templates[email](testData))
}

View File

@ -1,5 +0,0 @@
export default (req, res) => {
return res.json({
message: "Hi! from the deeps."
})
}

View File

@ -1,5 +0,0 @@
export default async (req, res) => {
return res.json({
msg: "HI!!"
})
}

View File

@ -0,0 +1,11 @@
import fs from "node:fs"
import path from "node:path"
import Handlebars from "handlebars"
export default {
new_login: Handlebars.compile(fs.readFileSync(path.resolve(__dirname, "new_login/index.handlebars"), "utf-8")),
mfa_code: Handlebars.compile(fs.readFileSync(path.resolve(__dirname, "mfa_code/index.handlebars"), "utf-8")),
password_recovery: Handlebars.compile(fs.readFileSync(path.resolve(__dirname, "password_recovery/index.handlebars"), "utf-8")),
password_changed: Handlebars.compile(fs.readFileSync(path.resolve(__dirname, "password_changed/index.handlebars"), "utf-8")),
}

View File

@ -0,0 +1,673 @@
<!doctype html>
<html lang="en" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title></title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 0;
}
</style>
<!--[if mso]> <noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.y{width:100% !important;}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Inter:400,300" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=DM Mono:400" rel="stylesheet" type="text/css">
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:599px) {
.i {
width: 528px !important;
max-width: 528px;
}
.m {
width: 496px !important;
max-width: 496px;
}
}
</style>
<style media="screen and (min-width:599px)">
.moz-text-html .i {
width: 528px !important;
max-width: 528px;
}
.moz-text-html .m {
width: 496px !important;
max-width: 496px;
}
</style>
<style type="text/css">
u+.emailify .gs {
background: #000;
mix-blend-mode: screen;
display: inline-block;
padding: 0;
margin: 0;
}
u+.emailify .gd {
background: #000;
mix-blend-mode: difference;
display: inline-block;
padding: 0;
margin: 0;
}
p {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
}
u+.emailify a {
color: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit !important;
text-decoration: none !important;
}
@media only screen and (max-width:599px) {
.emailify {
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
u+.emailify .glist {
margin-left: 1em !important;
}
td.x {
padding-left: 0 !important;
padding-right: 0 !important;
}
td.u {
height: auto !important;
}
div.r.e>table>tbody>tr>td,
div.r.e>div>table>tbody>tr>td {
padding-right: 16px !important
}
div.r.ys>table>tbody>tr>td,
div.r.ys>div>table>tbody>tr>td {
padding-left: 16px !important
}
}
</style>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<!--[if gte mso 9]>
<style>a:link{mso-style-priority:99;color:inherit;text-decoration:none;}a:visited{mso-style-priority:99;color:inherit;text-decoration:none;}li{text-indent:-1em;}table,td,p,div,span,ul,ol,li,a{mso-hyphenate:none;}sup,sub{font-size:100% !important;}
</style>
<![endif]-->
</head>
<body lang="en" link="#DD0000" vlink="#DD0000" class="emailify"
style="mso-line-height-rule:exactly;mso-hyphenate:none;word-spacing:normal;background-color:#f0f2f5; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; padding: 30px 0;">
<div style="background-color:#fffffe; border-radius: 20px; overflow: hidden; padding: 30px 0;" lang="en" dir="auto">
<!--[if mso | IE]>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0;padding:0;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0;">
<tbody>
<tr>
<td style="width:50px;"> <img alt
src="https://storage.ragestudio.net/rstudio/branding/comty/iso/logo_alt.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
width="91" height="auto">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:24px;mso-ansi-font-size:16px;">
<span
style="font-size:16px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;">Hi
@{{username}}</span></p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:transparent;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:transparent;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:18px;mso-ansi-font-size:16px;">
<span
style="font-size:15px;font-family:'Inter','Arial',sans-serif;font-weight:300;color:#000000;line-height:120%;mso-line-height-alt:18px;mso-ansi-font-size:16px;">Here
is the verification code you need to log in.</span></p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 32px 16px 32px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:496px;">
<![endif]-->
<div class="m y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0;padding:0;word-break:break-word;">
<p
style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]>
<table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:496px;" role="presentation" width="496px"><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:24px;mso-ansi-font-size:16px;">
<span
style="font-size:16px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;">{{mfa_code}}</span>
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 32px 16px 32px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:496px;">
<![endif]-->
<div class="m y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0;padding:0;word-break:break-word;">
<p
style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]>
<table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:496px;" role="presentation" width="496px"><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:2px 16px 2px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:14px;mso-ansi-font-size:14px;">
<span
style="font-size:14px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:100%;mso-line-height-alt:14px;mso-ansi-font-size:14px;">Wasn't
it you? So it looks like someone new is trying to log into
your account.</span></p>
<p
style="Margin:0;mso-line-height-alt:14px;mso-ansi-font-size:14px;">
<span
style="font-size:14px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:100%;mso-line-height-alt:14px;mso-ansi-font-size:14px;">&nbsp;</span>
</p>
<p
style="Margin:0;mso-line-height-alt:14px;mso-ansi-font-size:14px;">
<span
style="font-size:14px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:100%;mso-line-height-alt:14px;mso-ansi-font-size:14px;">&iexcl;In
this case, we recommend change your password as soon as
possible!</span></p>
<p
style="Margin:0;mso-line-height-alt:14px;mso-ansi-font-size:14px;">
<span
style="font-size:14px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:100%;mso-line-height-alt:14px;mso-ansi-font-size:14px;">&nbsp;</span>
</p>
<p
style="Margin:0;mso-line-height-alt:14px;mso-ansi-font-size:14px;">
<span
style="font-size:14px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:100%;mso-line-height-alt:14px;mso-ansi-font-size:14px;">Here's
more information about this login attempt.</span></p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center"
style="background:transparent;font-size:0;padding:0;word-break:break-word;">
<table cellpadding="0" cellspacing="0" width="100%" border="0"
style="color:#000000;font-family:Arial,sans-serif;font-size:13px;line-height:22px;table-layout:fixed;width:100%;border:none;">
<tr>
<td align="left" class="u"
style="padding:0;height:21px;word-wrap:break-word;vertical-align:middle;"
width="49.24242424242424%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="left" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#171717;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">Date</span>
</p>
</td>
</tr>
</table>
</td>
<td style="vertical-align:middle;color:transparent;font-size:0;"
width="1.5151515151515151%">&#8203;
</td>
<td align="center" class="u"
style="border-radius: 12px; padding:5px 10px 5px 10px;height:31px;background-color:#d7d7d7;word-wrap:break-word;vertical-align:middle;"
width="49.24242424242424%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="center" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'DM Mono','Courier',monospace;font-weight:400;color:#222222;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">{{date}}</span>
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr class>
<td style="font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;"
aria-hidden="true">
<div style="height:8px;line-height:8px;">&#8203;</div>
</td>
</tr>
<tr>
<td align="left" class="u"
style="padding:0;height:21px;word-wrap:break-word;vertical-align:middle;"
width="49.24242424242424%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="left" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#171717;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">Client</span>
</p>
</td>
</tr>
</table>
</td>
<td style="vertical-align:middle;color:transparent;font-size:0;"
width="1.5151515151515151%">&#8203;
</td>
<td align="center" class="u"
style="border-radius: 12px; padding:5px 10px 5px 10px;height:31px;background-color:#d7d7d7;word-wrap:break-word;vertical-align:middle;"
width="49.24242424242424%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="center" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'DM Mono','Courier',monospace;font-weight:400;color:#222222;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">{{client}}</span>
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr class>
<td style="font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;"
aria-hidden="true">
<div style="height:8px;line-height:8px;">&#8203;</div>
</td>
</tr>
<tr>
<td align="left" class="u"
style="padding:0;height:21px;word-wrap:break-word;vertical-align:middle;"
width="49.24242424242424%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="left" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#171717;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">IP</span>
</p>
</td>
</tr>
</table>
</td>
<td style="vertical-align:middle;color:transparent;font-size:0;"
width="1.5151515151515151%">&#8203;
</td>
<td align="center" class="u"
style="border-radius: 12px; padding:5px 10px 5px 10px;height:31px;background-color:#d7d7d7;word-wrap:break-word;vertical-align:middle;"
width="49.24242424242424%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="center" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'DM Mono','Courier',monospace;font-weight:400;color:#222222;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">{{ip}}</span>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 32px 16px 32px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:496px;">
<![endif]-->
<div class="m y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0;padding:0;word-break:break-word;">
<p
style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]>
<table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:496px;" role="presentation" width="496px"><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</div>
</body>
</html>

View File

@ -0,0 +1,444 @@
<!doctype html>
<html lang="en" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Comty</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 0;
}
</style>
<!--[if mso]> <noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.y{width:100% !important;}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Inter:400,300" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=DM Mono:400" rel="stylesheet" type="text/css">
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:599px) {
.k {
width: 548px !important;
max-width: 548px;
}
}
</style>
<style media="screen and (min-width:599px)">
.moz-text-html .k {
width: 548px !important;
max-width: 548px;
}
</style>
<style type="text/css">
u+.emailify .gs {
background: #000;
mix-blend-mode: screen;
display: inline-block;
padding: 0;
margin: 0;
}
u+.emailify .gd {
background: #000;
mix-blend-mode: difference;
display: inline-block;
padding: 0;
margin: 0;
}
p {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
}
u+.emailify a {
color: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit !important;
text-decoration: none !important;
}
@media only screen and (max-width:599px) {
.emailify {
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
u+.emailify .glist {
margin-left: 1em !important;
}
td.x {
padding-left: 0 !important;
padding-right: 0 !important;
}
td.u {
height: auto !important;
}
div.r.e>table>tbody>tr>td,
div.r.e>div>table>tbody>tr>td {
padding-right: 16px !important
}
div.r.ys>table>tbody>tr>td,
div.r.ys>div>table>tbody>tr>td {
padding-left: 16px !important
}
}
</style>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<!--[if gte mso 9]>
<style>a:link{mso-style-priority:99;color:inherit;text-decoration:none;}a:visited{mso-style-priority:99;color:inherit;text-decoration:none;}li{text-indent:-1em;}table,td,p,div,span,ul,ol,li,a{mso-hyphenate:none;}sup,sub{font-size:100% !important;}
</style>
<![endif]-->
</head>
<body lang="en" link="#DD0000" vlink="#DD0000" class="emailify"
style="mso-line-height-rule:exactly;mso-hyphenate:none;word-spacing:normal;background-color:#f0f2f5; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; padding: 30px 0;">
<div style="background-color:#fffffe; width: fit-content; border-radius: 20px; overflow: hidden; padding: 30px 0;"
lang="en" dir="auto">
<!--[if mso | IE]>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:548px;">
<![endif]-->
<div class="k y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0;padding:0;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0;">
<tbody>
<tr>
<td style="width:50px;"> <img alt
src="https://storage.ragestudio.net/rstudio/branding/comty/iso/logo_alt.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
width="91" height="auto">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:548px;">
<![endif]-->
<div class="k y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:24px;mso-ansi-font-size:16px;">
<span
style="font-size:16px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;">Hi
@{{username}}</span>
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:transparent;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:transparent;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:548px;">
<![endif]-->
<div class="k y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:18px;mso-ansi-font-size:16px;">
<span
style="font-size:15px;font-family:'Inter','Arial',sans-serif;font-weight:300;color:#000000;line-height:120%;mso-line-height-alt:18px;mso-ansi-font-size:16px;">A
new device has been logged into your account.</span>
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:548px;">
<![endif]-->
<div class="k y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center"
style="background:transparent;font-size:0;padding:0;word-break:break-word;">
<table cellpadding="0" cellspacing="0" width="100%" border="0"
style="color:#000000;font-family:Arial,sans-serif;font-size:13px;line-height:22px;table-layout:fixed;width:100%;border:none;">
<tr>
<td align="left" class="u"
style="padding:0;height:21px;word-wrap:break-word;vertical-align:middle;"
width="49.27007299270073%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="left" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#171717;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">Date</span>
</p>
</td>
</tr>
</table>
</td>
<td style="vertical-align:middle;color:transparent;font-size:0;"
width="1.4598540145985401%">&#8203;
</td>
<td align="center" class="u"
style="border-radius: 12px; padding:5px 10px 5px 10px;height:31px;background-color:#d7d7d7;word-wrap:break-word;vertical-align:middle;"
width="49.27007299270073%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="center" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'DM Mono','Courier',monospace;font-weight:400;color:#222222;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">{{date}}</span>
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr class>
<td style="font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;"
aria-hidden="true">
<div style="height:8px;line-height:8px;">&#8203;</div>
</td>
</tr>
<tr>
<td align="left" class="u"
style="padding:0;height:21px;word-wrap:break-word;vertical-align:middle;"
width="49.27007299270073%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="left" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#171717;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">Client</span>
</p>
</td>
</tr>
</table>
</td>
<td style="vertical-align:middle;color:transparent;font-size:0;"
width="1.4598540145985401%">&#8203;
</td>
<td align="center" class="u"
style="border-radius: 12px; padding:5px 10px 5px 10px;height:31px;background-color:#d7d7d7;word-wrap:break-word;vertical-align:middle;"
width="49.27007299270073%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="center" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'DM Mono','Courier',monospace;font-weight:400;color:#222222;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">{{client}}</span>
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr class>
<td style="font-size:0;padding:0;padding-bottom:0;word-break:break-word;color:transparent;"
aria-hidden="true">
<div style="height:8px;line-height:8px;">&#8203;</div>
</td>
</tr>
<tr>
<td align="left" class="u"
style="padding:0;height:21px;word-wrap:break-word;vertical-align:middle;"
width="49.27007299270073%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="left" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#171717;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">IP</span>
</p>
</td>
</tr>
</table>
</td>
<td style="vertical-align:middle;color:transparent;font-size:0;"
width="1.4598540145985401%">&#8203;
</td>
<td align="center" class="u"
style="border-radius: 12px; padding:5px 10px 5px 10px;height:31px;background-color:#d7d7d7;word-wrap:break-word;vertical-align:middle;"
width="49.27007299270073%">
<table border="0" cellpadding="0" cellspacing="0"
width="100%">
<tr>
<td class="x" align="center" width="100%">
<p
style="Margin:0;text-align:left;mso-ansi-font-size:14px;">
<span
style="font-size:13px;font-family:'DM Mono','Courier',monospace;font-weight:400;color:#222222;line-height:162%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">{{ip}}</span>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</div>
</body>
</html>

View File

@ -0,0 +1,36 @@
<html>
<body>
<h1>Hi @{{username}}</h1>
<p>Your password has been changed successfully.</p>
<span>
Wasn't it you? Please contact us inmediately to recover your account.
<button>
<a href="https://comty.app/support/stolen-account">Start a recover process</a>
</button>
or contact via email
<a href="mailto:support@ragestudio.net">support@ragestudio.net</a>
Request information:
</span>
<table>
<tr>
<td>Date:</td>
<td>{{date}}</td>
</tr>
<tr>
<td>Client:</td>
<td>{{client}}</td>
</tr>
<tr>
<td>IP</td>
<td>{{ip}}</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,457 @@
<!doctype html>
<html lang="en" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title></title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 0;
}
</style>
<!--[if mso]> <noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.y{width:100% !important;}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Inter:400,300" rel="stylesheet" type="text/css">
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:599px) {
.i {
width: 528px !important;
max-width: 528px;
}
.m {
width: 496px !important;
max-width: 496px;
}
}
</style>
<style media="screen and (min-width:599px)">
.moz-text-html .i {
width: 528px !important;
max-width: 528px;
}
.moz-text-html .m {
width: 496px !important;
max-width: 496px;
}
</style>
<style type="text/css">
u+.emailify .gs {
background: #000;
mix-blend-mode: screen;
display: inline-block;
padding: 0;
margin: 0;
}
u+.emailify .gd {
background: #000;
mix-blend-mode: difference;
display: inline-block;
padding: 0;
margin: 0;
}
p {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
}
u+.emailify a {
color: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit !important;
text-decoration: none !important;
}
@media only screen and (max-width:599px) {
.emailify {
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
u+.emailify .glist {
margin-left: 1em !important;
}
td.x {
padding-left: 0 !important;
padding-right: 0 !important;
}
div.r.e>table>tbody>tr>td,
div.r.e>div>table>tbody>tr>td {
padding-right: 16px !important
}
div.r.ys>table>tbody>tr>td,
div.r.ys>div>table>tbody>tr>td {
padding-left: 16px !important
}
}
</style>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<!--[if gte mso 9]>
<style>a:link{mso-style-priority:99;color:inherit;text-decoration:none;}a:visited{mso-style-priority:99;color:inherit;text-decoration:none;}li{text-indent:-1em;}table,td,p,div,span,ul,ol,li,a{mso-hyphenate:none;}sup,sub{font-size:100% !important;}
</style>
<![endif]-->
</head>
<body lang="en" link="#DD0000" vlink="#DD0000" class="emailify"
style="mso-line-height-rule:exactly;mso-hyphenate:none;word-spacing:normal;background-color:#f0f2f5; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; padding: 30px 0;">
<div style="background-color:#fffffe; border-radius: 20px; overflow: hidden; padding: 30px 0;" lang="en" dir="auto">
<!--[if mso | IE]>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0;padding:0;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0;">
<tbody>
<tr>
<td style="width:50px;"> <img alt
src="https://storage.ragestudio.net/rstudio/branding/comty/iso/logo_alt.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
width="91" height="auto">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:24px;mso-ansi-font-size:16px;">
<span
style="font-size:16px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;">Hi
@{{username}}</span></p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:transparent;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:transparent;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:18px;mso-ansi-font-size:16px;">
<span
style="font-size:15px;font-family:'Inter','Arial',sans-serif;font-weight:300;color:#000000;line-height:120%;mso-line-height-alt:18px;mso-ansi-font-size:16px;">It
sounds like you need some help recovering your
account.</span></p>
<p
style="Margin:0;mso-line-height-alt:18px;mso-ansi-font-size:16px;">
<span
style="font-size:15px;font-family:'Inter','Arial',sans-serif;font-weight:300;color:#000000;line-height:120%;mso-line-height-alt:18px;mso-ansi-font-size:16px;">&nbsp;</span>
</p>
<p
style="Margin:0;mso-line-height-alt:18px;mso-ansi-font-size:16px;">
<span
style="font-size:15px;font-family:'Inter','Arial',sans-serif;font-weight:300;color:#000000;line-height:120%;mso-line-height-alt:18px;mso-ansi-font-size:16px;">Don't
worry, here is a link to change your password.</span></p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 32px 16px 32px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:496px;">
<![endif]-->
<div class="m y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0;padding:0;word-break:break-word;">
<p
style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]>
<table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:496px;" role="presentation" width="496px"><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:24px;mso-ansi-font-size:16px;">
<span
style="font-size:16px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;"><a href="{{apr_link}}">{{apr_link}}</a></span>
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:16px 32px 16px 32px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:496px;">
<![endif]-->
<div class="m y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0;padding:0;word-break:break-word;">
<p
style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]>
<table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #cccccc;font-size:1px;margin:0px auto;width:496px;" role="presentation" width="496px"><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
<![endif]-->
<div class="r e ys" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#fffffe;background-color:#fffffe;width:100%;">
<tbody>
<tr>
<td style="border:none;direction:ltr;font-size:0;padding:2px 16px 2px 16px;text-align:left;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:middle;width:528px;">
<![endif]-->
<div class="i y"
style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border:none;vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="left" class="x" style="font-size:0;word-break:break-word;">
<div style="text-align:left;">
<p
style="Margin:0;text-align:left;mso-line-height-alt:22px;mso-ansi-font-size:14px;">
<span
style="font-size:14px;font-family:'Inter','Arial',sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">This
link will only be available for 1 hour</span></p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</div>
</body>
</html>

View File

@ -0,0 +1,140 @@
import { Controller } from "linebridge/dist/server"
import pmap from "p-map"
import getPosts from "./services/getPosts"
import getGlobalReleases from "./services/getGlobalReleases"
import getReleasesFromFollowing from "./services/getReleasesFromFollowing"
import getPlaylistsFromFollowing from "./services/getPlaylistsFromFollowing"
export default class FeedController extends Controller {
static refName = "FeedController"
static useRoute = "/feed"
httpEndpoints = {
get: {
"/timeline": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const for_user_id = req.user?._id.toString()
if (!for_user_id) {
return res.status(400).json({
error: "Invalid user id"
})
}
// fetch posts
let posts = await getPosts({
for_user_id,
limit: req.query?.limit,
skip: req.query?.trim,
})
// add type to posts and playlists
posts = posts.map((data) => {
data.type = "post"
return data
})
let feed = [
...posts,
]
// sort feed
feed.sort((a, b) => {
return new Date(b.created_at) - new Date(a.created_at)
})
return res.json(feed)
}
},
"/music/global": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const for_user_id = req.user?._id.toString()
if (!for_user_id) {
return res.status(400).json({
error: "Invalid user id"
})
}
// fetch playlists from global
const result = await getGlobalReleases({
for_user_id,
limit: req.query?.limit,
skip: req.query?.trim,
})
return res.json(result)
}
},
"/music": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const for_user_id = req.user?._id.toString()
if (!for_user_id) {
return res.status(400).json({
error: "Invalid user id"
})
}
const searchers = [
getGlobalReleases,
//getReleasesFromFollowing,
//getPlaylistsFromFollowing,
]
let result = await pmap(
searchers,
async (fn, index) => {
const data = await fn({
for_user_id,
limit: req.query?.limit,
skip: req.query?.trim,
})
return data
}, {
concurrency: 3,
},)
result = result.reduce((acc, cur) => {
return [...acc, ...cur]
}, [])
return res.json(result)
}
},
"/posts": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const for_user_id = req.user?._id.toString()
if (!for_user_id) {
return res.status(400).json({
error: "Invalid user id"
})
}
let feed = []
// fetch posts
const posts = await getPosts({
for_user_id,
limit: req.query?.limit,
skip: req.query?.trim,
})
feed = feed.concat(posts)
return res.json(feed)
}
},
}
}
}

View File

@ -0,0 +1,25 @@
import { Release } from "@db_models"
export default async (payload) => {
const {
limit = 20,
skip = 0,
} = payload
let releases = await Release.find({
$or: [
{ public: true },
]
})
.sort({ created_at: -1 })
.limit(limit)
.skip(skip)
releases = Promise.all(releases.map(async (release) => {
release = release.toObject()
return release
}))
return releases
}

View File

@ -0,0 +1,42 @@
import { Playlist, User, UserFollow } from "@db_models"
export default async (payload) => {
const {
for_user_id,
limit = 20,
skip = 0,
} = payload
// get post from users that the user follows
const followingUsers = await UserFollow.find({
user_id: for_user_id
})
const followingUserIds = followingUsers.map((followingUser) => followingUser.to)
const fetchFromUserIds = [
for_user_id,
...followingUserIds,
]
// firter out the playlists that are not public
let playlists = await Playlist.find({
user_id: { $in: fetchFromUserIds },
$or: [
{ public: true },
]
})
.sort({ created_at: -1 })
.limit(limit)
.skip(skip)
playlists = Promise.all(playlists.map(async (playlist) => {
playlist = playlist.toObject()
playlist.type = "playlist"
return playlist
}))
return playlists
}

View File

@ -0,0 +1,39 @@
import { Post, UserFollow } from "@db_models"
import fullfillPostsData from "@utils/fullfillPostsData"
export default async (payload) => {
const {
for_user_id,
limit = 20,
skip = 0,
} = payload
// get post from users that the user follows
const followingUsers = await UserFollow.find({
user_id: for_user_id
})
const followingUserIds = followingUsers.map((followingUser) => followingUser.to)
const fetchPostsFromIds = [
for_user_id,
...followingUserIds,
]
let posts = await Post.find({
user_id: { $in: fetchPostsFromIds }
})
.sort({ created_at: -1 })
.limit(limit)
.skip(skip)
// fullfill data
posts = await fullfillPostsData({
posts,
for_user_id,
skip,
})
return posts
}

View File

@ -0,0 +1,40 @@
import { Release, UserFollow } from "@db_models"
export default async (payload) => {
const {
for_user_id,
limit = 20,
skip = 0,
} = payload
// get post from users that the user follows
const followingUsers = await UserFollow.find({
user_id: for_user_id
})
const followingUserIds = followingUsers.map((followingUser) => followingUser.to)
const fetchFromUserIds = [
for_user_id,
...followingUserIds,
]
// firter out the releases that are not public
let releases = await Release.find({
user_id: { $in: fetchFromUserIds },
$or: [
{ public: true },
]
})
.sort({ created_at: -1 })
.limit(limit)
.skip(skip)
releases = Promise.all(releases.map(async (release) => {
release = release.toObject()
return release
}))
return releases
}

View File

@ -0,0 +1,25 @@
import { Server } from "linebridge/src/server"
import DbManager from "@shared-classes/DbManager"
import SharedMiddlewares from "@shared-middlewares"
export default class API extends Server {
static refName = "feed"
static useEngine = "hyper-express"
static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3007
middlewares = {
...SharedMiddlewares
}
contexts = {
db: new DbManager(),
}
async onInitialize() {
await this.contexts.db.initialize()
}
}
Boot(API)

View File

@ -0,0 +1,6 @@
{
"name": "feed",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}

View File

@ -1,188 +1,40 @@
import fs from "fs" import { Server } from "linebridge/src/server"
import path from "path"
import cors from "cors"
import express from "express"
import B2 from "backblaze-b2" import B2 from "backblaze-b2"
import RedisClient from "@shared-classes/RedisClient" import RedisClient from "@shared-classes/RedisClient"
import StorageClient from "@shared-classes/StorageClient" import StorageClient from "@shared-classes/StorageClient"
import CacheService from "@shared-classes/CacheService" import CacheService from "@shared-classes/CacheService"
import ComtyClient from "@shared-classes/ComtyClient"
import pkg from "./package.json" import SharedMiddlewares from "@shared-middlewares"
global.DEFAULT_HEADERS = { class API extends Server {
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, provider-type, Provider-Type", static refName = "files"
"Access-Control-Allow-Origin": "*", static useEngine = "hyper-express"
"Access-Control-Allow-Methods": "GET, POST, OPTIONS, PUT, PATCH, DELETE, DEL", static routesPath = `${__dirname}/routes`
"Access-Control-Allow-Credentials": "true", static listen_port = process.env.HTTP_LISTEN_PORT ?? 3008
}
global.DEFAULT_MIDDLEWARES = [
cors({
"origin": "*",
"methods": DEFAULT_HEADERS["Access-Control-Allow-Methods"],
"preflightContinue": true,
"optionsSuccessStatus": 204,
}),
]
export default class FileServerAPI {
// max body length is 1GB in bytes
static maxBodyLength = 1000 * 1000 * 1000 static maxBodyLength = 1000 * 1000 * 1000
internalRouter = express.Router() middlewares = {
...SharedMiddlewares
server = global.server = express()
listenIp = process.env.HTTP_LISTEN_IP ?? "0.0.0.0"
listenPort = process.env.HTTP_LISTEN_PORT ?? 3002
redis = global.redis = RedisClient()
storage = global.storage = StorageClient()
b2Storage = global.b2Storage = new B2({
applicationKeyId: process.env.B2_KEY_ID,
applicationKey: process.env.B2_APP_KEY,
})
cache = global.cache = new CacheService()
comty = global.comty = ComtyClient({
useWs: false,
})
async __loadControllers() {
let controllersPath = fs.readdirSync(path.resolve(__dirname, "controllers"))
for await (const controllerPath of controllersPath) {
const controller = require(path.resolve(__dirname, "controllers", controllerPath)).default
if (!controller) {
this.server.InternalConsole.error(`Controller ${controllerPath} not found.`)
continue
}
const handler = await controller(express.Router())
if (!handler) {
this.server.InternalConsole.error(`Controller ${controllerPath} returning not valid handler.`)
continue
}
this.internalRouter.use(handler.path ?? "/", handler.router)
continue
}
} }
async __loadMiddlewares() { contexts = {
let middlewaresPath = fs.readdirSync(path.resolve(__dirname, "useMiddlewares")) cache: new CacheService(),
redis: RedisClient(),
if (this.constructor.useMiddlewaresOrder) { storage: StorageClient(),
middlewaresPath = middlewaresPath.sort((a, b) => { b2Storage: new B2({
const aIndex = this.constructor.useMiddlewaresOrder.indexOf(a.replace(".js", "")) applicationKeyId: process.env.B2_KEY_ID,
const bIndex = this.constructor.useMiddlewaresOrder.indexOf(b.replace(".js", "")) applicationKey: process.env.B2_APP_KEY,
}),
if (aIndex === -1) {
return 1
}
if (bIndex === -1) {
return -1
}
return aIndex - bIndex
})
}
for await (const middlewarePath of middlewaresPath) {
const middleware = require(path.resolve(__dirname, "useMiddlewares", middlewarePath)).default
if (!middleware) {
this.server.InternalConsole.error(`Middleware ${middlewarePath} not found.`)
continue
}
this.server.use(middleware)
}
} }
__getRegisteredRoutes() { async onInitialize() {
return this.internalRouter.routes.map((route) => { await this.contexts.redis.initialize()
return { await this.contexts.storage.initialize()
method: route.method, await this.contexts.b2Storage.authorize()
path: route.pattern,
}
})
}
__registerInternalRoutes() {
this.internalRouter.get("/", (req, res) => {
return res.status(200).json({
name: pkg.name,
version: pkg.version,
})
})
// this.internalRouter.get("/routes", (req, res) => {
// return res.status(200).json(this.__getRegisteredRoutes())
// })
this.internalRouter.get("*", (req, res) => {
return res.status(404).json({
error: "Not found",
})
})
}
async initialize() {
const startHrTime = process.hrtime()
// initialize clients
await this.redis.initialize()
await this.storage.initialize()
await this.b2Storage.authorize()
this.server.use((req, res, next) => {
Object.keys(global.DEFAULT_HEADERS).forEach((key) => {
res.setHeader(key, global.DEFAULT_HEADERS[key])
res.header[key] = global.DEFAULT_HEADERS[key]
})
next()
})
global.DEFAULT_MIDDLEWARES.forEach((middleware) => {
this.server.use(middleware)
})
this.server.use(express.json({ extended: false }))
this.server.use(express.urlencoded({ extended: true }))
// register controllers & middlewares
await this.__loadControllers()
await this.__loadMiddlewares()
await this.__registerInternalRoutes()
// use internal router
this.server.use(this.internalRouter)
// start server
await this.server.listen(this.listenPort, this.listenIp)
// calculate elapsed time
const elapsedHrTime = process.hrtime(startHrTime)
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
// log server started
console.log(`🚀 Server started ready on \n\t - http://${this.listenIp}:${this.listenPort} \n\t - Tooks ${elapsedTimeInMs}ms`)
} }
} }
Boot(FileServerAPI) Boot(API)

View File

@ -1,25 +0,0 @@
export default async function (req, res, next) {
// extract authentification header
let auth = req.headers.authorization
if (!auth) {
return res.status(401).json({ error: "Unauthorized, missing token" })
}
auth = auth.replace("Bearer ", "")
// check if authentification is valid
const validation = await comty.rest.session.validateToken(auth).catch((error) => {
return {
valid: false,
}
})
if (!validation.valid) {
return res.status(401).json({ error: "Unauthorized" })
}
req.session = validation.data
return next()
}

View File

@ -1,23 +0,0 @@
export default function (req, res, next) {
// extract authentification header
let auth = req.headers.authorization
if (!auth) {
return next()
}
auth = req.sessionToken = auth.replace("Bearer ", "")
// check if authentification is valid
comty.rest.session.validateToken(auth)
.catch((error) => {
return {
valid: false,
}
})
.then((validation) => {
req.session = validation.data
next()
})
}

View File

@ -1,55 +0,0 @@
export default async (socket, next) => {
try {
const token = socket.handshake.auth.token
if (!token) {
return next(new Error(`auth:token_missing`))
}
const validation = await global.comty.rest.session.validateToken(token).catch((err) => {
console.error(`[${socket.id}] failed to validate session caused by server error`, err)
return {
valid: false,
error: err,
}
})
if (!validation.valid) {
if (validation.error) {
return next(new Error(`auth:server_error`))
}
return next(new Error(`auth:token_invalid`))
}
const session = validation.data
const userData = await global.comty.rest.user.data({
user_id: session.user_id,
}).catch((err) => {
console.error(`[${socket.id}] failed to get user data caused by server error`, err)
return null
})
if (!userData) {
return next(new Error(`auth:user_failed`))
}
try {
socket.userData = userData
socket.token = token
socket.session = session
}
catch (err) {
return next(new Error(`auth:decode_failed`))
}
next()
} catch (error) {
console.error(`[${socket.id}] failed to connect caused by server error`, error)
next(new Error(`auth:authentification_failed`))
}
}

View File

@ -20,7 +20,7 @@
"luxon": "^3.0.4", "luxon": "^3.0.4",
"merge-files": "^0.1.2", "merge-files": "^0.1.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"sharp": "^0.33.2", "sharp": "0.32.6",
"minio": "^7.0.32", "minio": "^7.0.32",
"moment": "^2.29.4", "moment": "^2.29.4",
"moment-timezone": "^0.5.40", "moment-timezone": "^0.5.40",

View File

@ -0,0 +1,25 @@
import mimetypes from "mime-types"
export default {
useContext: ["storage"],
fn: async (req, res) => {
const streamPath = req.path.replace(req.route.pattern.replace("*", ""), "/")
this.default.contexts.storage.getObject(process.env.S3_BUCKET, streamPath, (err, dataStream) => {
if (err) {
return res.status(404).end()
}
const extname = mimetypes.lookup(streamPath)
// send chunked response
res.status(200)
// set headers
res.setHeader("Content-Type", extname)
res.setHeader("Accept-Ranges", "bytes")
return dataStream.pipe(res)
})
}
}

View File

@ -0,0 +1,104 @@
import path from "path"
import fs from "fs"
import FileUpload from "@shared-classes/FileUpload"
import PostProcess from "@services/post-process"
export default {
useContext: ["cache", "storage", "b2Storage"],
middlewares: [
"withAuthentication",
],
fn: async (req, res) => {
const { cache, storage, b2Storage } = this.default.contexts
const providerType = req.headers["provider-type"]
const userPath = path.join(cache.constructor.cachePath, req.session.user_id)
// 10 GB in bytes
const maxFileSize = 10 * 1000 * 1000 * 1000
// 10MB in bytes
const maxChunkSize = 10 * 1000 * 1000
let build = await FileUpload(req, userPath, maxFileSize, maxChunkSize)
.catch((err) => {
console.log("err", err)
throw new OperationError(500, err.message)
})
if (build === false) {
return false
} else {
if (typeof build === "function") {
try {
build = await build()
if (!req.headers["no-compression"]) {
build = await PostProcess(build)
}
// compose remote path
const remotePath = `${req.session.user_id}/${path.basename(build.filepath)}`
let url = null
switch (providerType) {
case "premium-cdn": {
// use backblaze b2
await b2Storage.authorize()
const uploadUrl = await b2Storage.getUploadUrl({
bucketId: process.env.B2_BUCKET_ID,
})
const data = await fs.promises.readFile(build.filepath)
await b2Storage.uploadFile({
uploadUrl: uploadUrl.data.uploadUrl,
uploadAuthToken: uploadUrl.data.authorizationToken,
fileName: remotePath,
data: data,
info: build.metadata
})
url = `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${remotePath}`
break
}
default: {
// upload to storage
await storage.fPutObject(process.env.S3_BUCKET, remotePath, build.filepath, build.metadata ?? {
"Content-Type": build.mimetype,
})
// compose url
url = storage.composeRemoteURL(remotePath)
break
}
}
// remove from cache
fs.promises.rm(build.cachePath, { recursive: true, force: true })
return res.json({
name: build.filename,
id: remotePath,
url: url,
})
} catch (error) {
console.log(error)
throw new OperationError(500, error.message)
}
}
return res.json({
success: true,
})
}
}
}

View File

@ -1,8 +0,0 @@
import cors from "cors"
export default cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"],
preflightContinue: false,
optionsSuccessStatus: 204,
})

View File

@ -1,19 +0,0 @@
export default (req, res, next) => {
const startHrTime = process.hrtime()
res.on("finish", () => {
const elapsedHrTime = process.hrtime(startHrTime)
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
res._responseTimeMs = elapsedTimeInMs
// cut req.url if is too long
if (req.url.length > 100) {
req.url = req.url.substring(0, 100) + "..."
}
console.log(`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${req.url} ${elapsedTimeInMs}ms`)
})
next()
}

View File

@ -1,45 +0,0 @@
import fs from "fs"
function createRoutesFromDirectory(startFrom, directoryPath, router) {
const files = fs.readdirSync(directoryPath)
files.forEach((file) => {
const filePath = `${directoryPath}/${file}`
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
createRoutesFromDirectory(startFrom, filePath, router)
} else if (file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".ts") || file.endsWith(".tsx")) {
let splitedFilePath = filePath.split("/")
// slice the startFrom path
splitedFilePath = splitedFilePath.slice(splitedFilePath.indexOf(startFrom) + 1)
const method = splitedFilePath[0]
let route = splitedFilePath.slice(1, splitedFilePath.length).join("/")
route = route.replace(".jsx", "")
route = route.replace(".js", "")
route = route.replace(".ts", "")
route = route.replace(".tsx", "")
if (route === "index") {
route = "/"
} else {
route = `/${route}`
}
let handler = require(filePath)
handler = handler.default || handler
router[method](route, handler)
}
})
return router
}
export default createRoutesFromDirectory

View File

@ -1,46 +0,0 @@
import fs from "node:fs"
import path from "node:path"
export default async (middlewares, middlewaresPath) => {
if (typeof middlewaresPath === "undefined") {
middlewaresPath = path.resolve(globalThis["__src"], "middlewares")
}
if (!fs.existsSync(middlewaresPath)) {
return undefined
}
if (typeof middlewares === "string") {
middlewares = [middlewares]
}
let fns = []
for await (const middlewareName of middlewares) {
const middlewarePath = path.resolve(middlewaresPath, middlewareName)
if (!fs.existsSync(middlewarePath)) {
console.error(`Middleware ${middlewareName} not found.`)
continue
}
const middleware = require(middlewarePath).default
if (!middleware) {
console.error(`Middleware ${middlewareName} not valid export.`)
continue
}
if (typeof middleware !== "function") {
console.error(`Middleware ${middlewareName} not valid function.`)
continue
}
fns.push(middleware)
}
return fns
}

View File

@ -1,18 +0,0 @@
import resolveUrl from "@utils/resolveUrl"
export default (code, rootURL) => {
const importRegex = /import\s+(?:(?:([\w*\s{},]*)\s+from\s+)?["']([^"']*)["']|["']([^"']*)["'])/g
// replaces all imports with absolute paths
const absoluteImportCode = code.replace(importRegex, (match, p1, p2) => {
let resolved = resolveUrl(rootURL, p2)
if (!p1) {
return `import "${resolved}"`
}
return `import ${p1} from "${resolved}"`
})
return absoluteImportCode
}

View File

@ -1,4 +1,4 @@
import { User, Session, Post } from "@shared-classes/DbModels" import { User, Session, Post } from "@db_models"
export default { export default {
method: "GET", method: "GET",

View File

@ -1,4 +1,4 @@
import { FeaturedWallpaper } from "@shared-classes/DbModels" import { FeaturedWallpaper } from "@db_models"
export default { export default {
method: "DELETE", method: "DELETE",

View File

@ -1,4 +1,4 @@
import { FeaturedWallpaper } from "@shared-classes/DbModels" import { FeaturedWallpaper } from "@db_models"
import momentTimezone from "moment-timezone" import momentTimezone from "moment-timezone"
export default { export default {

View File

@ -1,4 +1,4 @@
import { User } from "@shared-classes/DbModels" import { User } from "@db_models"
import bcrypt from "bcrypt" import bcrypt from "bcrypt"

View File

@ -1,4 +1,4 @@
import { User } from "@shared-classes/DbModels" import { User } from "@db_models"
export default { export default {
method: "POST", method: "POST",

View File

@ -1,37 +0,0 @@
import { User } from "@shared-classes/DbModels"
export default {
method: "GET",
route: "/login/validation",
fn: async function (req, res) {
// just check if the provided user or/and email exists, if is return false, otherwise return true
const { username, email } = req.query
if (!username && !email) {
return res.status(400).json({
message: "Missing username or email",
})
}
const user = await User.findOne({
$or: [
{ username: username },
{ email: email },
]
}).catch((error) => {
return false
})
if (user) {
return res.json({
message: "User already exists",
exists: true,
})
} else {
return res.json({
message: "User doesn't exists",
exists: false,
})
}
}
}

View File

@ -1,7 +0,0 @@
export default {
method: "GET",
route: "/otp/verify",
fn: async function (req, res) {
}
}

View File

@ -1,39 +0,0 @@
import Token from "@lib/token"
import { User } from "@shared-classes/DbModels"
import bcrypt from "bcrypt"
export default {
method: "POST",
route: "/login",
fn: async (req, res) => {
const { username, password } = req.body
let isEmail = username.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
let query = isEmail ? { email: username } : { username: username }
const user = await User.findOne(query).select("+password")
if (!user) {
return res.status(401).json({
message: "Invalid credentials, user not found",
})
}
if (!bcrypt.compareSync(password, user.password)) {
return res.status(401).json({
message: "Invalid credentials",
})
}
const token = await Token.createAuth({
username: user.username,
user_id: user._id.toString(),
ip_address: req.headers["x-forwarded-for"]?.split(",")[0] ?? req.socket.remoteAddress,
client: req.headers["user-agent"],
signLocation: global.signLocation,
})
return res.json({ token: token })
}
}

View File

@ -1,33 +0,0 @@
import { Session } from "@shared-classes/DbModels"
export default {
method: "POST",
route: "/logout",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const { token, user_id } = req.body
if (typeof user_id === "undefined") {
return res.status(400).json({
message: "No user_id provided",
})
}
if (typeof token === "undefined") {
return res.status(400).json({
message: "No token provided",
})
}
const session = await Session.findOneAndDelete({ user_id, token })
if (session) {
return res.json({
message: "Session deleted",
})
}
return res.status(404).json({
message: "Session not found",
})
},
}

View File

@ -1,28 +0,0 @@
import { Schematized } from "@lib"
import createUser from "../methods/createUser"
export default {
method: "POST",
route: "/register",
fn: Schematized({
required: ["username", "email", "password"],
select: ["username", "email", "password", "fullName"],
}, async (req, res) => {
const result = await createUser(req.selection).catch((err) => {
console.error(err)
res.status(500).json({
message: `Error creating user > ${err.message}`,
})
return false
})
if (!result) {
return false
}
return res.json(result)
})
}

View File

@ -1,9 +0,0 @@
import { Controller } from "linebridge/dist/server"
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
export default class AuthController extends Controller {
static refName = "AuthController"
static useRoute = "/auth"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1,56 +0,0 @@
import { User } from "@shared-classes/DbModels"
import bcrypt from "bcrypt"
export default async function (payload) {
let { username, password, email, fullName, roles, avatar } = payload
// if username has capital letters, throw error
if (username !== username.toLowerCase()) {
throw new Error("Username must be lowercase")
}
// make sure the username has no spaces
if (username.includes(" ")) {
throw new Error("Username cannot contain spaces")
}
// make sure the username has no valid characters. Only letters, numbers, and underscores
if (!/^[a-z0-9_]+$/.test(username)) {
throw new Error("Username can only contain letters, numbers, and underscores")
}
// check if username is already taken
const existentUser = await User.findOne({ username: username })
if (existentUser) {
throw new Error("User already exists")
}
// check if the email is already in use
const existentEmail = await User.findOne({ email: email })
if (existentEmail) {
throw new Error("Email already in use")
}
// hash the password
const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3))
// create the doc
let user = new User({
username: username,
password: hash,
email: email,
fullName: fullName,
avatar: avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
roles: roles,
createdAt: new Date().getTime(),
})
await user.save()
// dispatch event bus
global.eventBus.emit("user.create", user)
return user
}

View File

@ -1,4 +1,4 @@
import { Badge } from "@shared-classes/DbModels" import { Badge } from "@db_models"
export default { export default {
method: "DELETE", method: "DELETE",

View File

@ -1,5 +1,5 @@
import { Schematized } from "@lib" import { Schematized } from "@lib"
import { Badge } from "@shared-classes/DbModels" import { Badge } from "@db_models"
export default { export default {
method: "GET", method: "GET",

View File

@ -1,4 +1,4 @@
import { User, Badge } from "@shared-classes/DbModels" import { User, Badge } from "@db_models"
export default { export default {
method: "GET", method: "GET",

View File

@ -1,4 +1,4 @@
import { Badge, User } from "@shared-classes/DbModels" import { Badge, User } from "@db_models"
import { Schematized } from "@lib" import { Schematized } from "@lib"
export default { export default {

View File

@ -1,4 +1,4 @@
import { Badge } from "@shared-classes/DbModels" import { Badge } from "@db_models"
import { Schematized } from "@lib" import { Schematized } from "@lib"
export default { export default {

Some files were not shown because too many files have changed in this diff Show More