Compare commits
1 Commits
develop
...
gek/user-r
Author | SHA1 | Date |
---|---|---|
|
cff76707f0 | 2 years ago |
990 changed files with 28658 additions and 26512 deletions
@ -0,0 +1,23 @@ |
|||||||
|
<!-- this template is for changes relating to #2119. You might want to use the standard template. --> |
||||||
|
|
||||||
|
# Description |
||||||
|
|
||||||
|
<!-- do not remove --> |
||||||
|
This PR is for updating/adding a component following the atomic design pattern set out in #2119. |
||||||
|
|
||||||
|
<!-- mention the component you changed, and describe any design choices if necessary --> |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
Extra Info |
||||||
|
|
||||||
|
<!-- fill these in --> |
||||||
|
- Component name in [kanban board](https://collab.owncast.tv/kanban/#/2/kanban/edit/omLI2N+LcnP+elmdT7qW9GHD/): `________` |
||||||
|
|
||||||
|
Checklist: |
||||||
|
- [] The component follows the [design guide](../../web/components/_COMPONENT_HOW_TO.md). |
||||||
|
- [] Moved the component to the correct `atoms` / `molecules` / `organisms` / `templates` directory. |
||||||
|
- [] Added an explanation to this PR for any major changes you made. |
||||||
|
- [] Replaced any [`defaultProps`](https://www.reactjstutorials.com/react-basics/17/react-default-props) with default args. |
||||||
|
- [] Added a (short) JSDoc description to the component. |
||||||
|
- [] Removed the component's Storybook description text with if it's not needed. |
@ -1,4 +0,0 @@ |
|||||||
name: Javascript config |
|
||||||
|
|
||||||
paths-ignore: |
|
||||||
- static/web |
|
@ -0,0 +1,44 @@ |
|||||||
|
name: Build and bundle web app into Owncast |
||||||
|
on: |
||||||
|
push: |
||||||
|
branches: |
||||||
|
- develop |
||||||
|
paths: |
||||||
|
- 'web/**' |
||||||
|
- '!**.md' |
||||||
|
|
||||||
|
jobs: |
||||||
|
bundle: |
||||||
|
runs-on: ubuntu-latest |
||||||
|
if: github.repository == 'owncast/owncast' |
||||||
|
|
||||||
|
steps: |
||||||
|
- id: skip_check |
||||||
|
uses: fkirc/skip-duplicate-actions@v5 |
||||||
|
with: |
||||||
|
concurrent_skipping: 'same_content_newer' |
||||||
|
|
||||||
|
- name: Cache node modules |
||||||
|
uses: actions/cache@v3 |
||||||
|
env: |
||||||
|
cache-name: cache-node-modules-bundle-web-app |
||||||
|
with: |
||||||
|
path: ~/.npm |
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
||||||
|
restore-keys: | |
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}- |
||||||
|
${{ runner.os }}-build- |
||||||
|
${{ runner.os }}- |
||||||
|
|
||||||
|
- name: Bundle web app (next.js build) |
||||||
|
uses: actions/checkout@v3 |
||||||
|
- run: build/web/bundleWeb.sh |
||||||
|
|
||||||
|
- name: Commit changes |
||||||
|
uses: EndBug/add-and-commit@v9 |
||||||
|
with: |
||||||
|
pull: --rebase --autostash |
||||||
|
message: 'Bundle embedded web app' |
||||||
|
add: 'static/web' |
||||||
|
author_name: Owncast |
||||||
|
author_email: owncast@owncast.online |
@ -1,185 +0,0 @@ |
|||||||
name: Javascript |
|
||||||
|
|
||||||
# This action works with pull requests and pushes |
|
||||||
on: |
|
||||||
push: |
|
||||||
paths: |
|
||||||
- web/** |
|
||||||
- '!**.md' |
|
||||||
|
|
||||||
pull_request: |
|
||||||
paths: |
|
||||||
- web/** |
|
||||||
- '!**.md' |
|
||||||
|
|
||||||
jobs: |
|
||||||
formatting: |
|
||||||
name: Code formatting |
|
||||||
runs-on: ubuntu-latest |
|
||||||
defaults: |
|
||||||
run: |
|
||||||
working-directory: ./web |
|
||||||
|
|
||||||
steps: |
|
||||||
- id: skip_check |
|
||||||
uses: fkirc/skip-duplicate-actions@v5 |
|
||||||
with: |
|
||||||
concurrent_skipping: 'same_content_newer' |
|
||||||
cancel_others: 'true' |
|
||||||
skip_after_successful_duplicate: 'true' |
|
||||||
|
|
||||||
- name: Checkout |
|
||||||
uses: actions/checkout@v4 |
|
||||||
with: |
|
||||||
# Make sure the actual branch is checked out when running on pull requests |
|
||||||
ref: ${{ github.event.pull_request.head.ref }} |
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }} |
|
||||||
fetch-depth: 0 |
|
||||||
persist-credentials: true |
|
||||||
|
|
||||||
- name: Get changed files |
|
||||||
id: changed-files-yaml |
|
||||||
uses: tj-actions/changed-files@v41 |
|
||||||
with: |
|
||||||
path: 'web' |
|
||||||
files_ignore: | |
|
||||||
static/** |
|
||||||
web/next.config.js |
|
||||||
files_yaml: | |
|
||||||
src: |
|
||||||
- '**/*.{js,ts,tsx,jsx,css,md}' |
|
||||||
|
|
||||||
- name: Cache node modules |
|
||||||
uses: actions/cache@v3 |
|
||||||
env: |
|
||||||
cache-name: cache-node-modules-bundle-web-app |
|
||||||
with: |
|
||||||
path: ~/.npm |
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
|
||||||
restore-keys: | |
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}- |
|
||||||
${{ runner.os }}-build- |
|
||||||
${{ runner.os }}- |
|
||||||
|
|
||||||
- name: Install Dependencies |
|
||||||
run: npm install |
|
||||||
|
|
||||||
- name: Lint |
|
||||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' |
|
||||||
run: npx eslint --fix ${{ steps.changed-files-yaml.outputs.src_all_changed_files }} |
|
||||||
|
|
||||||
- name: Prettier |
|
||||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' |
|
||||||
run: npx prettier --write ${{ steps.changed-files-yaml.outputs.src_all_changed_files }} |
|
||||||
|
|
||||||
- name: Commit changes |
|
||||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' |
|
||||||
uses: EndBug/add-and-commit@v9 |
|
||||||
with: |
|
||||||
author_name: Owncast |
|
||||||
author_email: owncast@owncast.online |
|
||||||
message: 'Javascript formatting autofixes' |
|
||||||
add: ${{ steps.changed-files-yaml.outputs.src_all_changed_files }} |
|
||||||
pull: '--rebase --autostash' |
|
||||||
|
|
||||||
unused-code: |
|
||||||
name: Test for unused code |
|
||||||
runs-on: ubuntu-latest |
|
||||||
defaults: |
|
||||||
run: |
|
||||||
working-directory: ./web |
|
||||||
|
|
||||||
steps: |
|
||||||
- id: skip_check |
|
||||||
uses: fkirc/skip-duplicate-actions@v5 |
|
||||||
with: |
|
||||||
concurrent_skipping: 'same_content_newer' |
|
||||||
cancel_others: 'true' |
|
||||||
skip_after_successful_duplicate: 'true' |
|
||||||
|
|
||||||
- name: Checkout |
|
||||||
uses: actions/checkout@v4 |
|
||||||
with: |
|
||||||
# Make sure the actual branch is checked out when running on pull requests |
|
||||||
ref: ${{ github.event.pull_request.head.ref }} |
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }} |
|
||||||
fetch-depth: 0 |
|
||||||
|
|
||||||
- name: Cache node modules |
|
||||||
uses: actions/cache@v3 |
|
||||||
env: |
|
||||||
cache-name: cache-node-modules-bundle-web-app |
|
||||||
with: |
|
||||||
path: ~/.npm |
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
|
||||||
restore-keys: | |
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}- |
|
||||||
${{ runner.os }}-build- |
|
||||||
${{ runner.os }}- |
|
||||||
|
|
||||||
- name: Install Dependencies |
|
||||||
run: npm install |
|
||||||
|
|
||||||
- name: Check for unused JS code and dependencies |
|
||||||
run: npx knip --include dependencies,files,exports |
|
||||||
|
|
||||||
# After any formatting and linting is complete we can run the build |
|
||||||
# and bundle step. This both will verify that the build is successful as |
|
||||||
# well as commiting the updated static files into the repository for use. |
|
||||||
web-bundle: |
|
||||||
name: Build and bundle web project |
|
||||||
runs-on: ubuntu-latest |
|
||||||
if: github.repository == 'owncast/owncast' |
|
||||||
needs: [formatting, unused-code] |
|
||||||
steps: |
|
||||||
- id: skip_check |
|
||||||
uses: fkirc/skip-duplicate-actions@v5 |
|
||||||
with: |
|
||||||
concurrent_skipping: 'same_content_newer' |
|
||||||
cancel_others: 'true' |
|
||||||
skip_after_successful_duplicate: 'true' |
|
||||||
|
|
||||||
- name: Cache node modules |
|
||||||
uses: actions/cache@v3 |
|
||||||
env: |
|
||||||
cache-name: cache-node-modules-bundle-web-app |
|
||||||
with: |
|
||||||
path: ~/.npm |
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
|
||||||
restore-keys: | |
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}- |
|
||||||
${{ runner.os }}-build- |
|
||||||
${{ runner.os }}- |
|
||||||
|
|
||||||
- name: Checkout |
|
||||||
uses: actions/checkout@v4 |
|
||||||
with: |
|
||||||
# Make sure the actual branch is checked out when running on pull requests |
|
||||||
ref: ${{ github.event.pull_request.head.ref }} |
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }} |
|
||||||
fetch-depth: 0 |
|
||||||
|
|
||||||
- name: Bundle web app (next.js build) |
|
||||||
run: build/web/bundleWeb.sh |
|
||||||
|
|
||||||
- name: Rebase |
|
||||||
if: ${{ github.ref == 'refs/heads/develop' }} |
|
||||||
run: | |
|
||||||
git add static/web |
|
||||||
git pull --rebase --autostash |
|
||||||
|
|
||||||
# Only commit built web project files on develop. |
|
||||||
- name: Commit changes |
|
||||||
if: ${{ github.ref == 'refs/heads/develop' }} |
|
||||||
uses: EndBug/add-and-commit@v9 |
|
||||||
with: |
|
||||||
message: 'Bundle embedded web app' |
|
||||||
add: 'static/web' |
|
||||||
author_name: Owncast |
|
||||||
author_email: owncast@owncast.online |
|
||||||
|
|
||||||
- name: Push changes |
|
||||||
if: ${{ github.ref == 'refs/heads/develop' }} |
|
||||||
run: | |
|
||||||
git pull --rebase --autostash |
|
||||||
git push |
|
@ -0,0 +1,96 @@ |
|||||||
|
name: Lint |
||||||
|
|
||||||
|
# This action works with pull requests and pushes |
||||||
|
on: |
||||||
|
push: |
||||||
|
paths: |
||||||
|
- web/** |
||||||
|
pull_request_target: |
||||||
|
paths: |
||||||
|
- web/** |
||||||
|
|
||||||
|
jobs: |
||||||
|
prettier: |
||||||
|
name: Javascript prettier |
||||||
|
runs-on: ubuntu-latest |
||||||
|
defaults: |
||||||
|
run: |
||||||
|
working-directory: ./web |
||||||
|
|
||||||
|
if: ${{ github.actor != 'dependabot[bot]' }} |
||||||
|
steps: |
||||||
|
- id: skip_check |
||||||
|
uses: fkirc/skip-duplicate-actions@v5 |
||||||
|
with: |
||||||
|
concurrent_skipping: 'same_content_newer' |
||||||
|
|
||||||
|
- name: Checkout |
||||||
|
uses: actions/checkout@v3 |
||||||
|
with: |
||||||
|
# Make sure the actual branch is checked out when running on pull requests |
||||||
|
ref: ${{ github.event.pull_request.head.ref }} |
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }} |
||||||
|
fetch-depth: 0 |
||||||
|
|
||||||
|
- name: Prettify code |
||||||
|
uses: creyD/prettier_action@v4.3 |
||||||
|
with: |
||||||
|
# This part is also where you can pass other options, for example: |
||||||
|
prettier_options: --write **/*.{js,ts,jsx,tsx,css,md} |
||||||
|
only_changed: true |
||||||
|
env: |
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||||
|
|
||||||
|
linter: |
||||||
|
name: Javascript linter |
||||||
|
runs-on: ubuntu-latest |
||||||
|
defaults: |
||||||
|
run: |
||||||
|
working-directory: ./web |
||||||
|
|
||||||
|
steps: |
||||||
|
- id: skip_check |
||||||
|
uses: fkirc/skip-duplicate-actions@v5 |
||||||
|
with: |
||||||
|
concurrent_skipping: 'same_content_newer' |
||||||
|
|
||||||
|
- name: Checkout |
||||||
|
uses: actions/checkout@v3 |
||||||
|
with: |
||||||
|
# Make sure the actual branch is checked out when running on pull requests |
||||||
|
ref: ${{ github.event.pull_request.head.ref }} |
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }} |
||||||
|
fetch-depth: 0 |
||||||
|
|
||||||
|
- name: Install Dependencies |
||||||
|
run: npm install |
||||||
|
|
||||||
|
- name: Lint |
||||||
|
run: npm run lint |
||||||
|
|
||||||
|
unused-code: |
||||||
|
name: Test for unused code |
||||||
|
runs-on: ubuntu-latest |
||||||
|
defaults: |
||||||
|
run: |
||||||
|
working-directory: ./web |
||||||
|
|
||||||
|
steps: |
||||||
|
- id: skip_check |
||||||
|
uses: fkirc/skip-duplicate-actions@v5 |
||||||
|
with: |
||||||
|
concurrent_skipping: 'same_content_newer' |
||||||
|
|
||||||
|
- name: Checkout |
||||||
|
uses: actions/checkout@v3 |
||||||
|
with: |
||||||
|
# Make sure the actual branch is checked out when running on pull requests |
||||||
|
ref: ${{ github.event.pull_request.head.ref }} |
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }} |
||||||
|
fetch-depth: 0 |
||||||
|
|
||||||
|
- name: Install Dependencies |
||||||
|
run: npm install |
||||||
|
|
||||||
|
- name: Check for unused JS code and dependencies |
||||||
|
run: npx knip --include dependencies,files,exports |
@ -0,0 +1,50 @@ |
|||||||
|
name: Webapp Test Build |
||||||
|
|
||||||
|
# This action works with pull requests and pushes |
||||||
|
on: |
||||||
|
push: |
||||||
|
paths: |
||||||
|
- web/** |
||||||
|
pull_request: |
||||||
|
paths: |
||||||
|
- web/** |
||||||
|
|
||||||
|
jobs: |
||||||
|
build: |
||||||
|
runs-on: ubuntu-latest |
||||||
|
defaults: |
||||||
|
run: |
||||||
|
working-directory: ./web |
||||||
|
|
||||||
|
name: Build webapp |
||||||
|
steps: |
||||||
|
- id: skip_check |
||||||
|
uses: fkirc/skip-duplicate-actions@v5 |
||||||
|
with: |
||||||
|
concurrent_skipping: 'same_content_newer' |
||||||
|
|
||||||
|
- name: Checkout |
||||||
|
uses: actions/checkout@v3 |
||||||
|
with: |
||||||
|
# Make sure the actual branch is checked out when running on pull requests |
||||||
|
ref: ${{ github.event.pull_request.head.ref }} |
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }} |
||||||
|
fetch-depth: 0 |
||||||
|
|
||||||
|
- name: Cache node modules |
||||||
|
uses: actions/cache@v3 |
||||||
|
env: |
||||||
|
cache-name: cache-node-modules-bundle-web-app |
||||||
|
with: |
||||||
|
path: ~/.npm |
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
||||||
|
restore-keys: | |
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}- |
||||||
|
${{ runner.os }}-build- |
||||||
|
${{ runner.os }}- |
||||||
|
|
||||||
|
- name: Install Dependencies |
||||||
|
run: npm install |
||||||
|
|
||||||
|
- name: Build |
||||||
|
run: npm run build |
@ -1,9 +1,9 @@ |
|||||||
package events |
package events |
||||||
|
|
||||||
import "github.com/owncast/owncast/core/user" |
import "github.com/owncast/owncast/models" |
||||||
|
|
||||||
// ConnectedClientInfo represents the information about a connected client.
|
// ConnectedClientInfo represents the information about a connected client.
|
||||||
type ConnectedClientInfo struct { |
type ConnectedClientInfo struct { |
||||||
User *user.User `json:"user"` |
|
||||||
Event |
Event |
||||||
|
User *models.User `json:"user"` |
||||||
} |
} |
||||||
|
@ -1,17 +0,0 @@ |
|||||||
package events |
|
||||||
|
|
||||||
// UserPartEvent is the event fired when a user leaves chat.
|
|
||||||
type UserPartEvent struct { |
|
||||||
Event |
|
||||||
UserEvent |
|
||||||
} |
|
||||||
|
|
||||||
// GetBroadcastPayload will return the object to send to all chat users.
|
|
||||||
func (e *UserPartEvent) GetBroadcastPayload() EventPayload { |
|
||||||
return EventPayload{ |
|
||||||
"type": UserParted, |
|
||||||
"id": e.ID, |
|
||||||
"timestamp": e.Timestamp, |
|
||||||
"user": e.User, |
|
||||||
} |
|
||||||
} |
|
@ -1,311 +0,0 @@ |
|||||||
package user |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"database/sql" |
|
||||||
"strings" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/owncast/owncast/utils" |
|
||||||
"github.com/pkg/errors" |
|
||||||
log "github.com/sirupsen/logrus" |
|
||||||
"github.com/teris-io/shortid" |
|
||||||
) |
|
||||||
|
|
||||||
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
|
|
||||||
// This struct mostly matches the User struct so they can be used interchangeably.
|
|
||||||
type ExternalAPIUser struct { |
|
||||||
CreatedAt time.Time `json:"createdAt"` |
|
||||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` |
|
||||||
ID string `json:"id"` |
|
||||||
AccessToken string `json:"accessToken"` |
|
||||||
DisplayName string `json:"displayName"` |
|
||||||
Type string `json:"type,omitempty"` // Should be API
|
|
||||||
Scopes []string `json:"scopes"` |
|
||||||
DisplayColor int `json:"displayColor"` |
|
||||||
IsBot bool `json:"isBot"` |
|
||||||
} |
|
||||||
|
|
||||||
const ( |
|
||||||
// ScopeCanSendChatMessages will allow sending chat messages as itself.
|
|
||||||
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES" |
|
||||||
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
|
|
||||||
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES" |
|
||||||
// ScopeHasAdminAccess will allow performing administrative actions on the server.
|
|
||||||
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS" |
|
||||||
) |
|
||||||
|
|
||||||
// For a scope to be seen as "valid" it must live in this slice.
|
|
||||||
var validAccessTokenScopes = []string{ |
|
||||||
ScopeCanSendChatMessages, |
|
||||||
ScopeCanSendSystemMessages, |
|
||||||
ScopeHasAdminAccess, |
|
||||||
} |
|
||||||
|
|
||||||
// InsertExternalAPIUser will add a new API user to the database.
|
|
||||||
func InsertExternalAPIUser(token string, name string, color int, scopes []string) error { |
|
||||||
log.Traceln("Adding new API user") |
|
||||||
|
|
||||||
_datastore.DbLock.Lock() |
|
||||||
defer _datastore.DbLock.Unlock() |
|
||||||
|
|
||||||
scopesString := strings.Join(scopes, ",") |
|
||||||
id := shortid.MustGenerate() |
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?)") |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
defer stmt.Close() |
|
||||||
|
|
||||||
if _, err = stmt.Exec(id, name, color, scopesString, "API", name); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if err := addAccessTokenForUser(token, id); err != nil { |
|
||||||
return errors.Wrap(err, "unable to save access token for new external api user") |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// DeleteExternalAPIUser will delete a token from the database.
|
|
||||||
func DeleteExternalAPIUser(token string) error { |
|
||||||
log.Traceln("Deleting access token") |
|
||||||
|
|
||||||
_datastore.DbLock.Lock() |
|
||||||
defer _datastore.DbLock.Unlock() |
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
stmt, err := tx.Prepare("UPDATE users SET disabled_at = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)") |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
defer stmt.Close() |
|
||||||
|
|
||||||
result, err := stmt.Exec(token) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 { |
|
||||||
tx.Rollback() //nolint
|
|
||||||
return errors.New(token + " not found") |
|
||||||
} |
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// GetExternalAPIUserForAccessTokenAndScope will determine if a specific token has access to perform a scoped action.
|
|
||||||
func GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*ExternalAPIUser, error) { |
|
||||||
// This will split the scopes from comma separated to individual rows
|
|
||||||
// so we can efficiently find if a token supports a single scope.
|
|
||||||
// This is SQLite specific, so if we ever support other database
|
|
||||||
// backends we need to support other methods.
|
|
||||||
query := `SELECT |
|
||||||
id, |
|
||||||
scopes, |
|
||||||
display_name, |
|
||||||
display_color, |
|
||||||
created_at, |
|
||||||
last_used |
|
||||||
FROM |
|
||||||
user_access_tokens |
|
||||||
INNER JOIN ( |
|
||||||
WITH RECURSIVE split( |
|
||||||
id, |
|
||||||
scopes, |
|
||||||
display_name, |
|
||||||
display_color, |
|
||||||
created_at, |
|
||||||
last_used, |
|
||||||
disabled_at, |
|
||||||
scope, |
|
||||||
rest |
|
||||||
) AS ( |
|
||||||
SELECT |
|
||||||
id, |
|
||||||
scopes, |
|
||||||
display_name, |
|
||||||
display_color, |
|
||||||
created_at, |
|
||||||
last_used, |
|
||||||
disabled_at, |
|
||||||
'', |
|
||||||
scopes || ',' |
|
||||||
FROM |
|
||||||
users AS u |
|
||||||
UNION ALL |
|
||||||
SELECT |
|
||||||
id, |
|
||||||
scopes, |
|
||||||
display_name, |
|
||||||
display_color, |
|
||||||
created_at, |
|
||||||
last_used, |
|
||||||
disabled_at, |
|
||||||
substr(rest, 0, instr(rest, ',')), |
|
||||||
substr(rest, instr(rest, ',') + 1) |
|
||||||
FROM |
|
||||||
split |
|
||||||
WHERE |
|
||||||
rest <> '' |
|
||||||
) |
|
||||||
SELECT |
|
||||||
id, |
|
||||||
display_name, |
|
||||||
display_color, |
|
||||||
created_at, |
|
||||||
last_used, |
|
||||||
disabled_at, |
|
||||||
scopes, |
|
||||||
scope |
|
||||||
FROM |
|
||||||
split |
|
||||||
WHERE |
|
||||||
scope <> '' |
|
||||||
) ON user_access_tokens.user_id = id |
|
||||||
WHERE |
|
||||||
disabled_at IS NULL |
|
||||||
AND token = ? |
|
||||||
AND scope = ?;` |
|
||||||
|
|
||||||
row := _datastore.DB.QueryRow(query, token, scope) |
|
||||||
integration, err := makeExternalAPIUserFromRow(row) |
|
||||||
|
|
||||||
return integration, err |
|
||||||
} |
|
||||||
|
|
||||||
// GetIntegrationNameForAccessToken will return the integration name associated with a specific access token.
|
|
||||||
func GetIntegrationNameForAccessToken(token string) *string { |
|
||||||
name, err := _datastore.GetQueries().GetUserDisplayNameByToken(context.Background(), token) |
|
||||||
if err != nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
return &name |
|
||||||
} |
|
||||||
|
|
||||||
// GetExternalAPIUser will return all API users with access tokens.
|
|
||||||
func GetExternalAPIUser() ([]ExternalAPIUser, error) { //nolint
|
|
||||||
query := "SELECT id, token, display_name, display_color, scopes, created_at, last_used FROM users, user_access_tokens WHERE user_access_tokens.user_id = id AND type IS 'API' AND disabled_at IS NULL" |
|
||||||
|
|
||||||
rows, err := _datastore.DB.Query(query) |
|
||||||
if err != nil { |
|
||||||
return []ExternalAPIUser{}, err |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
integrations, err := makeExternalAPIUsersFromRows(rows) |
|
||||||
|
|
||||||
return integrations, err |
|
||||||
} |
|
||||||
|
|
||||||
// SetExternalAPIUserAccessTokenAsUsed will update the last used timestamp for a token.
|
|
||||||
func SetExternalAPIUserAccessTokenAsUsed(token string) error { |
|
||||||
tx, err := _datastore.DB.Begin() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)") |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
defer stmt.Close() |
|
||||||
|
|
||||||
if _, err := stmt.Exec(token); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func makeExternalAPIUserFromRow(row *sql.Row) (*ExternalAPIUser, error) { |
|
||||||
var id string |
|
||||||
var displayName string |
|
||||||
var displayColor int |
|
||||||
var scopes string |
|
||||||
var createdAt time.Time |
|
||||||
var lastUsedAt *time.Time |
|
||||||
|
|
||||||
err := row.Scan(&id, &scopes, &displayName, &displayColor, &createdAt, &lastUsedAt) |
|
||||||
if err != nil { |
|
||||||
log.Debugln("unable to convert row to api user", err) |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
integration := ExternalAPIUser{ |
|
||||||
ID: id, |
|
||||||
DisplayName: displayName, |
|
||||||
DisplayColor: displayColor, |
|
||||||
CreatedAt: createdAt, |
|
||||||
Scopes: strings.Split(scopes, ","), |
|
||||||
LastUsedAt: lastUsedAt, |
|
||||||
} |
|
||||||
|
|
||||||
return &integration, nil |
|
||||||
} |
|
||||||
|
|
||||||
func makeExternalAPIUsersFromRows(rows *sql.Rows) ([]ExternalAPIUser, error) { |
|
||||||
integrations := make([]ExternalAPIUser, 0) |
|
||||||
|
|
||||||
for rows.Next() { |
|
||||||
var id string |
|
||||||
var accessToken string |
|
||||||
var displayName string |
|
||||||
var displayColor int |
|
||||||
var scopes string |
|
||||||
var createdAt time.Time |
|
||||||
var lastUsedAt *time.Time |
|
||||||
|
|
||||||
err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &scopes, &createdAt, &lastUsedAt) |
|
||||||
if err != nil { |
|
||||||
log.Errorln(err) |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
integration := ExternalAPIUser{ |
|
||||||
ID: id, |
|
||||||
AccessToken: accessToken, |
|
||||||
DisplayName: displayName, |
|
||||||
DisplayColor: displayColor, |
|
||||||
CreatedAt: createdAt, |
|
||||||
Scopes: strings.Split(scopes, ","), |
|
||||||
LastUsedAt: lastUsedAt, |
|
||||||
IsBot: true, |
|
||||||
} |
|
||||||
integrations = append(integrations, integration) |
|
||||||
} |
|
||||||
|
|
||||||
return integrations, nil |
|
||||||
} |
|
||||||
|
|
||||||
// HasValidScopes will verify that all the scopes provided are valid.
|
|
||||||
func HasValidScopes(scopes []string) bool { |
|
||||||
for _, scope := range scopes { |
|
||||||
_, foundInSlice := utils.FindInSlice(validAccessTokenScopes, scope) |
|
||||||
if !foundInSlice { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
@ -1,473 +0,0 @@ |
|||||||
package user |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"database/sql" |
|
||||||
"fmt" |
|
||||||
"sort" |
|
||||||
"strings" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/owncast/owncast/config" |
|
||||||
"github.com/owncast/owncast/core/data" |
|
||||||
"github.com/owncast/owncast/db" |
|
||||||
"github.com/owncast/owncast/utils" |
|
||||||
"github.com/pkg/errors" |
|
||||||
"github.com/teris-io/shortid" |
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus" |
|
||||||
) |
|
||||||
|
|
||||||
var _datastore *data.Datastore |
|
||||||
|
|
||||||
const ( |
|
||||||
moderatorScopeKey = "MODERATOR" |
|
||||||
minSuggestedUsernamePoolLength = 10 |
|
||||||
) |
|
||||||
|
|
||||||
// User represents a single chat user.
|
|
||||||
type User struct { |
|
||||||
CreatedAt time.Time `json:"createdAt"` |
|
||||||
DisabledAt *time.Time `json:"disabledAt,omitempty"` |
|
||||||
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"` |
|
||||||
AuthenticatedAt *time.Time `json:"-"` |
|
||||||
ID string `json:"id"` |
|
||||||
DisplayName string `json:"displayName"` |
|
||||||
PreviousNames []string `json:"previousNames"` |
|
||||||
Scopes []string `json:"scopes,omitempty"` |
|
||||||
DisplayColor int `json:"displayColor"` |
|
||||||
IsBot bool `json:"isBot"` |
|
||||||
Authenticated bool `json:"authenticated"` |
|
||||||
} |
|
||||||
|
|
||||||
// IsEnabled will return if this single user is enabled.
|
|
||||||
func (u *User) IsEnabled() bool { |
|
||||||
return u.DisabledAt == nil |
|
||||||
} |
|
||||||
|
|
||||||
// IsModerator will return if the user has moderation privileges.
|
|
||||||
func (u *User) IsModerator() bool { |
|
||||||
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey) |
|
||||||
return hasModerationScope |
|
||||||
} |
|
||||||
|
|
||||||
// SetupUsers will perform the initial initialization of the user package.
|
|
||||||
func SetupUsers() { |
|
||||||
_datastore = data.GetDatastore() |
|
||||||
} |
|
||||||
|
|
||||||
func generateDisplayName() string { |
|
||||||
suggestedUsernamesList := data.GetSuggestedUsernamesList() |
|
||||||
|
|
||||||
if len(suggestedUsernamesList) >= minSuggestedUsernamePoolLength { |
|
||||||
index := utils.RandomIndex(len(suggestedUsernamesList)) |
|
||||||
return suggestedUsernamesList[index] |
|
||||||
} else { |
|
||||||
return utils.GeneratePhrase() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// CreateAnonymousUser will create a new anonymous user with the provided display name.
|
|
||||||
func CreateAnonymousUser(displayName string) (*User, string, error) { |
|
||||||
// Try to assign a name that was requested.
|
|
||||||
if displayName != "" { |
|
||||||
// If name isn't available then generate a random one.
|
|
||||||
if available, _ := IsDisplayNameAvailable(displayName); !available { |
|
||||||
displayName = generateDisplayName() |
|
||||||
} |
|
||||||
} else { |
|
||||||
displayName = generateDisplayName() |
|
||||||
} |
|
||||||
|
|
||||||
displayColor := utils.GenerateRandomDisplayColor(config.MaxUserColor) |
|
||||||
|
|
||||||
id := shortid.MustGenerate() |
|
||||||
user := &User{ |
|
||||||
ID: id, |
|
||||||
DisplayName: displayName, |
|
||||||
DisplayColor: displayColor, |
|
||||||
CreatedAt: time.Now(), |
|
||||||
} |
|
||||||
|
|
||||||
// Create new user.
|
|
||||||
if err := create(user); err != nil { |
|
||||||
return nil, "", err |
|
||||||
} |
|
||||||
|
|
||||||
// Assign it an access token.
|
|
||||||
accessToken, err := utils.GenerateAccessToken() |
|
||||||
if err != nil { |
|
||||||
log.Errorln("Unable to create access token for new user") |
|
||||||
return nil, "", err |
|
||||||
} |
|
||||||
if err := addAccessTokenForUser(accessToken, id); err != nil { |
|
||||||
return nil, "", errors.Wrap(err, "unable to save access token for new user") |
|
||||||
} |
|
||||||
|
|
||||||
return user, accessToken, nil |
|
||||||
} |
|
||||||
|
|
||||||
// IsDisplayNameAvailable will check if the proposed name is available for use.
|
|
||||||
func IsDisplayNameAvailable(displayName string) (bool, error) { |
|
||||||
if available, err := _datastore.GetQueries().IsDisplayNameAvailable(context.Background(), displayName); err != nil { |
|
||||||
return false, errors.Wrap(err, "unable to check if display name is available") |
|
||||||
} else if available != 0 { |
|
||||||
return false, nil |
|
||||||
} |
|
||||||
|
|
||||||
return true, nil |
|
||||||
} |
|
||||||
|
|
||||||
// ChangeUsername will change the user associated to userID from one display name to another.
|
|
||||||
func ChangeUsername(userID string, username string) error { |
|
||||||
_datastore.DbLock.Lock() |
|
||||||
defer _datastore.DbLock.Unlock() |
|
||||||
|
|
||||||
if err := _datastore.GetQueries().ChangeDisplayName(context.Background(), db.ChangeDisplayNameParams{ |
|
||||||
DisplayName: username, |
|
||||||
ID: userID, |
|
||||||
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true}, |
|
||||||
NamechangedAt: sql.NullTime{Time: time.Now(), Valid: true}, |
|
||||||
}); err != nil { |
|
||||||
return errors.Wrap(err, "unable to change display name") |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ChangeUserColor will change the user associated to userID from one display name to another.
|
|
||||||
func ChangeUserColor(userID string, color int) error { |
|
||||||
_datastore.DbLock.Lock() |
|
||||||
defer _datastore.DbLock.Unlock() |
|
||||||
|
|
||||||
if err := _datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{ |
|
||||||
DisplayColor: int32(color), |
|
||||||
ID: userID, |
|
||||||
}); err != nil { |
|
||||||
return errors.Wrap(err, "unable to change display color") |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func addAccessTokenForUser(accessToken, userID string) error { |
|
||||||
return _datastore.GetQueries().AddAccessTokenForUser(context.Background(), db.AddAccessTokenForUserParams{ |
|
||||||
Token: accessToken, |
|
||||||
UserID: userID, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func create(user *User) error { |
|
||||||
_datastore.DbLock.Lock() |
|
||||||
defer _datastore.DbLock.Unlock() |
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin() |
|
||||||
if err != nil { |
|
||||||
log.Debugln(err) |
|
||||||
} |
|
||||||
defer func() { |
|
||||||
_ = tx.Rollback() |
|
||||||
}() |
|
||||||
|
|
||||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?)") |
|
||||||
if err != nil { |
|
||||||
log.Debugln(err) |
|
||||||
} |
|
||||||
defer stmt.Close() |
|
||||||
|
|
||||||
_, err = stmt.Exec(user.ID, user.DisplayName, user.DisplayColor, user.DisplayName, user.CreatedAt) |
|
||||||
if err != nil { |
|
||||||
log.Errorln("error creating new user", err) |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return tx.Commit() |
|
||||||
} |
|
||||||
|
|
||||||
// SetEnabled will set the enabled status of a single user by ID.
|
|
||||||
func SetEnabled(userID string, enabled bool) error { |
|
||||||
_datastore.DbLock.Lock() |
|
||||||
defer _datastore.DbLock.Unlock() |
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
defer tx.Rollback() //nolint
|
|
||||||
|
|
||||||
var stmt *sql.Stmt |
|
||||||
if !enabled { |
|
||||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?") |
|
||||||
} else { |
|
||||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=null WHERE id IS ?") |
|
||||||
} |
|
||||||
|
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
defer stmt.Close() |
|
||||||
|
|
||||||
if _, err := stmt.Exec(userID); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return tx.Commit() |
|
||||||
} |
|
||||||
|
|
||||||
// GetUserByToken will return a user by an access token.
|
|
||||||
func GetUserByToken(token string) *User { |
|
||||||
u, err := _datastore.GetQueries().GetUserByAccessToken(context.Background(), token) |
|
||||||
if err != nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
var scopes []string |
|
||||||
if u.Scopes.Valid { |
|
||||||
scopes = strings.Split(u.Scopes.String, ",") |
|
||||||
} |
|
||||||
|
|
||||||
var disabledAt *time.Time |
|
||||||
if u.DisabledAt.Valid { |
|
||||||
disabledAt = &u.DisabledAt.Time |
|
||||||
} |
|
||||||
|
|
||||||
var authenticatedAt *time.Time |
|
||||||
if u.AuthenticatedAt.Valid { |
|
||||||
authenticatedAt = &u.AuthenticatedAt.Time |
|
||||||
} |
|
||||||
|
|
||||||
return &User{ |
|
||||||
ID: u.ID, |
|
||||||
DisplayName: u.DisplayName, |
|
||||||
DisplayColor: int(u.DisplayColor), |
|
||||||
CreatedAt: u.CreatedAt.Time, |
|
||||||
DisabledAt: disabledAt, |
|
||||||
PreviousNames: strings.Split(u.PreviousNames.String, ","), |
|
||||||
NameChangedAt: &u.NamechangedAt.Time, |
|
||||||
AuthenticatedAt: authenticatedAt, |
|
||||||
Authenticated: authenticatedAt != nil, |
|
||||||
Scopes: scopes, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// SetAccessTokenToOwner will reassign an access token to be owned by a
|
|
||||||
// different user. Used for logging in with external auth.
|
|
||||||
func SetAccessTokenToOwner(token, userID string) error { |
|
||||||
return _datastore.GetQueries().SetAccessTokenToOwner(context.Background(), db.SetAccessTokenToOwnerParams{ |
|
||||||
UserID: userID, |
|
||||||
Token: token, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// SetUserAsAuthenticated will mark that a user has been authenticated
|
|
||||||
// in some way.
|
|
||||||
func SetUserAsAuthenticated(userID string) error { |
|
||||||
return errors.Wrap(_datastore.GetQueries().SetUserAsAuthenticated(context.Background(), userID), "unable to set user as authenticated") |
|
||||||
} |
|
||||||
|
|
||||||
// SetModerator will add or remove moderator status for a single user by ID.
|
|
||||||
func SetModerator(userID string, isModerator bool) error { |
|
||||||
if isModerator { |
|
||||||
return addScopeToUser(userID, moderatorScopeKey) |
|
||||||
} |
|
||||||
|
|
||||||
return removeScopeFromUser(userID, moderatorScopeKey) |
|
||||||
} |
|
||||||
|
|
||||||
func addScopeToUser(userID string, scope string) error { |
|
||||||
u := GetUserByID(userID) |
|
||||||
if u == nil { |
|
||||||
return errors.New("user not found when modifying scope") |
|
||||||
} |
|
||||||
|
|
||||||
scopesString := u.Scopes |
|
||||||
scopes := utils.StringSliceToMap(scopesString) |
|
||||||
scopes[scope] = true |
|
||||||
|
|
||||||
scopesSlice := utils.StringMapKeys(scopes) |
|
||||||
|
|
||||||
return setScopesOnUser(userID, scopesSlice) |
|
||||||
} |
|
||||||
|
|
||||||
func removeScopeFromUser(userID string, scope string) error { |
|
||||||
u := GetUserByID(userID) |
|
||||||
scopesString := u.Scopes |
|
||||||
scopes := utils.StringSliceToMap(scopesString) |
|
||||||
delete(scopes, scope) |
|
||||||
|
|
||||||
scopesSlice := utils.StringMapKeys(scopes) |
|
||||||
|
|
||||||
return setScopesOnUser(userID, scopesSlice) |
|
||||||
} |
|
||||||
|
|
||||||
func setScopesOnUser(userID string, scopes []string) error { |
|
||||||
_datastore.DbLock.Lock() |
|
||||||
defer _datastore.DbLock.Unlock() |
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
defer tx.Rollback() //nolint
|
|
||||||
|
|
||||||
scopesSliceString := strings.TrimSpace(strings.Join(scopes, ",")) |
|
||||||
stmt, err := tx.Prepare("UPDATE users SET scopes=? WHERE id IS ?") |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
defer stmt.Close() |
|
||||||
|
|
||||||
var val *string |
|
||||||
if scopesSliceString == "" { |
|
||||||
val = nil |
|
||||||
} else { |
|
||||||
val = &scopesSliceString |
|
||||||
} |
|
||||||
|
|
||||||
if _, err := stmt.Exec(val, userID); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return tx.Commit() |
|
||||||
} |
|
||||||
|
|
||||||
// GetUserByID will return a user by a user ID.
|
|
||||||
func GetUserByID(id string) *User { |
|
||||||
_datastore.DbLock.Lock() |
|
||||||
defer _datastore.DbLock.Unlock() |
|
||||||
|
|
||||||
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM users WHERE id = ?" |
|
||||||
row := _datastore.DB.QueryRow(query, id) |
|
||||||
if row == nil { |
|
||||||
log.Errorln(row) |
|
||||||
return nil |
|
||||||
} |
|
||||||
return getUserFromRow(row) |
|
||||||
} |
|
||||||
|
|
||||||
// GetDisabledUsers will return back all the currently disabled users that are not API users.
|
|
||||||
func GetDisabledUsers() []*User { |
|
||||||
query := "SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'" |
|
||||||
|
|
||||||
rows, err := _datastore.DB.Query(query) |
|
||||||
if err != nil { |
|
||||||
log.Errorln(err) |
|
||||||
return nil |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
users := getUsersFromRows(rows) |
|
||||||
|
|
||||||
sort.Slice(users, func(i, j int) bool { |
|
||||||
return users[i].DisabledAt.Before(*users[j].DisabledAt) |
|
||||||
}) |
|
||||||
|
|
||||||
return users |
|
||||||
} |
|
||||||
|
|
||||||
// GetModeratorUsers will return a list of users with moderator access.
|
|
||||||
func GetModeratorUsers() []*User { |
|
||||||
query := `SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM ( |
|
||||||
WITH RECURSIVE split(id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope, rest) AS ( |
|
||||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, '', scopes || ',' FROM users |
|
||||||
UNION ALL |
|
||||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, |
|
||||||
substr(rest, 0, instr(rest, ',')), |
|
||||||
substr(rest, instr(rest, ',')+1) |
|
||||||
FROM split |
|
||||||
WHERE rest <> '') |
|
||||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope |
|
||||||
FROM split |
|
||||||
WHERE scope <> '' |
|
||||||
ORDER BY created_at |
|
||||||
) AS token WHERE token.scope = ?` |
|
||||||
|
|
||||||
rows, err := _datastore.DB.Query(query, moderatorScopeKey) |
|
||||||
if err != nil { |
|
||||||
log.Errorln(err) |
|
||||||
return nil |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
users := getUsersFromRows(rows) |
|
||||||
|
|
||||||
return users |
|
||||||
} |
|
||||||
|
|
||||||
func getUsersFromRows(rows *sql.Rows) []*User { |
|
||||||
users := make([]*User, 0) |
|
||||||
|
|
||||||
for rows.Next() { |
|
||||||
var id string |
|
||||||
var displayName string |
|
||||||
var displayColor int |
|
||||||
var createdAt time.Time |
|
||||||
var disabledAt *time.Time |
|
||||||
var previousUsernames string |
|
||||||
var userNameChangedAt *time.Time |
|
||||||
var scopesString *string |
|
||||||
|
|
||||||
if err := rows.Scan(&id, &displayName, &scopesString, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil { |
|
||||||
log.Errorln("error creating collection of users from results", err) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
var scopes []string |
|
||||||
if scopesString != nil { |
|
||||||
scopes = strings.Split(*scopesString, ",") |
|
||||||
} |
|
||||||
|
|
||||||
user := &User{ |
|
||||||
ID: id, |
|
||||||
DisplayName: displayName, |
|
||||||
DisplayColor: displayColor, |
|
||||||
CreatedAt: createdAt, |
|
||||||
DisabledAt: disabledAt, |
|
||||||
PreviousNames: strings.Split(previousUsernames, ","), |
|
||||||
NameChangedAt: userNameChangedAt, |
|
||||||
Scopes: scopes, |
|
||||||
} |
|
||||||
users = append(users, user) |
|
||||||
} |
|
||||||
|
|
||||||
sort.Slice(users, func(i, j int) bool { |
|
||||||
return users[i].CreatedAt.Before(users[j].CreatedAt) |
|
||||||
}) |
|
||||||
|
|
||||||
return users |
|
||||||
} |
|
||||||
|
|
||||||
func getUserFromRow(row *sql.Row) *User { |
|
||||||
var id string |
|
||||||
var displayName string |
|
||||||
var displayColor int |
|
||||||
var createdAt time.Time |
|
||||||
var disabledAt *time.Time |
|
||||||
var previousUsernames string |
|
||||||
var userNameChangedAt *time.Time |
|
||||||
var scopesString *string |
|
||||||
|
|
||||||
if err := row.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt, &scopesString); err != nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
var scopes []string |
|
||||||
if scopesString != nil { |
|
||||||
scopes = strings.Split(*scopesString, ",") |
|
||||||
} |
|
||||||
|
|
||||||
return &User{ |
|
||||||
ID: id, |
|
||||||
DisplayName: displayName, |
|
||||||
DisplayColor: displayColor, |
|
||||||
CreatedAt: createdAt, |
|
||||||
DisabledAt: disabledAt, |
|
||||||
PreviousNames: strings.Split(previousUsernames, ","), |
|
||||||
NameChangedAt: userNameChangedAt, |
|
||||||
Scopes: scopes, |
|
||||||
} |
|
||||||
} |
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,19 @@ |
|||||||
|
package models |
||||||
|
|
||||||
|
import ( |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
|
||||||
|
// This struct mostly matches the User struct so they can be used interchangeably.
|
||||||
|
type ExternalAPIUser struct { |
||||||
|
CreatedAt time.Time `json:"createdAt"` |
||||||
|
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` |
||||||
|
ID string `json:"id"` |
||||||
|
AccessToken string `json:"accessToken"` |
||||||
|
DisplayName string `json:"displayName"` |
||||||
|
Type string `json:"type,omitempty"` // Should be API
|
||||||
|
Scopes []string `json:"scopes"` |
||||||
|
DisplayColor int `json:"displayColor"` |
||||||
|
IsBot bool `json:"isBot"` |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
package models |
||||||
|
|
||||||
|
import ( |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/owncast/owncast/utils" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
moderatorScopeKey = "MODERATOR" |
||||||
|
) |
||||||
|
|
||||||
|
type User struct { |
||||||
|
CreatedAt time.Time `json:"createdAt"` |
||||||
|
DisabledAt *time.Time `json:"disabledAt,omitempty"` |
||||||
|
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"` |
||||||
|
AuthenticatedAt *time.Time `json:"-"` |
||||||
|
ID string `json:"id"` |
||||||
|
DisplayName string `json:"displayName"` |
||||||
|
PreviousNames []string `json:"previousNames"` |
||||||
|
Scopes []string `json:"scopes,omitempty"` |
||||||
|
DisplayColor int `json:"displayColor"` |
||||||
|
IsBot bool `json:"isBot"` |
||||||
|
Authenticated bool `json:"authenticated"` |
||||||
|
} |
||||||
|
|
||||||
|
// IsEnabled will return if this single user is enabled.
|
||||||
|
func (u *User) IsEnabled() bool { |
||||||
|
return u.DisabledAt == nil |
||||||
|
} |
||||||
|
|
||||||
|
// IsModerator will return if the user has moderation privileges.
|
||||||
|
func (u *User) IsModerator() bool { |
||||||
|
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey) |
||||||
|
return hasModerationScope |
||||||
|
} |
@ -1,83 +0,0 @@ |
|||||||
<!doctype html> |
|
||||||
|
|
||||||
<html lang="en"> |
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
|
|
||||||
<title>{{.Name}}</title> |
|
||||||
<meta name="description" content="{{.Summary}}"> |
|
||||||
|
|
||||||
<meta property="og:title" content="{{.Name}}"> |
|
||||||
<meta property="og:site_name" content="{{.Name}}"> |
|
||||||
<meta property="og:url" content="{{.RequestedURL}}"> |
|
||||||
<meta property="og:description" content="{{.Summary}}"> |
|
||||||
<meta property="og:type" content="video.other"> |
|
||||||
<meta property="video:tag" content="{{.TagsString}}"> |
|
||||||
|
|
||||||
<meta property="og:image" content="{{.Thumbnail}}"> |
|
||||||
<meta property="og:image:url" content="{{.Thumbnail}}"> |
|
||||||
<meta property="og:image:alt" content="{{.Image}}"> |
|
||||||
|
|
||||||
<meta property="og:video" content='{{.RequestedURL}}embed/video' /> |
|
||||||
<meta property="og:video:secure_url" content='{{.RequestedURL}}embed/video' /> |
|
||||||
<meta property="og:video:height" content="315" /> |
|
||||||
<meta property="og:video:width" content="560" /> |
|
||||||
<meta property="og:video:type" content="text/html" /> |
|
||||||
<meta property="og:video:actor" content="{{.Name}}" /> |
|
||||||
|
|
||||||
<meta property="twitter:title" content="{{.Name}}"> |
|
||||||
<meta property="twitter:url" content="{{.RequestedURL}}"> |
|
||||||
<meta property="twitter:description" content="{{.Summary}}"> |
|
||||||
<meta property="twitter:image" content="{{.Image}}"> |
|
||||||
<meta property="twitter:card" content="player" /> |
|
||||||
<meta property="twitter:player" content='{{.RequestedURL}}embed/video' /> |
|
||||||
<meta property="twitter:player:width" content="560" /> |
|
||||||
<meta property="twitter:player:height" content="315" /> |
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="57x57" href="/img/favicon/apple-icon-57x57.png"> |
|
||||||
<link rel="apple-touch-icon" sizes="60x60" href="/img/favicon/apple-icon-60x60.png"> |
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="/img/favicon/apple-icon-72x72.png"> |
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="/img/favicon/apple-icon-76x76.png"> |
|
||||||
<link rel="apple-touch-icon" sizes="114x114" href="/img/favicon/apple-icon-114x114.png"> |
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/img/favicon/apple-icon-120x120.png"> |
|
||||||
<link rel="apple-touch-icon" sizes="144x144" href="/img/favicon/apple-icon-144x144.png"> |
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/img/favicon/apple-icon-152x152.png"> |
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-icon-180x180.png"> |
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="/img/favicon/android-icon-192x192.png"> |
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png"> |
|
||||||
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon/favicon-96x96.png"> |
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon/favicon-16x16.png"> |
|
||||||
<link rel="manifest" href="/manifest.json"> |
|
||||||
|
|
||||||
<link rel="authorization_endpoint" href="/api/auth/provider/indieauth"> |
|
||||||
|
|
||||||
<meta name="msapplication-TileColor" content="#ffffff"> |
|
||||||
<meta name="msapplication-TileImage" content="/img/favicon/ms-icon-144x144.png"> |
|
||||||
<meta name="theme-color" content="#ffffff"> |
|
||||||
|
|
||||||
</head> |
|
||||||
|
|
||||||
<body> |
|
||||||
<h1>{{.Name}}</h1> |
|
||||||
|
|
||||||
<center> |
|
||||||
<img src="{{.Thumbnail}}" width=10% /> |
|
||||||
</center> |
|
||||||
|
|
||||||
<h3>{{.Summary}}</h3> |
|
||||||
|
|
||||||
{{range .Tags}} |
|
||||||
<li>{{.}}</li> |
|
||||||
{{end}} |
|
||||||
|
|
||||||
<br/> |
|
||||||
|
|
||||||
<h3>Links for {{.Name}}:</h3> |
|
||||||
|
|
||||||
{{range .SocialHandles}} |
|
||||||
<li><a href="{{.URL}}">{{.Platform}}</a></li> |
|
||||||
{{end}} |
|
||||||
|
|
||||||
|
|
||||||
</body> |
|
||||||
</html> |
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@ |
|||||||
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1008],{98696:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M816 768h-24V428c0-141.1-104.3-257.7-240-277.1V112c0-22.1-17.9-40-40-40s-40 17.9-40 40v38.9c-135.7 19.4-240 136-240 277.1v340h-24c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h216c0 61.8 50.2 112 112 112s112-50.2 112-112h216c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM512 888c-26.5 0-48-21.5-48-48h96c0 26.5-21.5 48-48 48zM304 768V428c0-55.6 21.6-107.8 60.9-147.1S456.4 220 512 220c55.6 0 107.8 21.6 147.1 60.9S720 372.4 720 428v340H304z"}}]},name:"bell",theme:"outlined"}},11008:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(25594))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},25594:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),f=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==n(e)&&"function"!=typeof e)return{default:e};var r=o(t);if(r&&r.has(e))return r.get(e);var a={},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&Object.prototype.hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(a,f,l):a[f]=e[f]}return a.default=e,r&&r.set(e,a),a}(r(67294)),l=a(r(98696)),c=a(r(92074));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(o=function(e){return e?r:t})(e)}var i=function(e,t){return f.createElement(c.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))};i.displayName="BellOutlined";var d=f.forwardRef(i);t.default=d}}]); |
@ -1 +0,0 @@ |
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1008],{98696:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M816 768h-24V428c0-141.1-104.3-257.7-240-277.1V112c0-22.1-17.9-40-40-40s-40 17.9-40 40v38.9c-135.7 19.4-240 136-240 277.1v340h-24c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h216c0 61.8 50.2 112 112 112s112-50.2 112-112h216c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM512 888c-26.5 0-48-21.5-48-48h96c0 26.5-21.5 48-48 48zM304 768V428c0-55.6 21.6-107.8 60.9-147.1S456.4 220 512 220c55.6 0 107.8 21.6 147.1 60.9S720 372.4 720 428v340H304z"}}]},name:"bell",theme:"outlined"}},11008:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=_interopRequireDefault(r(25594));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}t.default=u,e.exports=u},25594:function(e,t,r){var u=r(64836),a=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=u(r(42122)),l=_interopRequireWildcard(r(67294)),i=u(r(98696)),c=u(r(92074));function _getRequireWildcardCache(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(_getRequireWildcardCache=function(e){return e?r:t})(e)}function _interopRequireWildcard(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==a(e)&&"function"!=typeof e)return{default:e};var r=_getRequireWildcardCache(t);if(r&&r.has(e))return r.get(e);var u={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&Object.prototype.hasOwnProperty.call(e,l)){var i=n?Object.getOwnPropertyDescriptor(e,l):null;i&&(i.get||i.set)?Object.defineProperty(u,l,i):u[l]=e[l]}return u.default=e,r&&r.set(e,u),u}var BellOutlined=function(e,t){return l.createElement(c.default,(0,n.default)((0,n.default)({},e),{},{ref:t,icon:i.default}))};BellOutlined.displayName="BellOutlined";var f=l.forwardRef(BellOutlined);t.default=f}}]); |
|
@ -1 +0,0 @@ |
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1010],{90034:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:function(e,t){return{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M180 292h80v440h-80zm369 180h-74a3 3 0 00-3 3v74a3 3 0 003 3h74a3 3 0 003-3v-74a3 3 0 00-3-3zm215-108h80v296h-80z",fill:t}},{tag:"path",attrs:{d:"M904 296h-66v-96c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v96h-66c-4.4 0-8 3.6-8 8v416c0 4.4 3.6 8 8 8h66v96c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8v-96h66c4.4 0 8-3.6 8-8V304c0-4.4-3.6-8-8-8zm-60 364h-80V364h80v296zM612 404h-66V232c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v172h-66c-4.4 0-8 3.6-8 8v200c0 4.4 3.6 8 8 8h66v172c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8V620h66c4.4 0 8-3.6 8-8V412c0-4.4-3.6-8-8-8zm-60 145a3 3 0 01-3 3h-74a3 3 0 01-3-3v-74a3 3 0 013-3h74a3 3 0 013 3v74zM320 224h-66v-56c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v56h-66c-4.4 0-8 3.6-8 8v560c0 4.4 3.6 8 8 8h66v56c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8v-56h66c4.4 0 8-3.6 8-8V232c0-4.4-3.6-8-8-8zm-60 508h-80V292h80v440z",fill:e}}]}},name:"sliders",theme:"twotone"}},71010:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=_interopRequireDefault(r(54626));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}t.default=a,e.exports=a},54626:function(e,t,r){var a=r(64836),c=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=a(r(42122)),u=_interopRequireWildcard(r(67294)),i=a(r(90034)),o=a(r(92074));function _getRequireWildcardCache(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(_getRequireWildcardCache=function(e){return e?r:t})(e)}function _interopRequireWildcard(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==c(e)&&"function"!=typeof e)return{default:e};var r=_getRequireWildcardCache(t);if(r&&r.has(e))return r.get(e);var a={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var u in e)if("default"!==u&&Object.prototype.hasOwnProperty.call(e,u)){var i=n?Object.getOwnPropertyDescriptor(e,u):null;i&&(i.get||i.set)?Object.defineProperty(a,u,i):a[u]=e[u]}return a.default=e,r&&r.set(e,a),a}var SlidersTwoTone=function(e,t){return u.createElement(o.default,(0,n.default)((0,n.default)({},e),{},{ref:t,icon:i.default}))};SlidersTwoTone.displayName="SlidersTwoTone";var l=u.forwardRef(SlidersTwoTone);t.default=l}}]); |
|
@ -0,0 +1 @@ |
|||||||
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1010],{90034:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:function(e,t){return{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M180 292h80v440h-80zm369 180h-74a3 3 0 00-3 3v74a3 3 0 003 3h74a3 3 0 003-3v-74a3 3 0 00-3-3zm215-108h80v296h-80z",fill:t}},{tag:"path",attrs:{d:"M904 296h-66v-96c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v96h-66c-4.4 0-8 3.6-8 8v416c0 4.4 3.6 8 8 8h66v96c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8v-96h66c4.4 0 8-3.6 8-8V304c0-4.4-3.6-8-8-8zm-60 364h-80V364h80v296zM612 404h-66V232c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v172h-66c-4.4 0-8 3.6-8 8v200c0 4.4 3.6 8 8 8h66v172c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8V620h66c4.4 0 8-3.6 8-8V412c0-4.4-3.6-8-8-8zm-60 145a3 3 0 01-3 3h-74a3 3 0 01-3-3v-74a3 3 0 013-3h74a3 3 0 013 3v74zM320 224h-66v-56c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v56h-66c-4.4 0-8 3.6-8 8v560c0 4.4 3.6 8 8 8h66v56c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8v-56h66c4.4 0 8-3.6 8-8V232c0-4.4-3.6-8-8-8zm-60 508h-80V292h80v440z",fill:e}}]}},name:"sliders",theme:"twotone"}},71010:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,c=(a=r(54626))&&a.__esModule?a:{default:a};t.default=c,e.exports=c},54626:function(e,t,r){var a=r(64836),c=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=a(r(42122)),u=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==c(e)&&"function"!=typeof e)return{default:e};var r=l(t);if(r&&r.has(e))return r.get(e);var a={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var u in e)if("default"!==u&&Object.prototype.hasOwnProperty.call(e,u)){var f=n?Object.getOwnPropertyDescriptor(e,u):null;f&&(f.get||f.set)?Object.defineProperty(a,u,f):a[u]=e[u]}return a.default=e,r&&r.set(e,a),a}(r(67294)),f=a(r(90034)),o=a(r(92074));function l(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(l=function(e){return e?r:t})(e)}var h=function(e,t){return u.createElement(o.default,(0,n.default)((0,n.default)({},e),{},{ref:t,icon:f.default}))};h.displayName="SlidersTwoTone";var v=u.forwardRef(h);t.default=v}}]); |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue