Support for VIP users via JWT Auth

This commit is contained in:
Noah 2023-09-03 12:08:23 -07:00
父節點 f65f653430
當前提交 6e2aa517f5
共有 7 個檔案被更改,包括 105 行新增9 行删除

查看文件

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

查看文件

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