Support for VIP users via JWT Auth
This commit is contained in:
parent
f65f653430
commit
6e2aa517f5
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
// Version of the config format - when new fields are added, it will attempt
|
// Version of the config format - when new fields are added, it will attempt
|
||||||
// to write the settings.toml to disk so new defaults populate.
|
// to write the settings.toml to disk so new defaults populate.
|
||||||
var currentVersion = 6
|
var currentVersion = 7
|
||||||
|
|
||||||
// Config for your BareRTC app.
|
// Config for your BareRTC app.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -45,6 +45,8 @@ type Config struct {
|
||||||
PublicChannels []Channel
|
PublicChannels []Channel
|
||||||
|
|
||||||
WebhookURLs []WebhookURL
|
WebhookURLs []WebhookURL
|
||||||
|
|
||||||
|
VIP VIP
|
||||||
}
|
}
|
||||||
|
|
||||||
type TurnConfig struct {
|
type TurnConfig struct {
|
||||||
|
@ -53,6 +55,13 @@ type TurnConfig struct {
|
||||||
Credential string
|
Credential string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VIP struct {
|
||||||
|
Name string
|
||||||
|
Branding string
|
||||||
|
Icon string
|
||||||
|
MutuallySecret bool
|
||||||
|
}
|
||||||
|
|
||||||
// GetChannels returns a JavaScript safe array of the default PublicChannels.
|
// GetChannels returns a JavaScript safe array of the default PublicChannels.
|
||||||
func (c Config) GetChannels() template.JS {
|
func (c Config) GetChannels() template.JS {
|
||||||
data, _ := json.Marshal(c.PublicChannels)
|
data, _ := json.Marshal(c.PublicChannels)
|
||||||
|
@ -121,6 +130,11 @@ func DefaultConfig() Config {
|
||||||
URL: "https://example.com/barertc/report",
|
URL: "https://example.com/barertc/report",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
VIP: VIP{
|
||||||
|
Name: "VIP",
|
||||||
|
Branding: "<em>VIP Members</em>",
|
||||||
|
Icon: "fa fa-circle",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
c.JWT.Strict = true
|
c.JWT.Strict = true
|
||||||
return c
|
return c
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
// Custom claims.
|
// Custom claims.
|
||||||
IsAdmin bool `json:"op,omitempty"`
|
IsAdmin bool `json:"op,omitempty"`
|
||||||
|
VIP bool `json:"vip,omitempty"`
|
||||||
Avatar string `json:"img,omitempty"`
|
Avatar string `json:"img,omitempty"`
|
||||||
ProfileURL string `json:"url,omitempty"`
|
ProfileURL string `json:"url,omitempty"`
|
||||||
Nick string `json:"nick,omitempty"`
|
Nick string `json:"nick,omitempty"`
|
||||||
|
|
|
@ -107,6 +107,7 @@ type WhoList struct {
|
||||||
|
|
||||||
// JWT auth extra settings.
|
// JWT auth extra settings.
|
||||||
Operator bool `json:"op"`
|
Operator bool `json:"op"`
|
||||||
|
VIP bool `json:"vip,omitempty"`
|
||||||
Avatar string `json:"avatar,omitempty"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
ProfileURL string `json:"profileURL,omitempty"`
|
ProfileURL string `json:"profileURL,omitempty"`
|
||||||
Emoji string `json:"emoji,omitempty"`
|
Emoji string `json:"emoji,omitempty"`
|
||||||
|
@ -122,4 +123,5 @@ const (
|
||||||
VideoFlagIsTalking // broadcaster seems to be talking
|
VideoFlagIsTalking // broadcaster seems to be talking
|
||||||
VideoFlagMutualRequired // video wants viewers to share their camera too
|
VideoFlagMutualRequired // video wants viewers to share their camera too
|
||||||
VideoFlagMutualOpen // viewer wants to auto-open viewers' cameras
|
VideoFlagMutualOpen // viewer wants to auto-open viewers' cameras
|
||||||
|
VideoFlagOnlyVIP // can only shows as active to VIP members
|
||||||
)
|
)
|
||||||
|
|
|
@ -126,6 +126,11 @@ func (sub *Subscriber) IsAdmin() bool {
|
||||||
return sub.JWTClaims != nil && sub.JWTClaims.IsAdmin
|
return sub.JWTClaims != nil && sub.JWTClaims.IsAdmin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsVIP safely checks if the subscriber has VIP status.
|
||||||
|
func (sub *Subscriber) IsVIP() bool {
|
||||||
|
return sub.JWTClaims != nil && sub.JWTClaims.VIP
|
||||||
|
}
|
||||||
|
|
||||||
// SendJSON sends a JSON message to the websocket client.
|
// SendJSON sends a JSON message to the websocket client.
|
||||||
func (sub *Subscriber) SendJSON(v interface{}) error {
|
func (sub *Subscriber) SendJSON(v interface{}) error {
|
||||||
data, err := json.Marshal(v)
|
data, err := json.Marshal(v)
|
||||||
|
@ -400,9 +405,19 @@ func (s *Server) SendWhoList() {
|
||||||
LoginAt: user.loginAt.Unix(),
|
LoginAt: user.loginAt.Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this person had booted us, force their camera to "off"
|
// Hide video flags of other users (never for the current user).
|
||||||
if (user.Boots(sub.Username) || user.Mutes(sub.Username)) && !sub.IsAdmin() {
|
if user.Username != sub.Username {
|
||||||
who.Video = 0
|
|
||||||
|
// If this person had booted us, force their camera to "off"
|
||||||
|
if (user.Boots(sub.Username) || user.Mutes(sub.Username)) && !sub.IsAdmin() {
|
||||||
|
who.Video = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this person's VideoFlag is set to VIP Only, force their camera to "off"
|
||||||
|
// except when the person looking has the VIP status.
|
||||||
|
if (user.VideoStatus&messages.VideoFlagOnlyVIP == messages.VideoFlagOnlyVIP) && (!sub.IsVIP() && !sub.IsAdmin()) {
|
||||||
|
who.Video = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.JWTClaims != nil {
|
if user.JWTClaims != nil {
|
||||||
|
@ -412,6 +427,16 @@ func (s *Server) SendWhoList() {
|
||||||
who.Nickname = user.JWTClaims.Nick
|
who.Nickname = user.JWTClaims.Nick
|
||||||
who.Emoji = user.JWTClaims.Emoji
|
who.Emoji = user.JWTClaims.Emoji
|
||||||
who.Gender = user.JWTClaims.Gender
|
who.Gender = user.JWTClaims.Gender
|
||||||
|
|
||||||
|
// VIP flags: if we are in MutuallySecret mode, only VIPs can see
|
||||||
|
// other VIP flags on the Who List.
|
||||||
|
if config.Current.VIP.MutuallySecret {
|
||||||
|
if sub.IsVIP() || sub.IsAdmin() {
|
||||||
|
who.VIP = user.JWTClaims.VIP
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
who.VIP = user.JWTClaims.VIP
|
||||||
|
}
|
||||||
}
|
}
|
||||||
users = append(users, who)
|
users = append(users, who)
|
||||||
}
|
}
|
||||||
|
|
|
@ -336,3 +336,8 @@ div.feed.popped-out {
|
||||||
.has-text-gender-other {
|
.has-text-gender-other {
|
||||||
color: #cc00cc !important;
|
color: #cc00cc !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* VIP colors for profile icon */
|
||||||
|
.has-background-vip {
|
||||||
|
background-image: linear-gradient(141deg, #d1e1ff 0, #ffddff 100%)
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ const app = Vue.createApp({
|
||||||
website: WebsiteURL,
|
website: WebsiteURL,
|
||||||
permitNSFW: PermitNSFW,
|
permitNSFW: PermitNSFW,
|
||||||
webhookURLs: WebhookURLs,
|
webhookURLs: WebhookURLs,
|
||||||
|
VIP: VIP,
|
||||||
fontSizeClasses: [
|
fontSizeClasses: [
|
||||||
[ "x-2", "Very small chat room text" ],
|
[ "x-2", "Very small chat room text" ],
|
||||||
[ "x-1", "50% smaller chat room text" ],
|
[ "x-1", "50% smaller chat room text" ],
|
||||||
|
@ -147,6 +148,7 @@ const app = Vue.createApp({
|
||||||
nsfw: false, // user has flagged their camera to be NSFW
|
nsfw: false, // user has flagged their camera to be NSFW
|
||||||
mutual: false, // user wants viewers to share their own videos
|
mutual: false, // user wants viewers to share their own videos
|
||||||
mutualOpen: false, // user wants to open video mutually
|
mutualOpen: false, // user wants to open video mutually
|
||||||
|
vipOnly: false, // only show camera to fellow VIP users
|
||||||
|
|
||||||
// Who all is watching me? map of users.
|
// Who all is watching me? map of users.
|
||||||
watching: {},
|
watching: {},
|
||||||
|
@ -186,6 +188,7 @@ const app = Vue.createApp({
|
||||||
IsTalking: 1 << 3,
|
IsTalking: 1 << 3,
|
||||||
MutualRequired: 1 << 4,
|
MutualRequired: 1 << 4,
|
||||||
MutualOpen: 1 << 5,
|
MutualOpen: 1 << 5,
|
||||||
|
VipOnly: 1 << 6,
|
||||||
},
|
},
|
||||||
|
|
||||||
// WebRTC sessions with other users.
|
// WebRTC sessions with other users.
|
||||||
|
@ -386,6 +389,11 @@ const app = Vue.createApp({
|
||||||
this.sendMe();
|
this.sendMe();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"webcam.vipOnly": function() {
|
||||||
|
if (this.webcam.active) {
|
||||||
|
this.sendMe();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Misc preference watches
|
// Misc preference watches
|
||||||
"prefs.joinMessages": function() {
|
"prefs.joinMessages": function() {
|
||||||
|
@ -495,6 +503,10 @@ const app = Vue.createApp({
|
||||||
// Returns if the current user has operator rights
|
// Returns if the current user has operator rights
|
||||||
return this.jwt.claims.op;
|
return this.jwt.claims.op;
|
||||||
},
|
},
|
||||||
|
isVIP() {
|
||||||
|
// Returns if the current user has VIP rights.
|
||||||
|
return this.jwt.claims.vip;
|
||||||
|
},
|
||||||
myVideoFlag() {
|
myVideoFlag() {
|
||||||
// Compute the current user's video status flags.
|
// Compute the current user's video status flags.
|
||||||
let status = 0;
|
let status = 0;
|
||||||
|
@ -504,6 +516,7 @@ const app = Vue.createApp({
|
||||||
if (this.webcam.nsfw) status |= this.VideoFlag.NSFW;
|
if (this.webcam.nsfw) status |= this.VideoFlag.NSFW;
|
||||||
if (this.webcam.mutual) status |= this.VideoFlag.MutualRequired;
|
if (this.webcam.mutual) status |= this.VideoFlag.MutualRequired;
|
||||||
if (this.webcam.mutualOpen) status |= this.VideoFlag.MutualOpen;
|
if (this.webcam.mutualOpen) status |= this.VideoFlag.MutualOpen;
|
||||||
|
if (this.webcam.vipOnly && this.isVIP) status |= this.VideoFlag.VipOnly;
|
||||||
return status;
|
return status;
|
||||||
},
|
},
|
||||||
sortedWhoList() {
|
sortedWhoList() {
|
||||||
|
@ -596,6 +609,9 @@ const app = Vue.createApp({
|
||||||
if (localStorage.videoAutoMute === "true") {
|
if (localStorage.videoAutoMute === "true") {
|
||||||
this.webcam.autoMute = true;
|
this.webcam.autoMute = true;
|
||||||
}
|
}
|
||||||
|
if (localStorage.videoVipOnly === "true") {
|
||||||
|
this.webcam.vipOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Misc preferences
|
// Misc preferences
|
||||||
if (localStorage.joinMessages != undefined) {
|
if (localStorage.joinMessages != undefined) {
|
||||||
|
@ -1613,6 +1629,7 @@ const app = Vue.createApp({
|
||||||
localStorage.videoMutual = this.webcam.mutual;
|
localStorage.videoMutual = this.webcam.mutual;
|
||||||
localStorage.videoMutualOpen = this.webcam.mutualOpen;
|
localStorage.videoMutualOpen = this.webcam.mutualOpen;
|
||||||
localStorage.videoAutoMute = this.webcam.autoMute;
|
localStorage.videoAutoMute = this.webcam.autoMute;
|
||||||
|
localStorage.videoVipOnly = this.webcam.vipOnly;
|
||||||
|
|
||||||
// Auto-mute our camera? Two use cases:
|
// Auto-mute our camera? Two use cases:
|
||||||
// 1. The user marked their cam as muted but then changed video device,
|
// 1. The user marked their cam as muted but then changed video device,
|
||||||
|
@ -2349,13 +2366,19 @@ const app = Vue.createApp({
|
||||||
|
|
||||||
// CSS classes for the profile button (color coded genders)
|
// CSS classes for the profile button (color coded genders)
|
||||||
profileButtonClass(user) {
|
profileButtonClass(user) {
|
||||||
|
// VIP background.
|
||||||
|
let result = "";
|
||||||
|
if (user.vip) {
|
||||||
|
result = "has-background-vip ";
|
||||||
|
}
|
||||||
|
|
||||||
let gender = (user.gender || "").toLowerCase();
|
let gender = (user.gender || "").toLowerCase();
|
||||||
if (gender.indexOf("m") === 0) {
|
if (gender.indexOf("m") === 0) {
|
||||||
return "has-text-gender-male";
|
return result+"has-text-gender-male";
|
||||||
} else if (gender.indexOf("f") === 0) {
|
} else if (gender.indexOf("f") === 0) {
|
||||||
return "has-text-gender-female";
|
return result+"has-text-gender-female";
|
||||||
} else if (gender.length > 0) {
|
} else if (gender.length > 0) {
|
||||||
return "has-text-gender-other";
|
return result+"has-text-gender-other";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
|
|
@ -330,7 +330,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field mb-1">
|
||||||
<label class="checkbox"
|
<label class="checkbox"
|
||||||
:class="{'cursor-notallowed': !webcam.active}">
|
:class="{'cursor-notallowed': !webcam.active}">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
|
@ -340,6 +340,17 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field" v-if="isVIP">
|
||||||
|
<label class="checkbox"
|
||||||
|
:class="{'cursor-notallowed': !webcam.active}">
|
||||||
|
<input type="checkbox"
|
||||||
|
v-model="webcam.vipOnly"
|
||||||
|
:disabled="!webcam.active">
|
||||||
|
Only <span v-html="config.VIP.Branding"></span> <sup class="is-size-7" :class="config.VIP.Icon"></sup>
|
||||||
|
members can see that my camera is broadcasting
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 class="subtitle mb-2" v-if="webcam.videoDevices.length > 0 || webcam.audioDevices.length > 0">
|
<h3 class="subtitle mb-2" v-if="webcam.videoDevices.length > 0 || webcam.audioDevices.length > 0">
|
||||||
Webcam Devices
|
Webcam Devices
|
||||||
<button type="button" class="button is-primary is-small is-outlined ml-2"
|
<button type="button" class="button is-primary is-small is-outlined ml-2"
|
||||||
|
@ -501,6 +512,16 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field" v-if="isVIP">
|
||||||
|
<label class="checkbox"
|
||||||
|
:class="{'cursor-notallowed': !webcam.active}">
|
||||||
|
<input type="checkbox"
|
||||||
|
v-model="webcam.vipOnly">
|
||||||
|
Only <span v-html="config.VIP.Branding"></span> <sup class="is-size-7" :class="config.VIP.Icon"></sup>
|
||||||
|
members can see that my camera is broadcasting
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Device Pickers: just in case the user had granted video permission in the past,
|
<!-- Device Pickers: just in case the user had granted video permission in the past,
|
||||||
and we are able to enumerate their device names, we can show them here before they
|
and we are able to enumerate their device names, we can show them here before they
|
||||||
go on this time.-->
|
go on this time.-->
|
||||||
|
@ -1413,6 +1434,10 @@
|
||||||
<sup class="fa fa-peace has-text-warning-dark is-size-7 ml-1"
|
<sup class="fa fa-peace has-text-warning-dark is-size-7 ml-1"
|
||||||
v-if="u.op"
|
v-if="u.op"
|
||||||
title="Operator"></sup>
|
title="Operator"></sup>
|
||||||
|
<sup class="is-size-7 ml-1"
|
||||||
|
:class="config.VIP.Icon"
|
||||||
|
v-else-if="u.vip"
|
||||||
|
:title="config.VIP.Name"></sup>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow pl-0">
|
<div class="column is-narrow pl-0">
|
||||||
<!-- Emoji icon -->
|
<!-- Emoji icon -->
|
||||||
|
@ -1427,7 +1452,7 @@
|
||||||
class="button is-small px-2 py-1"
|
class="button is-small px-2 py-1"
|
||||||
:class="profileButtonClass(u)"
|
:class="profileButtonClass(u)"
|
||||||
@click="openProfile(u)"
|
@click="openProfile(u)"
|
||||||
:title="'Open profile page' + (u.gender ? ` (gender: ${u.gender})` : '')">
|
:title="'Open profile page' + (u.gender ? ` (gender: ${u.gender})` : '') + (u.vip ? ` (${config.VIP.Name})` : '')">
|
||||||
<i class="fa fa-user"></i>
|
<i class="fa fa-user"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -1507,6 +1532,7 @@ const WebsiteURL = "{{.Config.WebsiteURL}}";
|
||||||
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
||||||
const TURN = {{.Config.TURN}};
|
const TURN = {{.Config.TURN}};
|
||||||
const WebhookURLs = {{.Config.WebhookURLs}};
|
const WebhookURLs = {{.Config.WebhookURLs}};
|
||||||
|
const VIP = {{.Config.VIP}};
|
||||||
const UserJWTToken = {{.JWTTokenString}};
|
const UserJWTToken = {{.JWTTokenString}};
|
||||||
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
|
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
|
||||||
const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user