This commit is contained in:
Troplo 2022-08-07 00:35:00 +10:00
parent b292277ec4
commit 0d6e209fc6
22 changed files with 424 additions and 207 deletions

View file

@ -1,15 +1,3 @@
HOSTNAME=localhost .ENV is no longer in use, replaced with:
CORS_HOSTNAME=http://localhost:8080 - config/database.json - Database credentials
RELEASE=stable - config/config.json - Colubrina configuration (used to be .env)
NOTIFICATION=
NOTIFICATION_TYPE=info
SITE_NAME=Colubrina
ALLOW_REGISTRATIONS=true
PUBLIC_USERS=false
EMAIL_VERIFICATION=false
EMAIL_SMTP_HOST=smtp.myhost.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=colubrina@myhost.com
EMAIL_SMTP_FROM=colubrina@myhost.com
EMAIL_SMTP_PASSWORD=myPassword
EMAIL_SMTP_SECURE=false

1
backend/.gitignore vendored
View file

@ -61,6 +61,7 @@ yarn-error.log*
# Config folder # Config folder
config/config.json config/config.json
config/database.json
# Editor directories and files # Editor directories and files
.idea .idea
*.suo *.suo

5
backend/.sequelizerc Normal file
View file

@ -0,0 +1,5 @@
var path = require('path')
module.exports = {
'config': path.resolve('config/database.json'),
}

View file

@ -1,23 +1,18 @@
{ {
"development": { "hostname": "localhost",
"username": "username", "corsHostname": "http://localhost:8080",
"password": "password", "siteName": "Colubrina",
"database": "database", "allowRegistrations": true,
"host": "host", "notification": "",
"dialect": "mariadb" "notificationType": "",
}, "release": "stable",
"test": { "publicUsers": true,
"username": "username", "emailVerification": false,
"password": "password", "emailSMTPHost": "mail.example.com",
"database": "database", "emailSMTPPort": 587,
"host": "host", "emailSMTPUser": "colubrina@example.com",
"dialect": "mariadb" "emailSMTPFrom": "colubrina@example.com",
}, "emailSMTPPassword": "",
"production": { "emailSMTPSecure": false,
"username": "username", "rules": "Write your instance rules here."
"password": "password",
"database": "database",
"host": "host",
"dialect": "mariadb"
}
} }

View file

@ -0,0 +1,23 @@
{
"development": {
"username": "username",
"password": "password",
"database": "database",
"host": "host",
"dialect": "mariadb"
},
"test": {
"username": "username",
"password": "password",
"database": "database",
"host": "host",
"dialect": "mariadb"
},
"production": {
"username": "username",
"password": "password",
"database": "database",
"host": "host",
"dialect": "mariadb"
}
}

View file

@ -5,6 +5,7 @@ let app = express()
let bodyParser = require("body-parser") let bodyParser = require("body-parser")
let os = require("os") let os = require("os")
app.set("trust proxy", true) app.set("trust proxy", true)
app.locals.config = require("./config/config.json")
const socket = require("./lib/socket") const socket = require("./lib/socket")
const server = require("http").createServer(app) const server = require("http").createServer(app)
@ -22,15 +23,17 @@ app.use("/api/v1/mediaproxy", require("./routes/mediaproxy.js"))
app.use("/api/v1/associations", require("./routes/associations.js")) app.use("/api/v1/associations", require("./routes/associations.js"))
app.get("/api/v1/state", async (req, res) => { app.get("/api/v1/state", async (req, res) => {
res.json({ res.json({
release: process.env.RELEASE, release: req.app.locals.config.release,
loading: true, notification: req.app.locals.config.notification,
notification: process.env.NOTIFICATION, notificationType: req.app.locals.config.notificationType,
notificationType: process.env.NOTIFICATION_TYPE,
latestVersion: require("../frontend/package.json").version, latestVersion: require("../frontend/package.json").version,
name: process.env.SITE_NAME, allowRegistrations: req.app.locals.config.allowRegistrations,
allowRegistrations: process.env.ALLOW_REGISTRATIONS === "true", publicUsers: req.app.locals.config.publicUsers,
publicUsers: process.env.PUBLIC_USERS === "true", emailVerification: req.app.locals.config.emailVerification,
emailVerification: process.env.EMAIL_VERIFICATION === "true" rules: req.app.locals.config.rules,
name: req.app.locals.config.siteName,
loading: true,
isColubrinaServer: true
}) })
}) })

View file

