mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
merge from local
This commit is contained in:
parent
b6a4942fd3
commit
6aba03e310
@ -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()
|
||||||
|
@ -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() {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SyncEntry } from "@shared-classes/DbModels"
|
import { SyncEntry } from "../../db_models"
|
||||||
|
|
||||||
import crypto from "crypto"
|
import crypto from "crypto"
|
||||||
|
|
||||||
|
17
packages/server/db_models/aprSession/index.js
Normal file
17
packages/server/db_models/aprSession/index.js
Normal 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 },
|
||||||
|
}
|
||||||
|
}
|
13
packages/server/db_models/mfaSessions/index.js
Normal file
13
packages/server/db_models/mfaSessions/index.js
Normal 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 },
|
||||||
|
}
|
||||||
|
}
|
13
packages/server/db_models/operationLog/index.js
Normal file
13
packages/server/db_models/operationLog/index.js
Normal 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 },
|
||||||
|
}
|
||||||
|
}
|
8
packages/server/db_models/userConfig/index.js
Normal file
8
packages/server/db_models/userConfig/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
name: "UserConfig",
|
||||||
|
collection: "user_config",
|
||||||
|
schema: {
|
||||||
|
user_id: { type: String, required: true },
|
||||||
|
values: { type: Object, default: {} },
|
||||||
|
}
|
||||||
|
}
|
@ -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") {
|
||||||
|
@ -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")
|
||||||
|
@ -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",
|
||||||
|
7
packages/server/services/auth/classes/account/index.js
Normal file
7
packages/server/services/auth/classes/account/index.js
Normal 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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { Session } from "@shared-classes/DbModels"
|
import { Session } from "@db_models"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
middlewares: ["withAuthentication"],
|
middlewares: ["withAuthentication"],
|
||||||
|
23
packages/server/services/auth/routes/auth/password/put.js
Normal file
23
packages/server/services/auth/routes/auth/password/put.js
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 }
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
}
|
||||||
|
}
|
49
packages/server/services/auth/routes/forgot/post.js
Normal file
49
packages/server/services/auth/routes/forgot/post.js
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
}
|
@ -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)
|
26
packages/server/services/ems/ipcEvents/aprSend.js
Normal file
26
packages/server/services/ems/ipcEvents/aprSend.js
Normal 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
|
||||||
|
}
|
27
packages/server/services/ems/ipcEvents/mfaSend.js
Normal file
27
packages/server/services/ems/ipcEvents/mfaSend.js
Normal 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
|
||||||
|
}
|
24
packages/server/services/ems/ipcEvents/newLogin.js
Normal file
24
packages/server/services/ems/ipcEvents/newLogin.js
Normal 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
|
||||||
|
}
|
24
packages/server/services/ems/ipcEvents/passwordChanged.js
Normal file
24
packages/server/services/ems/ipcEvents/passwordChanged.js
Normal 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
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
export default (req, res) => {
|
|
||||||
return res.json({
|
|
||||||
message: "Hi! from the deeps."
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export default async (req, res) => {
|
|
||||||
return res.json({
|
|
||||||
msg: "HI!!"
|
|
||||||
})
|
|
||||||
}
|
|
11
packages/server/services/ems/templates/index.js
Normal file
11
packages/server/services/ems/templates/index.js
Normal 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")),
|
||||||
|
}
|
673
packages/server/services/ems/templates/mfa_code/index.handlebars
Normal file
673
packages/server/services/ems/templates/mfa_code/index.handlebars
Normal 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;">
|
||||||
|
</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;">
|
||||||
|
</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;"> </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;">¡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;"> </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%">​
|
||||||
|
</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;">​</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%">​
|
||||||
|
</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;">​</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%">​
|
||||||
|
</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;">
|
||||||
|
</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>
|
@ -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%">​
|
||||||
|
</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;">​</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%">​
|
||||||
|
</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;">​</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%">​
|
||||||
|
</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>
|
@ -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>
|
@ -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;"> </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;">
|
||||||
|
</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;">
|
||||||
|
</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>
|
140
packages/server/services/feed/FeedController/index.js
Executable file
140
packages/server/services/feed/FeedController/index.js
Executable 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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
packages/server/services/feed/FeedController/services/getGlobalReleases.js
Executable file
25
packages/server/services/feed/FeedController/services/getGlobalReleases.js
Executable 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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
39
packages/server/services/feed/FeedController/services/getPosts.js
Executable file
39
packages/server/services/feed/FeedController/services/getPosts.js
Executable 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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
25
packages/server/services/feed/feed.service.js
Normal file
25
packages/server/services/feed/feed.service.js
Normal 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)
|
6
packages/server/services/feed/package.json
Normal file
6
packages/server/services/feed/package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "feed",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
@ -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)
|
@ -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()
|
|
||||||
}
|
|
@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
@ -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`))
|
|
||||||
}
|
|
||||||
}
|
|
@ -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",
|
||||||
|
25
packages/server/services/files/routes/stream/[$]/get.js
Normal file
25
packages/server/services/files/routes/stream/[$]/get.js
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
104
packages/server/services/files/routes/upload/chunk/post.js
Normal file
104
packages/server/services/files/routes/upload/chunk/post.js
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
|
||||||
})
|
|
@ -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()
|
|
||||||
}
|
|
@ -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
|
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FeaturedWallpaper } from "@shared-classes/DbModels"
|
import { FeaturedWallpaper } from "@db_models"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
@ -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 {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { User } from "@shared-classes/DbModels"
|
import { User } from "@db_models"
|
||||||
|
|
||||||
import bcrypt from "bcrypt"
|
import bcrypt from "bcrypt"
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { User } from "@shared-classes/DbModels"
|
import { User } from "@db_models"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/otp/verify",
|
|
||||||
fn: async function (req, res) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 })
|
|
||||||
}
|
|
||||||
}
|
|
@ -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",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import { Badge } from "@shared-classes/DbModels"
|
import { Badge } from "@db_models"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user