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
|
||||
// to write the settings.toml to disk so new defaults populate.
|
||||
var currentVersion = 6
|
||||
var currentVersion = 7
|
||||
|
||||
// Config for your BareRTC app.
|
||||
type Config struct {
|
||||
|
@ -45,6 +45,8 @@ type Config struct {
|
|||
PublicChannels []Channel
|
||||
|
||||
WebhookURLs []WebhookURL
|
||||
|
||||
VIP VIP
|
||||
}
|
||||
|
||||
type TurnConfig struct {
|
||||
|
@ -53,6 +55,13 @@ type TurnConfig struct {
|
|||
Credential string
|
||||
}
|
||||
|
||||
type VIP struct {
|
||||
Name string
|
||||
Branding string
|
||||
Icon string
|
||||
MutuallySecret bool
|
||||
}
|
||||
|
||||
// GetChannels returns a JavaScript safe array of the default PublicChannels.
|
||||
func (c Config) GetChannels() template.JS {
|
||||
data, _ := json.Marshal(c.PublicChannels)
|
||||
|
@ -121,6 +130,11 @@ func DefaultConfig() Config {
|
|||
URL: "https://example.com/barertc/report",
|
||||
},
|
||||
},
|
||||
VIP: VIP{
|
||||
Name: "VIP",
|
||||
Branding: "<em>VIP Members</em>",
|
||||
Icon: "fa fa-circle",
|
||||
},
|
||||
}
|
||||
c.JWT.Strict = true
|
||||
return c
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
type Claims struct {
|
||||
// Custom claims.
|
||||
IsAdmin bool `json:"op,omitempty"`
|
||||
VIP bool `json:"vip,omitempty"`
|
||||
Avatar string `json:"img,omitempty"`
|
||||
ProfileURL string `json:"url,omitempty"`
|
||||
Nick string `json:"nick,omitempty"`
|
||||
|
|
|
@ -107,6 +107,7 @@ type WhoList struct {
|
|||
|
||||
// JWT auth extra settings.
|
||||
Operator bool `json:"op"`
|
||||
VIP bool `json:"vip,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
ProfileURL string `json:"profileURL,omitempty"`
|
||||
Emoji string `json:"emoji,omitempty"`
|
||||
|
@ -122,4 +123,5 @@ const (
|
|||
VideoFlagIsTalking // broadcaster seems to be talking
|
||||
VideoFlagMutualRequired // video wants viewers to share their camera too
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (sub *Subscriber) SendJSON(v interface{}) error {
|
||||
data, err := json.Marshal(v)
|
||||
|
@ -400,9 +405,19 @@ func (s *Server) SendWhoList() {
|
|||
LoginAt: user.loginAt.Unix(),
|
||||
}
|
||||
|
||||
// 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
|
||||
// Hide video flags of other users (never for the current user).
|
||||
if user.Username != sub.Username {
|
||||
|
||||
// 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 {
|
||||
|
@ -412,6 +427,16 @@ func (s *Server) SendWhoList() {
|
|||
who.Nickname = user.JWTClaims.Nick
|
||||
who.Emoji = user.JWTClaims.Emoji
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -336,3 +336,8 @@ div.feed.popped-out {
|
|||
.has-text-gender-other {
|
||||
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,
|
||||
permitNSFW: PermitNSFW,
|
||||
webhookURLs: WebhookURLs,
|
||||
VIP: VIP,
|
||||
fontSizeClasses: [
|
||||
[ "x-2", "Very small 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
|
||||
mutual: false, // user wants viewers to share their own videos
|
||||
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.
|
||||
watching: {},
|
||||
|
@ -186,6 +188,7 @@ const app = Vue.createApp({
|
|||
IsTalking: 1 << 3,
|
||||
MutualRequired: 1 << 4,
|
||||
MutualOpen: 1 << 5,
|
||||
VipOnly: 1 << 6,
|
||||
},
|
||||
|
||||
// WebRTC sessions with other users.
|
||||
|
@ -386,6 +389,11 @@ const app = Vue.createApp({
|
|||
this.sendMe();
|
||||
}
|
||||
},
|
||||
"webcam.vipOnly": function() {
|
||||
if (this.webcam.active) {
|
||||
this.sendMe();
|
||||
}
|
||||
},
|
||||
|
||||
// Misc preference watches
|
||||
"prefs.joinMessages": function() {
|
||||
|
@ -495,6 +503,10 @@ const app = Vue.createApp({
|
|||
// Returns if the current user has operator rights
|
||||
return this.jwt.claims.op;
|
||||
},
|
||||
isVIP() {
|
||||
// Returns if the current user has VIP rights.
|
||||
return this.jwt.claims.vip;
|
||||
},
|
||||
myVideoFlag() {
|
||||
// Compute the current user's video status flags.
|
||||
let status = 0;
|
||||
|
@ -504,6 +516,7 @@ const app = Vue.createApp({
|
|||
if (this.webcam.nsfw) status |= this.VideoFlag.NSFW;
|
||||
if (this.webcam.mutual) status |= this.VideoFlag.MutualRequired;
|
||||
if (this.webcam.mutualOpen) status |= this.VideoFlag.MutualOpen;
|
||||
if (this.webcam.vipOnly && this.isVIP) status |= this.VideoFlag.VipOnly;
|
||||
return status;
|
||||
},
|
||||
sortedWhoList() {
|
||||
|
@ -596,6 +609,9 @@ const app = Vue.createApp({
|
|||
if (localStorage.videoAutoMute === "true") {
|
||||
this.webcam.autoMute = true;
|
||||
}
|
||||
if (localStorage.videoVipOnly === "true") {
|
||||
this.webcam.vipOnly = true;
|
||||
}
|
||||
|
||||
// Misc preferences
|
||||
if (localStorage.joinMessages != undefined) {
|
||||
|
@ -1613,6 +1629,7 @@ const app = Vue.createApp({
|
|||
localStorage.videoMutual = this.webcam.mutual;
|
||||
localStorage.videoMutualOpen = this.webcam.mutualOpen;
|
||||
localStorage.videoAutoMute = this.webcam.autoMute;
|
||||
localStorage.videoVipOnly = this.webcam.vipOnly;
|
||||
|
||||
// Auto-mute our camera? Two use cases:
|
||||
// 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)
|
||||
profileButtonClass(user) {
|
||||
// VIP background.
|
||||
let result = "";
|
||||
if (user.vip) {
|
||||
result = "has-background-vip ";
|
||||
}
|
||||
|
||||
let gender = (user.gender || "").toLowerCase();
|
||||
if (gender.indexOf("m") === 0) {
|
||||
return "has-text-gender-male";
|
||||
return result+"has-text-gender-male";
|
||||
} else if (gender.indexOf("f") === 0) {
|
||||
return "has-text-gender-female";
|
||||
return result+"has-text-gender-female";
|
||||
} else if (gender.length > 0) {
|
||||
return "has-text-gender-other";
|
||||
return result+"has-text-gender-other";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
|
|
@ -330,7 +330,7 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field mb-1">
|
||||
<label class="checkbox"
|
||||
:class="{'cursor-notallowed': !webcam.active}">
|
||||
<input type="checkbox"
|
||||
|
@ -340,6 +340,17 @@
|
|||
</label>
|
||||
</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">
|
||||
Webcam Devices
|
||||
<button type="button" class="button is-primary is-small is-outlined ml-2"
|
||||
|
@ -501,6 +512,16 @@
|
|||
</label>
|
||||
</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,
|
||||
and we are able to enumerate their device names, we can show them here before they
|
||||
go on this time.-->
|
||||
|
@ -1413,6 +1434,10 @@
|
|||
<sup class="fa fa-peace has-text-warning-dark is-size-7 ml-1"
|
||||
v-if="u.op"
|
||||
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 class="column is-narrow pl-0">
|
||||
<!-- Emoji icon -->
|
||||
|
@ -1427,7 +1452,7 @@
|
|||
class="button is-small px-2 py-1"
|
||||
:class="profileButtonClass(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>
|
||||
</button>
|
||||
|
||||
|
@ -1507,6 +1532,7 @@ const WebsiteURL = "{{.Config.WebsiteURL}}";
|
|||
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
||||
const TURN = {{.Config.TURN}};
|
||||
const WebhookURLs = {{.Config.WebhookURLs}};
|
||||
const VIP = {{.Config.VIP}};
|
||||
const UserJWTToken = {{.JWTTokenString}};
|
||||
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
|
||||
const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
||||
|
|
Loading…
Reference in New Issue
Block a user