@ -4,7 +4,7 @@ module.exports = {
init(app, server) { init(app, server) {
const io = require("socket.io")(server, { const io = require("socket.io")(server, {
cors: { cors: {
origin: [process.env.CORS_HOSTNAME], origin: [app.locals.config.corsHostname],
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"] methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]
} }
}) })
@ -19,10 +19,15 @@ module.exports = {
console.log(socket.user.id) console.log(socket.user.id)
socket.join(user.id) socket.join(user.id)
socket.emit("siteState", { socket.emit("siteState", {
release: process.env.RELEASE, release: app.locals.config.release,
notification: process.env.NOTIFICATION, notification: app.locals.config.notification,
notificationType: process.env.NOTIFICATION_TYPE, notificationType: app.locals.config.notificationType,
latestVersion: require("../../frontend/package.json").version latestVersion: require("../../frontend/package.json").version,
allowRegistrations: app.locals.config.allowRegistrations,
publicUsers: app.locals.config.publicUsers,
emailVerification: app.locals.config.emailVerification,
rules: app.locals.config.rules,
name: app.locals.config.siteName
}) })
const friends = await Friend.findAll({ const friends = await Friend.findAll({
where: { where: {
@ -114,10 +119,15 @@ module.exports = {
} else { } else {
socket.join(-1) socket.join(-1)
socket.emit("siteState", { socket.emit("siteState", {
release: process.env.RELEASE, release: app.locals.config.release,
notification: process.env.NOTIFICATION, notification: app.locals.config.notification,
notificationType: process.env.NOTIFICATION_TYPE, notificationType: app.locals.config.notificationType,
latestVersion: require("../../frontend/package.json").version latestVersion: require("../../frontend/package.json").version,
allowRegistrations: app.locals.config.allowRegistrations,
publicUsers: app.locals.config.publicUsers,
emailVerification: app.locals.config.emailVerification,
rules: app.locals.config.rules,
name: app.locals.config.siteName
}) })
socket.emit("unauthorized", { socket.emit("unauthorized", {
message: "Please reauth." message: "Please reauth."

View file

@ -5,7 +5,7 @@ const path = require("path")
const Sequelize = require("sequelize") const Sequelize = require("sequelize")
const basename = path.basename(__filename) const basename = path.basename(__filename)
const env = process.env.NODE_ENV || "development" const env = process.env.NODE_ENV || "development"
const config = require(__dirname + "/../config/config.json")[env] const config = require(__dirname + "/../config/database.json")[env]
const db = {} const db = {}
let sequelize let sequelize

View file

@ -5,7 +5,7 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"scripts": { "scripts": {
"start": "node .", "start": "node .",
"serve": "nodemon" "serve": "nodemon . --ignore config"
}, },
"dependencies": { "dependencies": {
"argon2": "^0.28.7", "argon2": "^0.28.7",

View file

@ -7,6 +7,7 @@ const { Op } = require("sequelize")
const dayjs = require("dayjs") const dayjs = require("dayjs")
const fs = require("fs") const fs = require("fs")
const os = require("os") const os = require("os")
const argon2 = require("argon2")
router.all("*", auth, async (req, res, next) => { router.all("*", auth, async (req, res, next) => {
try { try {
@ -22,7 +23,7 @@ router.all("*", auth, async (req, res, next) => {
router.all("*", auth, async (req, res, next) => { router.all("*", auth, async (req, res, next) => {
try { try {
if (!req.user.emailVerified && process.env.EMAIL_VERIFICATION === "true") { if (!req.user.emailVerified && req.app.locals.config.emailVerification) {
throw Errors.emailVerificationRequired throw Errors.emailVerificationRequired
} else { } else {
next() next()
@ -53,8 +54,45 @@ router.get("/", auth, async (req, res, next) => {
} }
}) })
}) })
} catch (err) { } catch (e) {
return next(err) next(e)
}
})
router.put("/user/:id", auth, async (req, res, next) => {
try {
const user = await User.findOne({
where: {
id: req.params.id
}
})
if (!user) {
throw Errors.communicationsUserNotFound
} else {
await user.update({
banned: req.body.banned
})
res.json(user)
}
} catch (e) {
next(e)
}
})
router.post("/user", auth, async (req, res, next) => {
try {
const user = await User.create({
username: req.body.username,
password: await argon2.hash(req.body.password),
email: req.body.email,
emailVerified: req.body.emailVerified,
admin: false,
banned: false,
lastSeenAt: new Date()
})
res.json(user)
} catch (e) {
next(e)
} }
}) })
@ -231,19 +269,23 @@ router.put("/state", auth, async (req, res, next) => {
} }
try { try {
const io = req.app.get("io") const io = req.app.get("io")
setEnvValue("ALLOW_REGISTRATIONS", req.body.allowRegistrations) req.app.locals.config.allowRegistrations = req.body.allowRegistrations
req.app.locals.config.rules = req.body.rules
if (req.body.broadcastType === "permanent") { if (req.body.broadcastType === "permanent") {
setEnvValue("NOTIFICATION", req.body.notification) req.app.locals.config.notification = req.body.notification
setEnvValue("NOTIFICATION_TYPE", req.body.notificationType) req.app.locals.config.notificationType = req.body.notificationType
process.env.NOTIFICATION = req.body.notification
process.env.NOTIFICATION_TYPE = req.body.notificationType
} }
io.emit("siteState", { io.emit("siteState", {
notification: req.body.notification, notification: req.body.notification,
notificationType: req.body.notificationType, notificationType: req.body.notificationType,
latestVersion: require("../../frontend/package.json").version, latestVersion: require("../../frontend/package.json").version,
allowRegistrations: req.body.allowRegistrations allowRegistrations: req.body.allowRegistrations,
rules: req.body.rules
}) })
fs.writeFileSync(
"./config/config.json",
JSON.stringify(req.app.locals.config, null, 2)
)
res.sendStatus(204) res.sendStatus(204)
} catch (err) { } catch (err) {
return next(err) return next(err)

View file

@ -13,7 +13,7 @@ const {
router.all("*", auth, async (req, res, next) => { router.all("*", auth, async (req, res, next) => {
try { try {
if (!req.user.emailVerified && process.env.EMAIL_VERIFICATION === "true") { if (!req.user.emailVerified && req.app.locals.config.emailVerification) {
throw Errors.emailVerificationRequired throw Errors.emailVerificationRequired
} else { } else {
next() next()

View file

@ -155,7 +155,7 @@ async function createMessage(req, type, content, association, userId) {
router.all("*", auth, async (req, res, next) => { router.all("*", auth, async (req, res, next) => {
try { try {
if (!req.user.emailVerified && process.env.EMAIL_VERIFICATION === "true") { if (!req.user.emailVerified && req.app.locals.config.emailVerification) {
throw Errors.emailVerificationRequired throw Errors.emailVerificationRequired
} else { } else {
next() next()
@ -376,7 +376,7 @@ router.get("/mutual/:id/groups", auth, async (req, res, next) => {
router.get("/users", auth, async (req, res, next) => { router.get("/users", auth, async (req, res, next) => {
try { try {
if (process.env.PUBLIC_USERS === "true") { if (req.app.locals.config.publicUsers) {
const users = await User.findAll({ const users = await User.findAll({
attributes: [ attributes: [
"id", "id",

View file

@ -6,7 +6,7 @@ const router = express.Router()
router.all("*", auth, async (req, res, next) => { router.all("*", auth, async (req, res, next) => {
try { try {
if (!req.user.emailVerified && process.env.EMAIL_VERIFICATION === "true") { if (!req.user.emailVerified && req.app.locals.config.emailVerification) {
throw Errors.emailVerificationRequired throw Errors.emailVerificationRequired
} else { } else {
next() next()

View file

@ -72,7 +72,7 @@ const upload = multer({
router.post("/verify/resend", auth, mailLimiter, async (req, res, next) => { router.post("/verify/resend", auth, mailLimiter, async (req, res, next) => {
try { try {
if (process.env.EMAIL_VERIFICATION !== "true") { if (!req.app.locals.config.emailVerification) {
throw Errors.invalidParameter("Email verification is disabled") throw Errors.invalidParameter("Email verification is disabled")
} }
if (req.user.emailVerified) { if (req.user.emailVerified) {
@ -85,20 +85,20 @@ router.post("/verify/resend", auth, mailLimiter, async (req, res, next) => {
const mailGenerator = new Mailgen({ const mailGenerator = new Mailgen({
theme: "default", theme: "default",
product: { product: {
name: process.env.SITE_NAME, name: req.app.locals.config.siteName,
link: process.env.CORS_HOSTNAME link: req.app.locals.config.corsHostname
} }
}) })
const email = { const email = {
body: { body: {
name: req.user.username, name: req.user.username,
intro: `${process.env.SITE_NAME} Account Verification`, intro: `${req.app.locals.config.siteName} Account Verification`,
action: { action: {
instructions: `You are receiving this email because you registered on ${process.env.SITE_NAME}, please use the link below to verify your account.`, instructions: `You are receiving this email because you registered on ${req.app.locals.config.siteName}, please use the link below to verify your account.`,
button: { button: {
color: "#1A97FF", color: "#1A97FF",
text: "Account Verification", text: "Account Verification",
link: process.env.CORS_HOSTNAME + "/email/confirm/" + token link: req.app.locals.config.corsHostname + "/email/confirm/" + token
} }
}, },
outro: "If you did not register, please disregard this email." outro: "If you did not register, please disregard this email."
@ -108,18 +108,18 @@ router.post("/verify/resend", auth, mailLimiter, async (req, res, next) => {
const emailText = mailGenerator.generatePlaintext(email) const emailText = mailGenerator.generatePlaintext(email)
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: process.env.EMAIL_SMTP_HOST, host: req.app.locals.config.emailSMTPHost,
port: process.env.EMAIL_SMTP_PORT, port: req.app.locals.config.emailSMTPPort,
secure: JSON.parse(process.env.EMAIL_SMTP_SECURE.toLowerCase()), secure: req.app.locals.config.emailSMTPSecure,
auth: { auth: {
user: process.env.EMAIL_SMTP_USER, user: req.app.locals.config.emailSMTPUser,
pass: process.env.EMAIL_SMTP_PASSWORD pass: req.app.locals.config.emailSMTPPassword
} }
}) })
let info = await transporter.sendMail({ let info = await transporter.sendMail({
from: process.env.EMAIL_SMTP_FROM, from: req.app.locals.config.emailSMTPFrom,
to: req.user.email, to: req.user.email,
subject: "Email Verification - " + process.env.SITE_NAME, subject: "Email Verification - " + req.app.locals.config.siteName,
text: emailText, text: emailText,
html: emailBody html: emailBody
}) })
@ -135,7 +135,7 @@ router.post("/verify/resend", auth, mailLimiter, async (req, res, next) => {
router.post("/verify/confirm/:token", async (req, res, next) => { router.post("/verify/confirm/:token", async (req, res, next) => {
try { try {
if (process.env.EMAIL_VERIFICATION !== "true") { if (!req.app.locals.config.emailVerification) {
throw Errors.invalidParameter("Email verification is disabled") throw Errors.invalidParameter("Email verification is disabled")
} }
if (!req.params.token) { if (!req.params.token) {
@ -294,7 +294,7 @@ router.post("/register", limiter, async (req, res, next) => {
} }
} }
try { try {
if (process.env.ALLOW_REGISTRATIONS !== "true") { if (!req.app.locals.config.allowRegistrations) {
throw Errors.registrationsDisabled throw Errors.registrationsDisabled
} }
if (req.body.password.length < 8) { if (req.body.password.length < 8) {

View file

@ -9,6 +9,11 @@ const os = require("os")
const { execSync } = require("child_process") const { execSync } = require("child_process")
console.log("Troplo/Colubrina CLI") console.log("Troplo/Colubrina CLI")
if (fs.existsSync("../backend/config/config.json")) {
console.log(
"Want to modify either the Colubrina, or database config? Check out the config files in backend/config."
)
}
console.log("Colubrina version", require("../frontend/package.json").version) console.log("Colubrina version", require("../frontend/package.json").version)
async function checkForUpdates() { async function checkForUpdates() {
if (!process.argv.includes("--skip-update")) { if (!process.argv.includes("--skip-update")) {
@ -139,10 +144,10 @@ async function testDB() {
async function dbSetup() { async function dbSetup() {
await doSetupDB() await doSetupDB()
fs.writeFileSync( fs.writeFileSync(
path.join(__dirname, "../backend/config/config.json"), path.join(__dirname, "../backend/config/database.json"),
JSON.stringify(state.dbConfig) JSON.stringify(state.dbConfig, null, 2)
) )
console.log("config/config.json overwritten") console.log("config/database.json overwritten")
} }
async function runMigrations() { async function runMigrations() {
console.log("Running migrations") console.log("Running migrations")
@ -171,116 +176,61 @@ async function createUser() {
console.log("User created") console.log("User created")
} }
async function configureDotEnv() { async function configureDotEnv() {
function setEnvValue(key, value) { if (!fs.existsSync("../backend/config/config.json")) {
const ENV_VARS = fs.readFileSync("../backend/.env", "utf8").split(os.EOL) fs.writeFileSync("../backend/config/config.json", "{}")
// find the env we want based on the key
const target = ENV_VARS.indexOf(
ENV_VARS.find((line) => {
// (?<!#\s*) Negative lookbehind to avoid matching comments (lines that starts with #).
// There is a double slash in the RegExp constructor to escape it.
// (?==) Positive lookahead to check if there is an equal sign right after the key.
// This is to prevent matching keys prefixed with the key of the env var to update.
const keyValRegex = new RegExp(`(?<!#\\s*)${key}(?==)`)
return line.match(keyValRegex)
})
)
// if key-value pair exists in the .env file,
if (target !== -1) {
// replace the key/value with the new value
ENV_VARS.splice(target, 1, `${key}=${value}`)
} else {
// if it doesn't exist, add it instead
ENV_VARS.push(`${key}=${value}`)
} }
let config = require("../backend/config/config.json")
// write everything back to the file system config.hostname = await input.text("Public Domain", {
fs.writeFileSync("../backend/.env", ENV_VARS.join(os.EOL))
}
if (!fs.existsSync("../backend/.env")) {
fs.writeFileSync("../backend/.env", "")
}
setEnvValue(
"HOSTNAME",
await input.text("Public Domain", {
default: "localhost" default: "localhost"
}) })
) config.corsHostname = await input.text("CORS Hostname", {
setEnvValue( default: "http://localhost"
"CORS_HOSTNAME",
await input.text("Public Hostname", {
default: "http://localhost:8080"
}) })
) config.siteName = await input.text("Site Name", {
setEnvValue(
"SITE_NAME",
await input.text("Site Name", {
default: "Colubrina" default: "Colubrina"
}) })
) config.allowRegistrations = await input.text("Allow Registrations", {
setEnvValue( default: true
"ALLOW_REGISTRATIONS", })
await input.text("Permit Public Registrations", { config.publicUsers = await input.text("Show instance users publicly?", {
default: true
})
config.emailVerify = await input.text("Enforce email verification?", {
default: false default: false
}) })
) if (config.emailVerify) {
setEnvValue( config.emailSMTPHost = await input.text("SMTP Host", {
"PUBLIC_USERS", default: "mail.example.com"
await input.text("Show instance users publicly", {
default: false
}) })
) config.emailSMTPPort = await input.text("SMTP Port", {
const emailVerify = await input.text("Enforce email verification?", {
default: false
})
if (emailVerify) {
setEnvValue("EMAIL_VERIFICATION", true)
setEnvValue(
"EMAIL_SMTP_HOST",
await input.text("SMTP Host", {
default: "smtp.myhost.com"
})
)
setEnvValue(
"EMAIL_SMTP_PORT",
await input.text("SMTP Port", {
default: 587 default: 587
}) })
) config.emailSMTPUsername = await input.text("SMTP Username", {
setEnvValue( default: "colubrina@example.com"
"EMAIL_SMTP_USER",
await input.text("SMTP User", {
default: "colubrina@myhost.com"
}) })
) config.emailSMTPPassword = await input.text("SMTP Password", {})
setEnvValue( config.emailSMTPFrom = await input.text("SMTP From Address", {
"EMAIL_SMTP_FROM", default: "colubrina@example.com"
await input.text("SMTP From Address", {
default: "colubrina@myhost.com"
}) })
) config.emailSMTPSecure = await input.text("SMTP Secure", {
setEnvValue("EMAIL_SMTP_PASSWORD", await input.text("SMTP Password", {})) default: true
setEnvValue(
"EMAIL_SMTP_SECURE",
await input.text("SMTP Secure", {
default: false
}) })
)
} else { } else {
setEnvValue("EMAIL_VERIFICATION", false) config.emailSMTPHost = "smtp.myhost.com"
setEnvValue("EMAIL_SMTP_HOST", "smtp.myhost.com") config.emailSMTPPort = 587
setEnvValue("EMAIL_SMTP_PORT", "587") config.emailSMTPUsername = "colubrina@example.com"
setEnvValue("EMAIL_SMTP_USER", "colubrina@myhost.com") config.emailSMTPFrom = "colubrina@example.com"
setEnvValue("EMAIL_SMTP_FROM", "colubrina@myhost.com") config.emailSMTPPassword = ""
setEnvValue("EMAIL_SMTP_PASSWORD", "myPassword") config.emailSMTPSecure = true
setEnvValue("EMAIL_SMTP_SECURE", false)
} }
setEnvValue("PUBLIC_USERS") config.notification = ""
setEnvValue("NOTIFICATION", "") config.notificationType = "info"
setEnvValue("NOTIFICATION_TYPE", "info") config.release = "stable"
setEnvValue("RELEASE", "stable") config.rules = "Write your instance rules here."
fs.writeFileSync(
"../backend/config/config.json",
JSON.stringify(config, null, 2)
)
} }
async function init() { async function init() {
while (true) { while (true) {
@ -288,7 +238,7 @@ async function init() {
"First-time setup", "First-time setup",
"Create user", "Create user",
"Run migrations", "Run migrations",
"Update/create config file", "Update/create database config file",
"Check for updates", "Check for updates",
"Build frontend for production", "Build frontend for production",
"Exit" "Exit"
@ -303,10 +253,15 @@ async function init() {
execSync("cd ../frontend && yarn install --frozen-lockfile", () => { execSync("cd ../frontend && yarn install --frozen-lockfile", () => {
console.log("yarn install complete (frontend)") console.log("yarn install complete (frontend)")
}) })
if (fs.existsSync(path.join(__dirname, "../backend/.env"))) { if (
const option = await input.confirm(".env already exists, overwrite?", { fs.existsSync(path.join(__dirname, "../backend/config/config.json"))
) {
const option = await input.confirm(
"Colubrina config already exists, overwrite?",
{
default: false default: false
}) }
)
if (option) { if (option) {
await configureDotEnv() await configureDotEnv()
} }
@ -314,10 +269,10 @@ async function init() {
await configureDotEnv() await configureDotEnv()
} }
if ( if (
fs.existsSync(path.join(__dirname, "../backend/config/config.json")) fs.existsSync(path.join(__dirname, "../backend/config/database.json"))
) { ) {
const option = await input.select( const option = await input.select(
`config/config.json already exists. Do you want to overwrite it?`, `config/database.json already exists. Do you want to overwrite it?`,
["Yes", "No"] ["Yes", "No"]
) )
if (option === "Yes") { if (option === "Yes") {
@ -368,9 +323,9 @@ async function init() {
console.log( console.log(
"The Colubrina frontend can be built with `yarn build` in the root project directory, and is recommended to be served via NGINX, with a proxy_pass to the backend on /api and /socket.io." "The Colubrina frontend can be built with `yarn build` in the root project directory, and is recommended to be served via NGINX, with a proxy_pass to the backend on /api and /socket.io."
) )
} else if (option === "Update/create config file") { } else if (option === "Update/create database config file") {
await dbSetup() await dbSetup()
console.log("config/config.json overwritten or created") console.log("config/database.json overwritten or created")
} else if (option === "Create user") { } else if (option === "Create user") {
await createUser() await createUser()
} else if (option === "Run migrations") { } else if (option === "Run migrations") {

View file

@ -1,6 +1,6 @@
{ {
"name": "colubrina", "name": "colubrina",
"version": "1.0.18", "version": "1.0.19",
"private": true, "private": true,
"author": "Troplo <troplo@troplo.com>", "author": "Troplo <troplo@troplo.com>",
"scripts": { "scripts": {

View file

@ -624,6 +624,9 @@ export default {
this.$store.state.site.latestVersion = state.latestVersion this.$store.state.site.latestVersion = state.latestVersion
this.$store.state.site.notification = state.notification this.$store.state.site.notification = state.notification
this.$store.state.site.notificationType = state.notificationType this.$store.state.site.notificationType = state.notificationType
this.$store.state.site.allowRegistrations = state.allowRegistrations
this.$store.state.site.rules = state.rules
this.$store.state.site.emailVerification = state.emailVerification
}) })
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
this.$store.dispatch("updateQuickSwitch") this.$store.dispatch("updateQuickSwitch")

View file

@ -32,6 +32,14 @@
label="Allow registrations" label="Allow registrations"
v-model="allowRegistrations" v-model="allowRegistrations"
></v-switch> ></v-switch>
<v-textarea ref="rules" label="Instance Rules" v-model="rules" class="mx-3">
</v-textarea>
<v-card-title>
<v-icon class="mr-1">mdi-language-markdown</v-icon>Rules Preview:
</v-card-title>
<v-card-text>
<span v-markdown class="mx-3" :key="rules">{{ rules }}</span>
</v-card-text>
<v-btn text class="mx-3 mb-3" color="primary" @click="updateState" <v-btn text class="mx-3 mb-3" color="primary" @click="updateState"
>Save</v-btn >Save</v-btn
> >
@ -47,6 +55,7 @@ export default {
notification: "", notification: "",
notificationType: "info", notificationType: "info",
allowRegistrations: true, allowRegistrations: true,
rules: "",
notificationTypes: [ notificationTypes: [
{ text: "Info", value: "info" }, { text: "Info", value: "info" },
{ text: "Success", value: "success" }, { text: "Success", value: "success" },
@ -73,7 +82,8 @@ export default {
notification: this.notification, notification: this.notification,
notificationType: this.notificationType, notificationType: this.notificationType,
broadcastType: this.broadcastType, broadcastType: this.broadcastType,
allowRegistrations: this.allowRegistrations allowRegistrations: this.allowRegistrations,
rules: this.rules
}) })
.then(() => { .then(() => {
this.$toast.success("State updated") this.$toast.success("State updated")
@ -87,6 +97,7 @@ export default {
this.notification = this.$store.state.site.notification this.notification = this.$store.state.site.notification
this.notificationType = this.$store.state.site.notificationType this.notificationType = this.$store.state.site.notificationType
this.allowRegistrations = this.$store.state.site.allowRegistrations this.allowRegistrations = this.$store.state.site.allowRegistrations
this.rules = this.$store.state.site.rules
} }
} }
</script> </script>

View file

@ -1,8 +1,38 @@
<template> <template>
<div id="admin-users"> <div id="admin-users">
<v-dialog width="500" v-model="create.dialog">
<v-card color="card">
<v-toolbar color="toolbar">
<v-toolbar-title>Create User</v-toolbar-title>
</v-toolbar>
<v-container>
<v-form @submit.prevent="createUser">
<v-text-field
label="Username"
v-model="create.username"
></v-text-field>
<v-text-field label="Email" v-model="create.email"></v-text-field>
<v-text-field
label="Password"
type="password"
v-model="create.password"
></v-text-field>
<v-switch
inset
label="Email Verified?"
v-model="create.emailVerified"
></v-switch>
<v-btn text type="submit" color="primary">Create</v-btn>
</v-form>
</v-container>
</v-card>
</v-dialog>
<v-toolbar color="toolbar"> <v-toolbar color="toolbar">
<v-toolbar-title>Users ({{ users.count }})</v-toolbar-title> <v-toolbar-title>Users ({{ users.count }})</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn @click="create.dialog = true" icon>
<v-icon>mdi-plus</v-icon>
</v-btn>
<v-btn @click="getUsers" icon> <v-btn @click="getUsers" icon>
<v-icon>mdi-refresh</v-icon> <v-icon>mdi-refresh</v-icon>
</v-btn> </v-btn>
@ -19,6 +49,21 @@
<template v-slot:item.index="{ index }"> <template v-slot:item.index="{ index }">
{{ index }} {{ index }}
</template> </template>
<template v-slot:item.actions="{ item }">
<v-tooltip top>
<template v-slot:activator="{ on }">
<v-btn
v-on="on"
icon
@click="banUser(item)"
:disabled="item.id === $store.state.user.id || item.admin"
>
<v-icon>mdi-gavel</v-icon>
</v-btn>
</template>
<span>Ban</span>
</v-tooltip>
</template>
</v-data-table> </v-data-table>
</div> </div>
</template> </template>
@ -31,6 +76,13 @@ export default {
data() { data() {
return { return {
users: [], users: [],
create: {
dialog: false,
username: "",
email: "",
password: "",
emailVerified: false
},
headers: [ headers: [
{ {
text: "Index", text: "Index",
@ -68,11 +120,59 @@ export default {
{ {
text: "Last Seen At", text: "Last Seen At",
value: "lastSeenAt" value: "lastSeenAt"
},
{
text: "Admin",
value: "admin"
},
{
text: "Banned",
value: "banned"
},
{
text: "Email Verified",
value: "emailVerified"
},
{
text: "Actions",
value: "actions",
sortable: false
} }
] ]
} }
}, },
methods: { methods: {
createUser() {
this.axios
.post(`/api/v1/admin/user`, this.create)
.then(() => {
this.getUsers()
this.$toast.success("Action performed successfully.")
this.create = {
dialog: false,
username: "",
email: "",
password: "",
emailVerified: false
}
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
},
banUser(item) {
this.axios
.put(`/api/v1/admin/user/${item.id}`, {
banned: !item.banned
})
.then(() => {
this.getUsers()
this.$toast.success("Action performed successfully.")
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
},
getUsers() { getUsers() {
this.axios this.axios
.get("/api/v1/admin/users") .get("/api/v1/admin/users")

View file

@ -30,8 +30,7 @@
<v-form ref="form" class="pa-4 pt-6"> <v-form ref="form" class="pa-4 pt-6">
<p class="text-center text-h4"> <p class="text-center text-h4">
Login to Login to
<span class="troplo-title">{{ $store.state.site.name }}</span <span class="troplo-title">{{ $store.state.site.name }}</span>
><small style="font-size: 15px">beta</small>
</p> </p>
<v-text-field <v-text-field
@keyup.enter="doLogin()" @keyup.enter="doLogin()"

View file

@ -1,5 +1,19 @@
<template> <template>
<div id="login" v-if="!$store.state.user?.bcUser?.id"> <div id="login" v-if="!$store.state.user?.id">
<v-dialog v-model="rulesDialog" max-width="700px">
<v-card color="card">
<v-toolbar color="toolbar">
<v-toolbar-title>
{{ $store.state.site.name }} Rules
</v-toolbar-title>
</v-toolbar>
<v-card-text class="mt-3" style="color: unset">
<span v-markdown :key="$store.state.site.rules">{{
$store.state.site.rules
}}</span>
</v-card-text>
</v-card>
</v-dialog>
<div :class="{ outer: !$vuetify.breakpoint.mobile }"> <div :class="{ outer: !$vuetify.breakpoint.mobile }">
<div :class="{ middle: !$vuetify.breakpoint.mobile }"> <div :class="{ middle: !$vuetify.breakpoint.mobile }">
<div :class="{ innerLogin: !$vuetify.breakpoint.mobile }"> <div :class="{ innerLogin: !$vuetify.breakpoint.mobile }">
@ -8,8 +22,7 @@
<v-form ref="form" class="pa-4 pt-6"> <v-form ref="form" class="pa-4 pt-6">
<p class="text-center text-h4"> <p class="text-center text-h4">
Register to Register to
<span class="troplo-title">{{ $store.state.site.name }}</span <span class="troplo-title">{{ $store.state.site.name }}</span>
><small style="font-size: 15px">beta</small>
</p> </p>
<v-text-field <v-text-field
@keyup.enter="doRegister()" @keyup.enter="doRegister()"
@ -52,6 +65,38 @@
>This instance has email verification enforced, ensure your >This instance has email verification enforced, ensure your
email is correct.</small email is correct.</small
> >
<v-row align="center">
<v-tooltip top v-if="!rulesOpenedOnce">
<template v-slot:activator="{ on }">
<div v-on="on">
<v-switch
class="ml-4 mt-5"
inset
v-model="rules"
:disabled="!rulesOpenedOnce"
></v-switch>
</div>
</template>
<span>You need to view the rules first.</span>
</v-tooltip>
<v-switch
class="ml-4 mt-5"
inset
v-model="rules"
v-else
:disabled="!rulesOpenedOnce"
></v-switch>
I have read and agree to the&nbsp;
<a
@click="
rulesDialog = true
rulesOpenedOnce = true
"
target="_blank"
>
instance rules </a
>.
</v-row>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
@ -96,6 +141,9 @@ export default {
totp: "", totp: "",
totpDialog: false, totpDialog: false,
loading: false, loading: false,
rules: false,
rulesDialog: false,
rulesOpenedOnce: false,
instance: instance:
localStorage.getItem("instance") || "https://colubrina.troplo.com", localStorage.getItem("instance") || "https://colubrina.troplo.com",
instanceString: "" instanceString: ""
@ -123,6 +171,12 @@ export default {
return window.innerHeight return window.innerHeight
}, },
doRegister() { doRegister() {
if (!this.rules) {
this.$toast.error(
"You need to accept the rules before you can register."
)
return
}
this.loading = true this.loading = true
this.axios this.axios
.post("/api/v1/user/register", { .post("/api/v1/user/register", {

View file

@ -94,9 +94,37 @@ module.exports = {
builderOptions: { builderOptions: {
appId: "com.troplo.colubrina", appId: "com.troplo.colubrina",
win: { win: {
target: [
{
target: "nsis",
arch: ["x64", "ia32", "armv7l", "arm64"]
}
],
publish: ["github"] publish: ["github"]
}, },
linux: { linux: {
target: [
{
target: "AppImage",
arch: ["x64", "ia32", "armv7l", "arm64"]
},
{
target: "deb",
arch: ["x64", "ia32", "armv7l", "arm64"]
},
{
target: "snap",
arch: ["x64", "ia32", "armv7l", "arm64"]
},
{
target: "pacman",
arch: ["x64", "ia32", "armv7l", "arm64"]
},
{
target: "tar.gz",
arch: ["x64", "ia32", "armv7l", "arm64"]
}
],
publish: ["github"], publish: ["github"],
category: "Network", category: "Network",
synopsis: "Instant Messaging", synopsis: "Instant Messaging",