This commit is contained in:
Troplo 2022-08-03 19:11:19 +10:00
parent 344fce43ab
commit e2cb2adff6
22 changed files with 436 additions and 532 deletions

View file

@ -4,8 +4,6 @@ let express = require("express")
let app = express() let app = express()
let bodyParser = require("body-parser") let bodyParser = require("body-parser")
let os = require("os") let os = require("os")
const cookieParser = require("cookie-parser")
app.use(cookieParser())
app.set("trust proxy", true) app.set("trust proxy", true)
const socket = require("./lib/socket") const socket = require("./lib/socket")
const server = require("http").createServer(app) const server = require("http").createServer(app)
@ -45,7 +43,6 @@ app.all("/api/*", (req, res) => {
console.log(os.hostname()) console.log(os.hostname())
app.use(require("./lib/errorHandler")) app.use(require("./lib/errorHandler"))
server.listen(23998, () => { server.listen(23998, () => {
console.log("Initialized") console.log("Initialized")
console.log("Listening on port 0.0.0.0:" + 23998) console.log("Listening on port 0.0.0.0:" + 23998)

View file

@ -1,10 +1,18 @@
const { User, Theme, Session } = require("../models") const { User, Theme, Session } = require("../models")
const Errors = require("./errors") const Errors = require("./errors")
const { Op } = require("sequelize")
module.exports = async function (req, res, next) { module.exports = async function (req, res, next) {
try { try {
if (req.header("Authorization") && req.header("Authorization") !== "null") { if (req.header("Authorization") && req.header("Authorization") !== "null") {
const token = req.header("Authorization") const token = req.header("Authorization")
const session = await Session.findOne({ where: { session: token } }) const session = await Session.findOne({
where: {
session: token,
expiredAt: {
[Op.gt]: new Date()
}
}
})
if (session) { if (session) {
const user = await User.findOne({ const user = await User.findOne({
where: { id: session.userId }, where: { id: session.userId },

View file

@ -1,51 +1,35 @@
const { User, Theme, Session } = require("../models") const { User, Theme, Session } = require("../models")
const { Op } = require("sequelize")
const parseCookie = (str) =>
str
.split(";")
.map((v) => v.split("="))
.reduce((acc, v) => {
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim())
return acc
}, {})
module.exports = async function (socket, next) { module.exports = async function (socket, next) {
try { try {
const cookies = parseCookie(socket.handshake.headers.cookie) const token = socket.handshake.auth.token
if (cookies["session"]) { const session = await Session.findOne({
const session = await Session.findOne({ where: {
where: { session: cookies["session"] } session: token,
}) expiredAt: {
if (session) { [Op.gt]: new Date()
const user = await User.findOne({ }
where: { id: session.userId }, }
attributes: { })
exclude: ["totp", "password", "emailToken"] if (session) {
}, const user = await User.findOne({
include: [ where: { id: session.userId },
{ attributes: {
model: Theme, exclude: ["totp", "compassSession", "password"]
as: "themeObject" },
} include: [
], {
raw: true model: Theme,
}) as: "themeObject"
if (user) {
if (user.banned) {
socket.user = {
id: null,
username: "Not Authenticated"
}
next()
} else {
socket.user = user
next()
} }
} ]
} else { })
socket.user = { if (user) {
id: null, await user.update({
username: "Not Authenticated" lastSeenAt: new Date().toISOString()
} })
socket.user = user
next() next()
} }
} else { } else {

View file

@ -45,7 +45,8 @@ let Errors = {
401 401
], ],
mailFail: ["There was an error sending the verification email.", 400], mailFail: ["There was an error sending the verification email.", 400],
invalidToken: ["The token you provided is invalid.", 400] invalidToken: ["The token you provided is invalid.", 400],
cannotFriendYourself: ["You cannot friend yourself.", 400]
} }
function processErrors(errorName) { function processErrors(errorName) {

View file

@ -122,126 +122,10 @@ module.exports = {
socket.emit("unauthorized", { socket.emit("unauthorized", {
message: "Please reauth." message: "Please reauth."
}) })
socket.on("token", async (token) => { socket.on("token", async () => {
const session = await Session.findOne({ where: { session: token } }) socket.emit("unsupported", {
if (session) { message: "This authentication method is unsupported."
const user = await User.findOne({ })
where: { id: session.userId },
attributes: {
exclude: ["totp", "password", "emailToken"]
},
include: [
{
model: Theme,
as: "themeObject"
}
]
})
if (user) {
socket.user = user
socket.join(user.id)
socket.emit("authorized")
socket.join(user.id)
socket.emit("siteState", {
release: process.env.RELEASE,
notification: process.env.NOTIFICATION,
notificationType: process.env.NOTIFICATION_TYPE,
latestVersion: require("../../frontend/package.json").version
})
const friends = await Friend.findAll({
where: {
userId: user.id,
status: "accepted"
}
})
await user.update({
status:
user.storedStatus === "invisible"
? "offline"
: user.storedStatus
})
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status:
user.storedStatus === "invisible"
? "offline"
: user.storedStatus
})
})
socket.on("ping", () => {
socket.emit("pong")
})
socket.on("bcBots/deleteMessage", (e) => {
if (socket.user.bot) {
socket.to(e.userId).emit("deleteMessage", e)
} else {
socket.emit("bcBots/deleteMessage", {
error: "You cannot perform this action."
})
}
})
socket.on("idle", async () => {
const user = await User.findOne({
where: {
id: socket.user.id
}
})
if (user.storedStatus === "online") {
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status: "away"
})
})
io.to(user.id).emit("userStatus", {
userId: user.id,
status: "away"
})
await user.update({
status: "away"
})
}
})
socket.on("online", async () => {
const user = await User.findOne({
where: {
id: socket.user.id
}
})
if (user.storedStatus === "online") {
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status: "online"
})
})
io.to(user.id).emit("userStatus", {
userId: user.id,
status: "online"
})
await user.update({
status: "online"
})
}
})
socket.on("disconnect", async function () {
const clients =
io.sockets.adapter.rooms.get(user.id) || new Set()
if (!clients.size || clients.size === 0) {
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status: "offline"
})
})
await user.update({
status: "offline"
})
}
})
}
}
}) })
console.log("Unauthenticated user") console.log("Unauthenticated user")
socket.on("reAuth", async () => { socket.on("reAuth", async () => {

View file

@ -50,6 +50,9 @@ router.post("/", auth, async (req, res, next) => {
} }
}) })
if (user) { if (user) {
if (user.id === req.user.id) {
throw Errors.cannotFriendYourself
}
const friend = await Friend.findOne({ const friend = await Friend.findOne({
where: { where: {
userId: req.user.id, userId: req.user.id,

View file

@ -133,27 +133,29 @@ router.post("/verify/resend", auth, mailLimiter, async (req, res, next) => {
} }
}) })
router.post("/verify/confirm/:token", auth, async (req, res, next) => { router.post("/verify/confirm/:token", async (req, res, next) => {
try { try {
const user = await User.findOne({
where: {
id: req.user.id
}
})
if (process.env.EMAIL_VERIFICATION !== "true") { if (process.env.EMAIL_VERIFICATION !== "true") {
throw Errors.invalidParameter("Email verification is disabled") throw Errors.invalidParameter("Email verification is disabled")
} }
if (!req.params.token) { if (!req.params.token) {
throw Errors.invalidToken throw Errors.invalidToken
} }
if (req.params.token !== user.emailToken) { const user = await User.findOne({
where: {
emailToken: req.params.token,
emailVerified: false
}
})
if (user) {
await user.update({
emailVerified: true,
emailToken: null
})
res.json({ success: true })
} else {
throw Errors.invalidToken throw Errors.invalidToken
} }
await user.update({
emailVerified: true,
emailToken: null
})
res.json({ success: true })
} catch (e) { } catch (e) {
next(e) next(e)
} }
@ -198,12 +200,6 @@ router.post("/login", async (req, res, next) => {
osVersion: ua.os.version osVersion: ua.os.version
} }
}) })
res.cookie("session", session.session, {
maxAge: 1000 * 60 * 60 * 24 * 365,
httpOnly: true,
secure: false,
sameSite: "strict"
})
res.json({ res.json({
session: session.session, session: session.session,
success: true, success: true,
@ -287,12 +283,6 @@ router.post("/register", limiter, async (req, res, next) => {
osVersion: ua.os.version osVersion: ua.os.version
} }
}) })
res.cookie("session", session.session, {
maxAge: 1000 * 60 * 60 * 24 * 365,
httpOnly: true,
secure: false,
sameSite: "strict"
})
res.json({ res.json({
session: session.session, session: session.session,
success: true, success: true,
@ -371,15 +361,6 @@ router.delete("/sessions/:id", auth, async (req, res, next) => {
} }
}) })
router.post("/logout", (req, res, next) => {
try {
res.clearCookie("session")
res.sendStatus(204)
} catch (e) {
next(e)
}
})
router.post( router.post(
"/settings/avatar", "/settings/avatar",
auth, auth,

View file

@ -1,8 +1,26 @@
{ {
"name": "colubrina", "name": "colubrina",
"version": "1.0.12", "version": "1.0.13",
"private": true, "private": true,
"author": "Troplo <troplo@troplo.com>", "author": "Troplo <troplo@troplo.com>",
"build": {
"appId": "com.troplo.colubrina",
"win": {
"publish": [
"github"
]
},
"linux": {
"publish": [
"github"
]
},
"mac": {
"publish": [
"github"
]
}
},
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
@ -12,9 +30,11 @@
"postinstall": "patch-package && electron-builder install-app-deps", "postinstall": "patch-package && electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps" "postuninstall": "electron-builder install-app-deps"
}, },
"main": "background.js",
"repository": "https://github.com/Troplo/Colubrina", "repository": "https://github.com/Troplo/Colubrina",
"dependencies": { "dependencies": {
"@babel/preset-env": "^7.17.10", "@babel/preset-env": "^7.17.10",
"@mdi/font": "6.5.95",
"@mdi/js": "^6.6.95", "@mdi/js": "^6.6.95",
"axios": "^0.26.1", "axios": "^0.26.1",
"brace": "^0.11.1", "brace": "^0.11.1",
@ -23,8 +43,6 @@
"dayjs": "^1.11.0", "dayjs": "^1.11.0",
"ejs": "^3.1.7", "ejs": "^3.1.7",
"glob-parent": "^5.1.2", "glob-parent": "^5.1.2",
"graphql": "^16.3.0",
"graphql-tag": "^2.12.6",
"highlight.js": "^11.6.0", "highlight.js": "^11.6.0",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-emoji": "git+https://github.com/Troplo/markdown-it-unicode-emoji", "markdown-it-emoji": "git+https://github.com/Troplo/markdown-it-unicode-emoji",
@ -39,16 +57,13 @@
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
"twemoji": "^14.0.2", "twemoji": "^14.0.2",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-apollo": "^3.1.0",
"vue-axios": "^3.4.1", "vue-axios": "^3.4.1",
"vue-chartjs": "^4.0.1", "vue-chartjs": "^4.0.1",
"vue-final-modal": "^2.4.1", "vue-final-modal": "^2.4.1",
"vue-matomo": "^4.1.0",
"vue-native-notification": "^1.1.1", "vue-native-notification": "^1.1.1",
"vue-router": "^3.2.0", "vue-router": "^3.2.0",
"vue-sanitize": "^0.2.1", "vue-sanitize": "^0.2.1",
"vue-shortkey": "^3.1.7", "vue-shortkey": "^3.1.7",
"vue-swatches": "^2.1.1",
"vue-toastification": "^1.7.14", "vue-toastification": "^1.7.14",
"vue2-ace-editor": "^0.0.15", "vue2-ace-editor": "^0.0.15",
"vuetify": "^2.6.4", "vuetify": "^2.6.4",
@ -56,7 +71,6 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.16.7", "@babel/plugin-proposal-optional-chaining": "^7.16.7",
"@mdi/font": "6.5.95",
"@vue/cli-plugin-babel": "~4.5.15", "@vue/cli-plugin-babel": "~4.5.15",
"@vue/cli-plugin-eslint": "~4.5.15", "@vue/cli-plugin-eslint": "~4.5.15",
"@vue/cli-plugin-pwa": "~4.5.15", "@vue/cli-plugin-pwa": "~4.5.15",

View file

@ -437,12 +437,15 @@
is limited. is limited.
</v-alert> </v-alert>
</v-container> </v-container>
<router-view <div @touchstart="touchStart" @touchend="touchEnd">
:style=" <router-view
'background-color: ' + id="main"
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].bg :style="
" 'background-color: ' +
/> $vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].bg
"
/>
</div>
</v-main> </v-main>
</v-app> </v-app>
</template> </template>
@ -451,7 +454,7 @@
import AjaxErrorHandler from "@/lib/errorHandler" import AjaxErrorHandler from "@/lib/errorHandler"
import { VueFinalModal } from "vue-final-modal" import { VueFinalModal } from "vue-final-modal"
import Header from "@/components/Header" import Header from "@/components/Header"
import Vue from "vue"
export default { export default {
name: "App", name: "App",
components: { components: {
@ -479,7 +482,9 @@ export default {
results: [], results: [],
searchInput: null, searchInput: null,
themes: [], themes: [],
cssTips: true cssTips: true,
touchStartX: null,
touchEndX: null
}), }),
computed: { computed: {
creatorJSON: { creatorJSON: {
@ -702,6 +707,19 @@ export default {
}) })
}) })
}, },
touchEnd(event) {
this.touchEndX = event.changedTouches[0].screenX
if (this.touchEndX > this.touchStartX) {
if (this.touchEndX - this.touchStartX > 100) {
this.touchStartX = null
this.touchEndX = null
this.$store.state.drawer = true
}
}
},
touchStart(event) {
this.touchStartX = event.changedTouches[0].screenX
},
setTheme(theme) { setTheme(theme) {
const name = theme.id const name = theme.id
const dark = theme.dark const dark = theme.dark
@ -728,23 +746,8 @@ export default {
}) })
} }
}, },
mounted() { async mounted() {
Vue.axios.defaults.headers.common["X-Colubrina"] = true await this.$store.dispatch("doInit")
Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("session")
console.log(localStorage.getItem("instance"))
if (process.env.IS_ELECTRON) {
this.axios.defaults.baseURL = localStorage.getItem("instance")
this.$store.state.baseURL = localStorage.getItem("instance")
}
if (localStorage.getItem("customHeaders")) {
console.log(JSON.parse(localStorage.getItem("customHeaders")))
for (let header in JSON.parse(localStorage.getItem("customHeaders"))) {
Vue.axios.defaults.headers[header] = JSON.parse(
localStorage.getItem("customHeaders")
)[header]
}
}
if (this.$vuetify.breakpoint.mobile) { if (this.$vuetify.breakpoint.mobile) {
this.$store.state.drawer = false this.$store.state.drawer = false
} }
@ -772,7 +775,8 @@ export default {
}) })
this.$socket.connect() this.$socket.connect()
this.$socket.on("unauthorized", () => { this.$socket.on("unauthorized", () => {
this.$socket.emit("token", localStorage.getItem("session")) console.log("Reauth requested")
this.$socket.emit("token", localStorage.getItem("token"))
}) })
document.title = this.$route.name document.title = this.$route.name
? this.$route.name + " - " + this.$store.state.site.name ? this.$route.name + " - " + this.$store.state.site.name
@ -780,13 +784,10 @@ export default {
this.$store.commit("setLoading", true) this.$store.commit("setLoading", true)
this.$vuetify.theme.dark = this.$store.state.user?.theme === "dark" || true this.$vuetify.theme.dark = this.$store.state.user?.theme === "dark" || true
this.$store.dispatch("getState") this.$store.dispatch("getState")
this.getThemes()
this.communicationsIdleCheck()
this.$store this.$store
.dispatch("getUserInfo") .dispatch("getUserInfo")
.then(() => { .then(() => {
console.log(window.location.pathname) console.log(window.location.pathname)
// check if its /email/confirm/<token>
if ( if (
!window.location.pathname.includes("/email/confirm/") && !window.location.pathname.includes("/email/confirm/") &&
!window.location.pathname.includes("/email/verify") && !window.location.pathname.includes("/email/verify") &&
@ -797,10 +798,10 @@ export default {
} }
}) })
.catch(() => { .catch(() => {
if (!["/login", "/register"].includes(this.$route.path)) { this.$router.push("/login")
this.$router.push("/login")
}
}) })
this.getThemes()
this.communicationsIdleCheck()
this.registerSocket() this.registerSocket()
}, },
watch: { watch: {

View file

@ -1,5 +1,5 @@
.offset-message { .offset-message {
padding-left: 47px; padding-left: 53px;
} }
.message-action-card { .message-action-card {
position: absolute; position: absolute;

View file

@ -1,6 +1,6 @@
"use strict" "use strict"
import { app, protocol, BrowserWindow } from "electron" import { app, protocol, BrowserWindow, screen } from "electron"
import { createProtocol } from "vue-cli-plugin-electron-builder/lib" import { createProtocol } from "vue-cli-plugin-electron-builder/lib"
import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer" import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"
const isDevelopment = process.env.NODE_ENV !== "production" const isDevelopment = process.env.NODE_ENV !== "production"
@ -12,15 +12,25 @@ protocol.registerSchemesAsPrivileged([
async function createWindow() { async function createWindow() {
// Create the browser window. // Create the browser window.
const { bounds } = screen.getDisplayNearestPoint(
screen.getCursorScreenPoint()
)
const height = Math.floor(bounds.height * (2 / 3))
const width = Math.floor(bounds.width * (2 / 3))
const y = Math.floor(bounds.y + (bounds.height - height) / 2)
const x = Math.floor(bounds.x + (bounds.width - width) / 2)
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 1400, width,
height: 1000, height,
x,
y,
webPreferences: { webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone // Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION, contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
preload: "window-preload.js", preload: "window-preload.js",
// required for multi-instance support
webSecurity: false webSecurity: false
} }
}) })

View file

@ -21,8 +21,8 @@
<v-textarea <v-textarea
:style=" :style="
!$vuetify.breakpoint.mobile !$vuetify.breakpoint.mobile
? 'margin-bottom: 3px' ? 'margin-bottom: 5px'
: 'margin-bottom: -17px' : 'margin-bottom: 0px'
" "
autofocus autofocus
label="Type a message" label="Type a message"

View file

@ -307,7 +307,7 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-app-bar app color="bg"> <v-app-bar app color="dark" elevation="5" style="z-index: 15">
<v-app-bar-nav-icon <v-app-bar-nav-icon
@click.stop="$store.state.drawer = !$store.state.drawer" @click.stop="$store.state.drawer = !$store.state.drawer"
v-if="$vuetify.breakpoint.mobile || !$store.state.drawer" v-if="$vuetify.breakpoint.mobile || !$store.state.drawer"
@ -381,10 +381,11 @@
</template> </template>
</v-app-bar> </v-app-bar>
<v-navigation-drawer <v-navigation-drawer
color="bg" color="dark"
floating
app app
style="max-height: 100%" floating
style="max-height: 100%; z-index: 15"
class="elevation-5"
v-model="$store.state.drawer" v-model="$store.state.drawer"
:width="$vuetify.breakpoint.mobile ? 270 : 320" :width="$vuetify.breakpoint.mobile ? 270 : 320"
> >
@ -414,7 +415,7 @@
</template> </template>
<template v-else> <template v-else>
<v-btn <v-btn
color="toolbar" color="sheet"
to="/communications/friends" to="/communications/friends"
block block
class="mb-3 rounded-xl" class="mb-3 rounded-xl"
@ -429,12 +430,12 @@
solo solo
label="Search..." label="Search..."
append-icon="mdi-magnify" append-icon="mdi-magnify"
background-color="toolbar" background-color="sheet"
style="margin-bottom: -18px" style="margin-bottom: -18px"
elevation="2" elevation="2"
v-model="search" v-model="search"
></v-text-field> ></v-text-field>
<v-toolbar color="toolbar" class="rounded-xl mb-3" elevation="2"> <v-toolbar color="sheet" class="rounded-xl mb-3" elevation="2">
<v-toolbar-title class="subtitle-1"> <v-toolbar-title class="subtitle-1">
CHATS ({{ chats.length }}) CHATS ({{ chats.length }})
</v-toolbar-title> </v-toolbar-title>
@ -522,6 +523,7 @@
</v-container> </v-container>
<template v-slot:append> <template v-slot:append>
<v-card tile color="bg" elevation="0"> <v-card tile color="bg" elevation="0">
<v-divider></v-divider>
<v-overlay :value="!$store.state.wsConnected" absolute> <v-overlay :value="!$store.state.wsConnected" absolute>
<v-progress-circular indeterminate size="48"></v-progress-circular> <v-progress-circular indeterminate size="48"></v-progress-circular>
</v-overlay> </v-overlay>
@ -985,7 +987,7 @@ export default {
}, },
mounted() { mounted() {
Vue.axios.defaults.headers.common["Authorization"] = Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("session") localStorage.getItem("token")
this.searchUsers() this.searchUsers()
this.searchUsersForGroupAdmin() this.searchUsersForGroupAdmin()
this.$store.dispatch("getChats") this.$store.dispatch("getChats")

View file

@ -37,12 +37,15 @@
</v-toolbar> </v-toolbar>
<v-list-item <v-list-item
:key="message.keyId" :key="message.keyId"
:class="{ 'message-hover': hover }" :class="{
'message-hover': hover,
'pa-0': $vuetify.breakpoint.mobile
}"
:id="'message-' + index" :id="'message-' + index"
@contextmenu="show($event, 'message', message)" @contextmenu="show($event, 'message', message)"
:style="lastMessage ? 'margin-bottom: -5px; margin-top: -5px;' : ''" :style="lastMessage ? 'margin-bottom: -5px; margin-top: -5px;' : ''"
> >
<v-avatar size="40" class="mr-2" v-if="!lastMessage"> <v-avatar size="45" class="mr-2" v-if="!lastMessage">
<v-img <v-img
:src=" :src="
$store.state.baseURL + '/usercontent/' + message.user.avatar $store.state.baseURL + '/usercontent/' + message.user.avatar
@ -124,7 +127,7 @@
no-gutters no-gutters
> >
<v-card <v-card
elevaion="0" elevation="0"
:color=" :color="
embed.type === 'embed-v1' ? embed.backgroundColor : 'bg' embed.type === 'embed-v1' ? embed.backgroundColor : 'bg'
" "
@ -308,7 +311,7 @@
:id="'attachment-' + index" :id="'attachment-' + index"
:max-width="500" :max-width="500"
:min-width="!$vuetify.breakpoint.mobile ? 400 : 0" :min-width="!$vuetify.breakpoint.mobile ? 400 : 0"
elevaion="0" elevation="0"
color="card" color="card"
> >
<v-hover <v-hover

View file

@ -69,7 +69,10 @@ Vue.use({
{ {
transports: ["websocket", "polling"], transports: ["websocket", "polling"],
headers: { headers: {
Authorization: localStorage.getItem("session") Authorization: localStorage.getItem("token")
},
auth: {
token: localStorage.getItem("token")
} }
} }
) )

View file

@ -25,6 +25,7 @@ function getDirectRecipient(context, item) {
} }
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
desktop: !!process.env.IS_ELECTRON,
online: true, online: true,
selectedChat: null, selectedChat: null,
chats: [], chats: [],
@ -142,7 +143,7 @@ export default new Vuex.Store({
actions: { actions: {
getChats(context) { getChats(context) {
Vue.axios.defaults.headers.common["Authorization"] = Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("session") localStorage.getItem("token")
Vue.axios Vue.axios
.get("/api/v1/communications") .get("/api/v1/communications")
.then((res) => { .then((res) => {
@ -266,7 +267,7 @@ export default new Vuex.Store({
checkAuth() { checkAuth() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Vue.axios.defaults.headers.common["Authorization"] = Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("session") localStorage.getItem("token")
Vue.axios Vue.axios
.get("/api/v1/user") .get("/api/v1/user")
.then(() => { .then(() => {
@ -282,21 +283,13 @@ export default new Vuex.Store({
}) })
}, },
logout(context) { logout(context) {
Vue.axios localStorage.removeItem("userCache")
.post("/api/v1/user/logout") localStorage.removeItem("token")
.then(() => { Vue.axios.defaults.headers.common["Authorization"] = null
context.commit("setToken", null) context.commit("setUser", {
localStorage.removeItem("userCache") bcUser: null,
localStorage.removeItem("session") loggedIn: false
Vue.axios.defaults.headers.common["Authorization"] = null })
context.commit("setUser", {
bcUser: null,
loggedIn: false
})
})
.catch(() => {
Vue.$toast.error("Failed to logout.")
})
}, },
generateCache() { generateCache() {
// todo // todo
@ -366,38 +359,58 @@ export default new Vuex.Store({
} }
}) })
}, },
getUserInfo(context) { doInit(context) {
if (context.state.desktop) {
Vue.axios.defaults.baseURL = localStorage.getItem("instance")
context.state.baseURL = localStorage.getItem("instance")
}
Vue.axios.defaults.headers.common["X-Colubrina"] = true
Vue.axios.defaults.headers.common["X-Colubrina-Version"] =
context.state.versioning.version
Vue.axios.defaults.headers.common["Authorization"] = Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("session") localStorage.getItem("token")
return new Promise((resolve, reject) => { if (localStorage.getItem("customHeaders")) {
for (let header in JSON.parse(localStorage.getItem("customHeaders"))) {
Vue.axios.defaults.headers[header] = JSON.parse(
localStorage.getItem("customHeaders")
)[header]
}
}
},
getUserInfo(context) {
function setUser(user) {
try { try {
const user = JSON.parse(localStorage.getItem("userCache")) Vue.$socket.emit("connection", {
if (user?.id) { message: true
context.commit("setUser", user) })
const name = user.themeObject.id } catch {
const dark = user.themeObject.theme.dark console.log("Socket not connected")
const light = user.themeObject.theme.light }
if (user.accentColor) { localStorage.setItem("userCache", JSON.stringify(user))
user.themeObject.theme.dark.primary = user.accentColor const name = user.themeObject.id
user.themeObject.theme.light.primary = user.accentColor const dark = user.themeObject.theme.dark
} const light = user.themeObject.theme.light
Vuetify.framework.theme.themes.dark = dark if (user.accentColor) {
Vuetify.framework.theme.themes.light = light user.themeObject.theme.dark.primary = user.accentColor
Vuetify.framework.theme.themes.name = name user.themeObject.theme.light.primary = user.accentColor
Vuetify.framework.theme.themes.primaryType = }
user.themeObject.theme.primaryType Vuetify.framework.theme.themes.dark = dark
const themeElement = document.getElementById("user-theme") Vuetify.framework.theme.themes.light = light
if (!themeElement) { Vuetify.framework.theme.themes.name = name
const style = document.createElement("style") Vuetify.framework.theme.themes.primaryType =
style.id = "user-theme" user.themeObject.theme.primaryType
style.innerHTML = user.themeObject.theme.css const themeElement = document.getElementById("user-theme")
document.head.appendChild(style) if (!themeElement) {
} const style = document.createElement("style")
const fontElement = document.getElementById("user-font") style.id = "user-theme"
if (!fontElement) { style.innerHTML = user.themeObject.theme.css
const style = document.createElement("style") document.head.appendChild(style)
style.id = "user-font" }
style.innerHTML = `/* Stop from font breaking CSS code editor */ const fontElement = document.getElementById("user-font")
if (!fontElement) {
const style = document.createElement("style")
style.id = "user-font"
style.innerHTML = `/* Stop from font breaking CSS code editor */
.ace_editor div { .ace_editor div {
font-family: "JetBrains Mono" !important; font-family: "JetBrains Mono" !important;
} }
@ -406,8 +419,18 @@ div {
font-family: "${user.font}", sans-serif; font-family: "${user.font}", sans-serif;
} }
` `
document.head.appendChild(style) document.head.appendChild(style)
} }
context.commit("setLoading", false)
context.commit("setUser", user)
}
Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("token")
return new Promise((resolve, reject) => {
try {
const user = JSON.parse(localStorage.getItem("userCache"))
if (user?.id) {
setUser(user)
} }
} catch { } catch {
// //
@ -415,94 +438,113 @@ div {
Vue.axios Vue.axios
.get("/api/v1/user") .get("/api/v1/user")
.then((res) => { .then((res) => {
context.commit("setUser", res.data) setUser(res.data)
try {
Vue.$socket.emit("connection", {
message: true
})
} catch {
console.log("Socket not connected")
}
localStorage.setItem("userCache", JSON.stringify(res.data))
const name = res.data.themeObject.id
const dark = res.data.themeObject.theme.dark
const light = res.data.themeObject.theme.light
if (res.data.accentColor) {
res.data.themeObject.theme.dark.primary = res.data.accentColor
res.data.themeObject.theme.light.primary = res.data.accentColor
}
Vuetify.framework.theme.themes.dark = dark
Vuetify.framework.theme.themes.light = light
Vuetify.framework.theme.themes.name = name
Vuetify.framework.theme.themes.primaryType =
res.data.themeObject.theme.primaryType
const themeElement = document.getElementById("user-theme")
if (!themeElement) {
const style = document.createElement("style")
style.id = "user-theme"
style.innerHTML = res.data.themeObject.theme.css
document.head.appendChild(style)
}
const fontElement = document.getElementById("user-font")
if (!fontElement) {
const style = document.createElement("style")
style.id = "user-font"
style.innerHTML = `/* Stop from font breaking CSS code editor */
.ace_editor div {
font-family: "JetBrains Mono" !important;
}
div {
font-family: "${res.data.font}", sans-serif;
}
`
document.head.appendChild(style)
}
context.commit("setLoading", false) context.commit("setLoading", false)
context.commit("setOnline", true) context.commit("setOnline", true)
resolve(res.data) resolve(res.data)
}) })
.catch((e) => { .catch((e) => {
if (JSON.parse(localStorage.getItem("userCache"))?.id) { try {
const user = JSON.parse(localStorage.getItem("userCache")) const user = JSON.parse(localStorage.getItem("userCache"))
const name = user.themeObject.id if (user?.id && !e?.response?.data?.errors?.length) {
const dark = user.themeObject.theme.dark setUser(user)
const light = user.themeObject.theme.light } else {
if (user.accentColor) { const theme = {
user.themeObject.theme.dark.primary = user.accentColor id: 1,
user.themeObject.theme.light.primary = user.accentColor name: "Colubrina Classic",
primaryType: "all",
dark: {
primary: "#0190ea",
secondary: "#757575",
accent: "#000000",
error: "#ff1744",
info: "#2196F3",
success: "#4CAF50",
warning: "#ff9800",
card: "#151515",
toolbar: "#191919",
sheet: "#181818",
text: "#000000",
dark: "#151515",
bg: "#151515"
},
light: {
primary: "#0190ea",
secondary: "#757575",
accent: "#000000",
error: "#ff1744",
info: "#2196F3",
success: "#4CAF50",
warning: "#ff9800",
card: "#f8f8f8",
toolbar: "#f8f8f8",
sheet: "#f8f8f8",
text: "#000000",
dark: "#f8f8f8",
bg: "#f8f8f8"
}
}
const name = theme.id
const dark = theme.dark
const light = theme.light
Vuetify.framework.theme.themes.dark = dark
Vuetify.framework.theme.themes.light = light
Vuetify.framework.theme.themes.name = name
this.name = name
console.log("Failed to load Colubrina Account")
context.user = null
localStorage.removeItem("userCache")
reject(e)
} }
} catch {
const theme = {
id: 1,
name: "Colubrina Classic",
primaryType: "all",
dark: {
primary: "#0190ea",
secondary: "#757575",
accent: "#000000",
error: "#ff1744",
info: "#2196F3",
success: "#4CAF50",
warning: "#ff9800",
card: "#151515",
toolbar: "#191919",
sheet: "#181818",
text: "#000000",
dark: "#151515",
bg: "#151515"
},
light: {
primary: "#0190ea",
secondary: "#757575",
accent: "#000000",
error: "#ff1744",
info: "#2196F3",
success: "#4CAF50",
warning: "#ff9800",
card: "#f8f8f8",
toolbar: "#f8f8f8",
sheet: "#f8f8f8",
text: "#000000",
dark: "#f8f8f8",
bg: "#f8f8f8"
}
}
const name = theme.id
const dark = theme.dark
const light = theme.light
Vuetify.framework.theme.themes.dark = dark Vuetify.framework.theme.themes.dark = dark
Vuetify.framework.theme.themes.light = light Vuetify.framework.theme.themes.light = light
Vuetify.framework.theme.themes.name = name Vuetify.framework.theme.themes.name = name
Vuetify.framework.theme.themes.primaryType = this.name = name
user.themeObject.theme.primaryType console.log("Failed to load Colubrina Account")
const themeElement = document.getElementById("user-theme") localStorage.removeItem("userCache")
if (!themeElement) { context.user = null
const style = document.createElement("style") reject(e)
style.id = "user-theme" }
style.innerHTML = user.themeObject.theme.css if (!localStorage.getItem("userCache")) {
document.head.appendChild(style)
}
const fontElement = document.getElementById("user-font")
if (!fontElement) {
const style = document.createElement("style")
style.id = "user-font"
style.innerHTML = `/* Stop from font breaking CSS code editor */
.ace_editor div {
font-family: "JetBrains Mono" !important;
}
div {
font-family: "${user.font}", sans-serif;
}
`
document.head.appendChild(style)
}
context.commit("setLoading", false)
context.commit("setUser", user)
resolve(user)
} else {
const theme = { const theme = {
id: 1, id: 1,
name: "Colubrina Classic", name: "Colubrina Classic",

View file

@ -1,17 +1,11 @@
<template> <template>
<div id="communications"> <div id="communications">
<v-container fluid> <router-view
<v-row> v-if="$route.params.id !== 'home'"
<v-col> :chat="selectedChat"
<router-view :loading="false"
v-if="$route.params.id !== 'home'" :items="$store.state.chats"
:chat="selectedChat" ></router-view>
:loading="false"
:items="$store.state.chats"
></router-view>
</v-col>
</v-row>
</v-container>
</div> </div>
</template> </template>

View file

@ -169,8 +169,8 @@
<v-row v-if="!loading" @drop="handleDrag" no-gutters> <v-row v-if="!loading" @drop="handleDrag" no-gutters>
<v-col class="flex-grow-1 flex-shrink-1 pb-0" id="chat-col"> <v-col class="flex-grow-1 flex-shrink-1 pb-0" id="chat-col">
<v-card <v-card
class="d-flex flex-column fill-height rounded-xl" class="d-flex flex-column fill-height rounded-xl mb-n3"
style="overflow: auto; height: calc(100vh - 24px - 40px - 40px)" style="overflow: auto; height: calc(100vh - 24px - 40px)"
color="card" color="card"
elevation="0" elevation="0"
> >
@ -202,32 +202,30 @@
style="display: block; width: 100px; margin: 0 auto" style="display: block; width: 100px; margin: 0 auto"
></v-progress-circular> ></v-progress-circular>
<template v-for="(message, index) in messages"> <template v-for="(message, index) in messages">
<div :key="'div-' + message.keyId"> <Message
<Message :key="message.keyId"
:key="message.keyId" :message="message"
:message="message" :jump-to-message="jumpToMessage"
:jump-to-message="jumpToMessage" :edit="edit"
:edit="edit" :focus-input="focusInput"
:focus-input="focusInput" :replying="setReply"
:replying="setReply" :get-name="getName"
:get-name="getName" :end-edit="endEdit"
:end-edit="endEdit" :auto-scroll="autoScroll"
:auto-scroll="autoScroll" :chat="chat"
:chat="chat" :index="index"
:index="index" :show="show"
:show="show" :set-image-preview="setImagePreview"
:set-image-preview="setImagePreview" :delete-message="deleteMessage"
:delete-message="deleteMessage" :last-message="
:last-message=" messages[index - 1]?.userId === message?.userId &&
messages[index - 1]?.userId === message?.userId && $date(message.createdAt).diff(
$date(message.createdAt).diff( messages[index - 1]?.createdAt,
messages[index - 1]?.createdAt, 'minute'
'minute' ) < 10 &&
) < 10 && !message.replyId
!message.replyId "
" ></Message>
></Message>
</div>
<div <div
:key="'div2-' + message.keyId" :key="'div2-' + message.keyId"
v-if="message.readReceipts.length" v-if="message.readReceipts.length"
@ -277,31 +275,33 @@
</template> </template>
<v-tooltip top> <v-tooltip top>
<template v-slot:activator="{ on }"> <template v-slot:activator="{ on }">
<v-btn <span>
icon <v-btn
small icon
fab small
width="20" fab
height="20" width="20"
class="ml-2 mt-2" height="20"
style="float: right" class="ml-2 mt-2"
@click="openUserPanel($store.state.user)" style="float: right"
> @click="openUserPanel($store.state.user)"
<v-avatar size="20" v-on="on" color="primary"> >
<img <v-avatar size="20" v-on="on" color="primary">
v-if="$store.state.user.avatar" <img
:src=" v-if="$store.state.user.avatar"
$store.state.baseURL + :src="
'/usercontent/' + $store.state.baseURL +
$store.state.user.avatar '/usercontent/' +
" $store.state.user.avatar
alt="avatar" "
/> alt="avatar"
<span v-else>{{ />
$store.state.user.username[0].toUpperCase() <span v-else>{{
}}</span> $store.state.user.username[0].toUpperCase()
</v-avatar> }}</span>
</v-btn> </v-avatar>
</v-btn>
</span>
</template> </template>
<span> <span>
{{ $store.state.user.username }} has read up to this point. {{ $store.state.user.username }} has read up to this point.
@ -368,25 +368,18 @@
v-model="usersTyping.length" v-model="usersTyping.length"
v-if="$vuetify.breakpoint.mobile" v-if="$vuetify.breakpoint.mobile"
> >
<v-toolbar <div
height="22"
color="toolbar"
elevation="0"
style=" style="
border-radius: 20px 20px 0 0; border-radius: 0 0 20px 20px;
cursor: pointer;
position: relative; position: relative;
top: -30px; top: -30px;
margin-bottom: -27px; margin-bottom: -22px;
" "
width="100%"
v-if="usersTyping.length" v-if="usersTyping.length"
> >
<div> {{ usersTyping.map((user) => getName(user)).join(", ") }}
{{ usersTyping.map((user) => getName(user)).join(", ") }} {{ usersTyping.length > 1 ? " are" : " is" }} typing...
{{ usersTyping.length > 1 ? " are" : " is" }} typing... </div>
</div>
</v-toolbar>
</v-fade-transition> </v-fade-transition>
<CommsInput <CommsInput
:chat="chat" :chat="chat"
@ -402,9 +395,9 @@
<div <div
style=" style="
border-radius: 0 0 20px 20px; border-radius: 0 0 20px 20px;
position: relative; position: absolute;
margin-top: -22px; margin-top: -2px;
bottom: -16px; bottom: 1px;
" "
v-if="usersTyping.length" v-if="usersTyping.length"
> >
@ -415,14 +408,6 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>
<v-divider
vertical
style="z-index: 2; padding-right: 3px; padding-left: 3px"
v-if="
($store.state.userPanel && !$vuetify.breakpoint.mobile) ||
($store.state.searchPanel && !$vuetify.breakpoint.mobile)
"
></v-divider>
<v-col <v-col
cols="3" cols="3"
class="" class=""
@ -431,7 +416,7 @@
> >
<v-card <v-card
class="d-flex flex-column fill-height" class="d-flex flex-column fill-height"
style="overflow: scroll; height: calc(100vh - 24px - 40px - 40px)" style="overflow: scroll; height: calc(100vh - 24px - 40px)"
color="card" color="card"
elevation="0" elevation="0"
> >
@ -484,7 +469,6 @@
</v-col> </v-col>
<v-col <v-col
:cols="$vuetify.breakpoint.xl ? 2 : 3" :cols="$vuetify.breakpoint.xl ? 2 : 3"
class="ml-2"
id="user-col" id="user-col"
v-if=" v-if="
$store.state.userPanel && $store.state.userPanel &&
@ -493,10 +477,10 @@
" "
> >
<v-card <v-card
class="d-flex flex-column fill-height rounded-xl" class="d-flex flex-column fill-height rounded-0"
elevation="0" elevation="0"
style="overflow: scroll; height: calc(100vh - 24px - 40px - 40px)" style="overflow: scroll; height: calc(100vh - 24px - 40px)"
color="card" color="sheet"
> >
<v-menu <v-menu
v-model="context.user.value" v-model="context.user.value"
@ -520,7 +504,7 @@
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
<v-list two-line color="card"> <v-list two-line color="sheet">
<v-list-item-group class="rounded-xl"> <v-list-item-group class="rounded-xl">
<template v-for="item in associations"> <template v-for="item in associations">
<v-list-item <v-list-item

View file

@ -373,7 +373,8 @@ export default {
}) })
} }
}, },
mounted() { async mounted() {
await this.$store.dispatch("doInit")
this.getFriends() this.getFriends()
this.getUsers() this.getUsers()
this.$socket.on("friendRequest", () => { this.$socket.on("friendRequest", () => {

View file

@ -123,7 +123,8 @@ export default {
totp: "", totp: "",
totpDialog: false, totpDialog: false,
loading: false, loading: false,
instance: "https://colubrina.troplo.com", instance:
localStorage.getItem("instance") || "https://colubrina.troplo.com",
customHeaders: {} customHeaders: {}
} }
}, },
@ -149,14 +150,17 @@ export default {
}) })
.then(async (res) => { .then(async (res) => {
const session = const session =
res.data.session || res.data.bcToken || res.data.cookieToken res.data.session ||
res.data.token ||
localStorage.setItem("session", session) res.data.bcToken ||
res.data.cookieToken
localStorage.setItem("token", session)
Vue.axios.defaults.headers.common["Authorization"] = session Vue.axios.defaults.headers.common["Authorization"] = session
this.$store.commit("setToken", session) this.$store.commit("setToken", session)
await this.$store.dispatch("getUserInfo") await this.$store.dispatch("getUserInfo")
this.$store.dispatch("getChats") this.$store.dispatch("getChats")
this.loading = false this.loading = false
this.$socket.auth.token = session
this.$socket.disconnect() this.$socket.disconnect()
this.$socket.connect() this.$socket.connect()
if (this.isElectron()) { if (this.isElectron()) {
@ -204,9 +208,17 @@ export default {
} }
}, },
mounted() { mounted() {
if (this.$store.state.user?.id) { this.$store
this.$router.push("/") .dispatch("getUserInfo")
} .then(() => {
this.$router.push("/")
})
.catch(() => {
this.$store.state.user = {
bcUser: null,
loggedIn: false
}
})
this.testInstance() this.testInstance()
}, },
watch: { watch: {

View file

@ -96,7 +96,8 @@ export default {
totp: "", totp: "",
totpDialog: false, totpDialog: false,
loading: false, loading: false,
instance: "https://colubrina.troplo.com", instance:
localStorage.getItem("instance") || "https://colubrina.troplo.com",
instanceString: "" instanceString: ""
} }
}, },
@ -131,11 +132,18 @@ export default {
totp: this.totp totp: this.totp
}) })
.then(async (res) => { .then(async (res) => {
localStorage.setItem("session", res.data.session) const session =
Vue.axios.defaults.headers.common["Authorization"] = res.data.session res.data.session ||
this.$store.commit("setToken", res.data.session) res.data.token ||
res.data.bcToken ||
res.data.cookieToken
localStorage.setItem("token", session)
Vue.axios.defaults.headers.common["Authorization"] = session
this.$store.commit("setToken", session)
await this.$store.dispatch("getUserInfo") await this.$store.dispatch("getUserInfo")
this.$store.dispatch("getChats")
this.loading = false this.loading = false
this.$socket.auth.token = session
this.$socket.disconnect() this.$socket.disconnect()
this.$socket.connect() this.$socket.connect()
if (this.isElectron()) { if (this.isElectron()) {
@ -169,9 +177,17 @@ export default {
} }
}, },
mounted() { mounted() {
if (this.$store.state.user?.id) { this.$store
this.$router.push("/") .dispatch("getUserInfo")
} .then(() => {
this.$router.push("/")
})
.catch(() => {
this.$store.state.user = {
bcUser: null,
loggedIn: false
}
})
this.testInstance() this.testInstance()
}, },
watch: { watch: {

View file

@ -5311,18 +5311,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
integrity sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w== integrity sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==
graphql-tag@^2.12.6:
version "2.12.6"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==
dependencies:
tslib "^2.1.0"
graphql@^16.3.0:
version "16.5.0"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.5.0.tgz#41b5c1182eaac7f3d47164fb247f61e4dfb69c85"
integrity sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA==
gzip-size@^5.0.0: gzip-size@^5.0.0:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
@ -9802,11 +9790,6 @@ thread-loader@^2.1.3:
loader-utils "^1.1.0" loader-utils "^1.1.0"
neo-async "^2.6.0" neo-async "^2.6.0"
throttle-debounce@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
through2-filter@^3.0.0: through2-filter@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
@ -10393,15 +10376,6 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vue-apollo@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.1.0.tgz#d0370b64f928f488b7ad98177cca8e1a82fa725e"
integrity sha512-TkXMpgypbqM/27vKIpWNhL347QRtcu9kWkEmxplPGGMnZb+hSbbIwU07LjQVzFwrLBer0ha9zJP4hIW/fCQDuw==
dependencies:
chalk "^2.4.2"
serialize-javascript "^4.0.0"
throttle-debounce "^2.1.0"
vue-axios@^3.4.1: vue-axios@^3.4.1:
version "3.4.1" version "3.4.1"
resolved "https://registry.yarnpkg.com/vue-axios/-/vue-axios-3.4.1.tgz#e092ec4060b42e941ea5059df9b8f09058c3eea9" resolved "https://registry.yarnpkg.com/vue-axios/-/vue-axios-3.4.1.tgz#e092ec4060b42e941ea5059df9b8f09058c3eea9"
@ -10490,11 +10464,6 @@ vue-loader@^15.9.2:
vue-hot-reload-api "^2.3.0" vue-hot-reload-api "^2.3.0"
vue-style-loader "^4.1.0" vue-style-loader "^4.1.0"
vue-matomo@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/vue-matomo/-/vue-matomo-4.1.0.tgz#17b513bc8ac60e168bfa8190ad05b3257a3c441e"
integrity sha512-y+tdmhY835Ip3EAGfIAgA33+aBYrvRT7fNnBnA7bSM459XpoWXgqJKdbopVpEUrxCPIq8IkuF7g4KqSLc0Fa3w==
vue-native-notification@^1.1.1: vue-native-notification@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/vue-native-notification/-/vue-native-notification-1.1.1.tgz#8a57950dded83b05b9e261f45bc2e027107eb9cd" resolved "https://registry.yarnpkg.com/vue-native-notification/-/vue-native-notification-1.1.1.tgz#8a57950dded83b05b9e261f45bc2e027107eb9cd"
@ -10528,11 +10497,6 @@ vue-style-loader@^4.1.0, vue-style-loader@^4.1.2:
hash-sum "^1.0.2" hash-sum "^1.0.2"
loader-utils "^1.0.2" loader-utils "^1.0.2"
vue-swatches@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/vue-swatches/-/vue-swatches-2.1.1.tgz#26c467fb7648ff4ee0887aea36d1e03b15032b83"
integrity sha512-YugkNbByxMz1dnx1nZyHSL3VSf/TnBH3/NQD+t8JKxPSqUmX87sVGBxjEaqH5IMraOLfVmU0pHCHl2BfXNypQg==
vue-template-babel-compiler@^1.1.3: vue-template-babel-compiler@^1.1.3:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/vue-template-babel-compiler/-/vue-template-babel-compiler-1.2.0.tgz#f77126886e868f64d2acefe5c0ecfd9c323bf943" resolved "https://registry.yarnpkg.com/vue-template-babel-compiler/-/vue-template-babel-compiler-1.2.0.tgz#f77126886e868f64d2acefe5c0ecfd9c323bf943"