Support for VIP users via JWT Auth

This commit is contained in:
Noah 2023-09-03 12:08:23 -07:00
parent f65f653430
commit 6e2aa517f5
7 changed files with 105 additions and 9 deletions

View File

@ -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

View File

@ -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"`

View File

@ -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
)

View File

@ -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,11 +405,21 @@ func (s *Server) SendWhoList() {
LoginAt: user.loginAt.Unix(),
}
// 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 {
who.Operator = user.JWTClaims.IsAdmin
who.Avatar = user.JWTClaims.Avatar
@ -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)
}

View File

@ -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%)
}

View File

@ -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 "";
},

View File

@ -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}};