mirror of
https://github.com/balzack/databag.git
synced 2025-05-05 07:55:15 +00:00
adding access token options
This commit is contained in:
parent
1861f1acb2
commit
961216ac29
@ -51,11 +51,40 @@ const theme = createTheme({
|
|||||||
'#777777',
|
'#777777',
|
||||||
'#666666',
|
'#666666',
|
||||||
],
|
],
|
||||||
|
'dark-text': [
|
||||||
|
'#ffffff',
|
||||||
|
'#eeeeee',
|
||||||
|
'#dddddd',
|
||||||
|
'#cccccc',
|
||||||
|
'#bbbbbb',
|
||||||
|
'#aaaaaa',
|
||||||
|
'#999999',
|
||||||
|
'#888888',
|
||||||
|
'#777777',
|
||||||
|
'#666666',
|
||||||
|
],
|
||||||
|
'light-text': [
|
||||||
|
'#000000',
|
||||||
|
'#111111',
|
||||||
|
'#222222',
|
||||||
|
'#333333',
|
||||||
|
'#444444',
|
||||||
|
'#555555',
|
||||||
|
'#666666',
|
||||||
|
'#777777',
|
||||||
|
'#888888',
|
||||||
|
'#999999',
|
||||||
|
],
|
||||||
surface: virtualColor({
|
surface: virtualColor({
|
||||||
name: 'surface',
|
name: 'surface',
|
||||||
dark: 'dark-surface',
|
dark: 'dark-surface',
|
||||||
light: 'light-surface',
|
light: 'light-surface',
|
||||||
}),
|
}),
|
||||||
|
text: virtualColor({
|
||||||
|
name: 'text',
|
||||||
|
dark: 'dark-text',
|
||||||
|
light: 'light-text',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
@ -7,14 +7,19 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
Image,
|
Image,
|
||||||
Button,
|
Button,
|
||||||
|
Modal,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import login from '../images/login.png'
|
import login from '../images/login.png'
|
||||||
import { IconLock, IconUser, IconSettings } from '@tabler/icons-react'
|
import { IconLock, IconUser, IconSettings, IconServer, IconKey } from '@tabler/icons-react'
|
||||||
|
|
||||||
export function Access() {
|
export function Access() {
|
||||||
const { state, actions } = useAccess()
|
const { state, actions } = useAccess()
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
|
console.log("AVAILABLE: ", state.availableSet);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.split}>
|
<div className={classes.split}>
|
||||||
@ -48,6 +53,11 @@ export function Access() {
|
|||||||
<>
|
<>
|
||||||
<Title order={3}>{state.strings.login}</Title>
|
<Title order={3}>{state.strings.login}</Title>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
<Button
|
||||||
|
size="compact-sm"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={open}
|
||||||
|
>{ state.hostname }</Button>
|
||||||
<TextInput
|
<TextInput
|
||||||
className={classes.input}
|
className={classes.input}
|
||||||
size="md"
|
size="md"
|
||||||
@ -72,17 +82,77 @@ export function Access() {
|
|||||||
{state.strings.login}
|
{state.strings.login}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
size="compact-sm"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => actions.setMode('create')}
|
onClick={() => actions.setMode('create')}
|
||||||
>
|
>
|
||||||
{state.strings.createAccount}
|
{state.strings.createAccount}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="compact-sm"
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => actions.setMode('access')}
|
||||||
|
>
|
||||||
|
{state.strings.forgotPassword}
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{state.mode === 'access' && (
|
||||||
|
<>
|
||||||
|
<Title order={3}>{state.strings.accessAccount}</Title>
|
||||||
|
<Space h="md" />
|
||||||
|
<Button
|
||||||
|
size="compact-sm"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={open}
|
||||||
|
>{ state.hostname }</Button>
|
||||||
|
<TextInput
|
||||||
|
className={classes.input}
|
||||||
|
size="md"
|
||||||
|
leftSectionPointerEvents="none"
|
||||||
|
leftSection={<IconKey />}
|
||||||
|
placeholder={state.strings.accessCode}
|
||||||
|
onChange={(event) =>
|
||||||
|
actions.setToken(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Space h="md" />
|
||||||
|
<Button variant="filled" className={classes.submit}>
|
||||||
|
{state.strings.login}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="compact-sm"
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => actions.setMode('login')}
|
||||||
|
>
|
||||||
|
{state.strings.accountLogin}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
{state.mode === 'create' && (
|
{state.mode === 'create' && (
|
||||||
<>
|
<>
|
||||||
<Title order={3}>{state.strings.createAccount}</Title>
|
<Title order={3}>{state.strings.createAccount}</Title>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
<Button
|
||||||
|
size="compact-sm"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={open}
|
||||||
|
>{ state.hostname }</Button>
|
||||||
|
{ (state.available === 0 || !state.availableSet) && (
|
||||||
|
<TextInput
|
||||||
|
className={classes.input}
|
||||||
|
size="md"
|
||||||
|
disabled={!state.availableSet}
|
||||||
|
leftSectionPointerEvents="none"
|
||||||
|
leftSection={<IconKey />}
|
||||||
|
placeholder={state.strings.accessCode}
|
||||||
|
onChange={(event) =>
|
||||||
|
actions.setToken(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<TextInput
|
<TextInput
|
||||||
className={classes.input}
|
className={classes.input}
|
||||||
size="md"
|
size="md"
|
||||||
@ -115,7 +185,7 @@ export function Access() {
|
|||||||
<Button variant="filled" className={classes.submit}>
|
<Button variant="filled" className={classes.submit}>
|
||||||
{state.strings.create}
|
{state.strings.create}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="subtle" onClick={() => actions.setMode('login')}>
|
<Button variant="subtle" onClick={() => actions.setMode('login')} size="compact-sm">
|
||||||
{state.strings.accountLogin}
|
{state.strings.accountLogin}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@ -124,6 +194,11 @@ export function Access() {
|
|||||||
<>
|
<>
|
||||||
<Title order={3}>{state.strings.admin}</Title>
|
<Title order={3}>{state.strings.admin}</Title>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
<Button
|
||||||
|
size="compact-sm"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={open}
|
||||||
|
>{ state.hostname }</Button>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
className={classes.input}
|
className={classes.input}
|
||||||
size="md"
|
size="md"
|
||||||
@ -155,6 +230,18 @@ export function Access() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Modal opened={opened} onClose={close} withCloseButton={false} centered>
|
||||||
|
<TextInput
|
||||||
|
size="md"
|
||||||
|
leftSectionPointerEvents="none"
|
||||||
|
leftSection={<IconServer />}
|
||||||
|
placeholder={state.strings.host}
|
||||||
|
value={state.node}
|
||||||
|
onChange={(event) =>
|
||||||
|
actions.setNode(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { useState, useContext, useEffect } from 'react'
|
import { useRef, useState, useContext, useEffect } from 'react'
|
||||||
import { SettingsContext } from '../context/SettingsContext'
|
import { SettingsContext } from '../context/SettingsContext'
|
||||||
|
import { AppContext } from '../context/AppContext'
|
||||||
import { ContextType } from '../context/ContextType'
|
import { ContextType } from '../context/ContextType'
|
||||||
|
|
||||||
export function useAccess() {
|
export function useAccess() {
|
||||||
|
const debounce = useRef(null);
|
||||||
|
const app = useContext(AppContext) as ContextType
|
||||||
const settings = useContext(SettingsContext) as ContextType
|
const settings = useContext(SettingsContext) as ContextType
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
display: null,
|
display: null,
|
||||||
@ -11,8 +14,13 @@ export function useAccess() {
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirm: '',
|
confirm: '',
|
||||||
|
token: '',
|
||||||
theme: '',
|
theme: '',
|
||||||
language: '',
|
language: '',
|
||||||
|
node: '',
|
||||||
|
hostname: '',
|
||||||
|
available: 0,
|
||||||
|
availableSet: false,
|
||||||
themes: settings.state.themes,
|
themes: settings.state.themes,
|
||||||
languages: settings.state.languages,
|
languages: settings.state.languages,
|
||||||
})
|
})
|
||||||
@ -22,6 +30,44 @@ export function useAccess() {
|
|||||||
setState((s) => ({ ...s, ...value }))
|
setState((s) => ({ ...s, ...value }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { protocol, hostname, port } = location
|
||||||
|
setUrl(`${protocol}//${hostname}:${port}`);
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setUrl = (node: string) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(node);
|
||||||
|
const { protocol, hostname, port } = url;
|
||||||
|
getAvailable(`${hostname}:${port}`, protocol === 'https:');
|
||||||
|
updateState({ node, hostname: hostname });
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
const { protocol, hostname, port } = location;
|
||||||
|
getAvailable(`${hostname}:${port}`, protocol === 'https:');
|
||||||
|
updateState({ node, hostname: location.hostname });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAvailable = (node: string, secure: boolean) => {
|
||||||
|
updateState({ availableSet: false });
|
||||||
|
clearTimeout(debounce.current);
|
||||||
|
debounce.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const available = await app.actions.getAvailable(node, secure);
|
||||||
|
|
||||||
|
console.log("AVAILABLE: ", available);
|
||||||
|
|
||||||
|
updateState({ available, availableSet: true });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { display, strings, themes, theme, languages, language } =
|
const { display, strings, themes, theme, languages, language } =
|
||||||
settings.state
|
settings.state
|
||||||
@ -48,12 +94,20 @@ export function useAccess() {
|
|||||||
setConfirm: (confirm: string) => {
|
setConfirm: (confirm: string) => {
|
||||||
updateState({ confirm })
|
updateState({ confirm })
|
||||||
},
|
},
|
||||||
|
setToken: (token: string) => {
|
||||||
|
updateState({ token })
|
||||||
|
},
|
||||||
|
setNode: (node: string) => {
|
||||||
|
setUrl(node);
|
||||||
|
},
|
||||||
setLanguage: (code: string) => {
|
setLanguage: (code: string) => {
|
||||||
settings.actions.setLanguage(code)
|
settings.actions.setLanguage(code)
|
||||||
},
|
},
|
||||||
setTheme: (theme: string) => {
|
setTheme: (theme: string) => {
|
||||||
settings.actions.setTheme(theme)
|
settings.actions.setTheme(theme)
|
||||||
},
|
},
|
||||||
|
login: () => {
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return { state, actions }
|
return { state, actions }
|
||||||
|
@ -10,6 +10,8 @@ export const en = {
|
|||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
enableNotifications: 'Push Notifications',
|
enableNotifications: 'Push Notifications',
|
||||||
|
accessCode: 'Access Code',
|
||||||
|
forgotPassword: 'Forgot Password',
|
||||||
|
|
||||||
new: 'New',
|
new: 'New',
|
||||||
newMessage: 'New Message',
|
newMessage: 'New Message',
|
||||||
@ -220,6 +222,8 @@ export const fr = {
|
|||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
enableNotifications: 'Notifications Push',
|
enableNotifications: 'Notifications Push',
|
||||||
|
accessCode: 'Code d\'Accès',
|
||||||
|
forgotPassword: 'Mot de Passe Oublié',
|
||||||
|
|
||||||
new: 'Nouveau',
|
new: 'Nouveau',
|
||||||
newMessage: 'Nouveau Message',
|
newMessage: 'Nouveau Message',
|
||||||
@ -433,6 +437,8 @@ export const sp = {
|
|||||||
ok: 'Aceptar',
|
ok: 'Aceptar',
|
||||||
cancel: 'Cancelar',
|
cancel: 'Cancelar',
|
||||||
enableNotifications: 'Notificaciones Push',
|
enableNotifications: 'Notificaciones Push',
|
||||||
|
accessCode: 'Código de Acceso',
|
||||||
|
forgotPassword: 'Contraseña Olvidada',
|
||||||
|
|
||||||
new: 'Nuevo',
|
new: 'Nuevo',
|
||||||
newMessage: 'Nuevo mensaje',
|
newMessage: 'Nuevo mensaje',
|
||||||
@ -644,6 +650,8 @@ export const pt = {
|
|||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
cancel: 'Cancelar',
|
cancel: 'Cancelar',
|
||||||
enableNotifications: 'Notificações Push',
|
enableNotifications: 'Notificações Push',
|
||||||
|
accessCode: 'Código de Acesso',
|
||||||
|
forgotPassword: 'Senha Esquecida',
|
||||||
|
|
||||||
new: 'Novo',
|
new: 'Novo',
|
||||||
newMessage: 'Nova mensagem',
|
newMessage: 'Nova mensagem',
|
||||||
@ -855,6 +863,8 @@ export const de = {
|
|||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
enableNotifications: 'Mitteilungen',
|
enableNotifications: 'Mitteilungen',
|
||||||
|
accessCode: 'Zugangscode',
|
||||||
|
forgotPassword: 'Passwort Vergessen',
|
||||||
|
|
||||||
new: 'Neu',
|
new: 'Neu',
|
||||||
newMessage: 'Neue Nachricht',
|
newMessage: 'Neue Nachricht',
|
||||||
@ -1067,6 +1077,8 @@ export const ru = {
|
|||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
cancel: 'Отмена',
|
cancel: 'Отмена',
|
||||||
enableNotifications: 'Уведомления',
|
enableNotifications: 'Уведомления',
|
||||||
|
accessCode: 'Код доступа',
|
||||||
|
forgotPassword: 'Пароль забыт',
|
||||||
|
|
||||||
new: 'Новый',
|
new: 'Новый',
|
||||||
newMessage: 'Новое сообщение',
|
newMessage: 'Новое сообщение',
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { DatabagSDK, Session } from 'databag-client-sdk'
|
import { DatabagSDK, Session } from 'databag-client-sdk'
|
||||||
import { SessionStore } from '../SessionStore'
|
import { SessionStore } from '../SessionStore'
|
||||||
|
|
||||||
export function useAppContext() {
|
export function useAppContext() {
|
||||||
const [state, setState] = useState({})
|
const sdk = useRef(new DatabagSDK(null))
|
||||||
|
const [state, setState] = useState({
|
||||||
|
session: null as null | Session,
|
||||||
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const updateState = (value: any) => {
|
const updateState = (value: any) => {
|
||||||
@ -11,16 +14,19 @@ export function useAppContext() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//init()
|
init()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const sdk = new DatabagSDK(null)
|
|
||||||
const store = new SessionStore()
|
const store = new SessionStore()
|
||||||
const session: Session | null = await sdk.initOnlineStore(store)
|
const session: Session | null = await sdk.current.initOnlineStore(store)
|
||||||
if (session) {
|
if (session) {
|
||||||
updateState({ sdk, session })
|
updateState({ session })
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
accountLogin: async () => {
|
||||||
const params = {
|
const params = {
|
||||||
topicBatch: 16,
|
topicBatch: 16,
|
||||||
tagBatch: 16,
|
tagBatch: 16,
|
||||||
@ -32,8 +38,7 @@ export function useAppContext() {
|
|||||||
version: '0.0.1',
|
version: '0.0.1',
|
||||||
appName: 'databag',
|
appName: 'databag',
|
||||||
}
|
}
|
||||||
console.log('-----> SDK LOGIN')
|
const login = await sdk.current.login(
|
||||||
const login = await sdk.login(
|
|
||||||
'asdf',
|
'asdf',
|
||||||
'asdf',
|
'asdf',
|
||||||
'balzack.coredb.org',
|
'balzack.coredb.org',
|
||||||
@ -41,12 +46,22 @@ export function useAppContext() {
|
|||||||
null,
|
null,
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
console.log(login)
|
updateState({ session: login })
|
||||||
updateState({ sdk, session: login })
|
},
|
||||||
}
|
accountLogout: async () => {
|
||||||
|
if (state.session) {
|
||||||
|
await sdk.current.logout(state.session, false);
|
||||||
|
updateState({ session: null })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getAvailable: async (node: string, secure: boolean) => {
|
||||||
|
return await sdk.current.available(node, secure);
|
||||||
|
},
|
||||||
|
adminLogin: async () => {
|
||||||
|
},
|
||||||
|
adminLogout: async () => {
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {}
|
|
||||||
|
|
||||||
return { state, actions }
|
return { state, actions }
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
|
import { Button } from '@mantine/core'
|
||||||
|
import { AppContext } from '../context/AppContext';
|
||||||
|
import { ContextType } from '../context/ContextType';
|
||||||
|
|
||||||
export function Session() {
|
export function Session() {
|
||||||
|
const app = useContext(AppContext) as ContextType;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Button onClick={app.actions.accountLogout}>Session Logout</Button>
|
||||||
<span>Session</span>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { clearLogin } from './net/clearLogin';
|
|||||||
import { setAccess } from './net/setAccess';
|
import { setAccess } from './net/setAccess';
|
||||||
import { addAccount } from './net/addAccount';
|
import { addAccount } from './net/addAccount';
|
||||||
import { setAdmin } from './net/setAdmin';
|
import { setAdmin } from './net/setAdmin';
|
||||||
|
import { getAvailable } from './net/getAvailable';
|
||||||
import type { Session, Node, Bot, SqlStore, WebStore, Crypto, Logging } from './api';
|
import type { Session, Node, Bot, SqlStore, WebStore, Crypto, Logging } from './api';
|
||||||
import type { SessionParams } from './types';
|
import type { SessionParams } from './types';
|
||||||
import type { Login } from './entities';
|
import type { Login } from './entities';
|
||||||
@ -40,6 +41,10 @@ export class DatabagSDK {
|
|||||||
return login ? new SessionModule(this.store, this.crypto, this.log, login.token, login.node, login.secure, login.timestamp) : null
|
return login ? new SessionModule(this.store, this.crypto, this.log, login.token, login.node, login.secure, login.timestamp) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async available(node: string, secure: boolean): Promise<number> {
|
||||||
|
return await getAvailable(node, secure);
|
||||||
|
}
|
||||||
|
|
||||||
public async login(handle: string, password: string, node: string, secure: boolean, mfaCode: string | null, params: SessionParams): Promise<Session> {
|
public async login(handle: string, password: string, node: string, secure: boolean, mfaCode: string | null, params: SessionParams): Promise<Session> {
|
||||||
const { appName, version, deviceId, deviceToken, pushType, notifications } = params;
|
const { appName, version, deviceId, deviceToken, pushType, notifications } = params;
|
||||||
const { guid, appToken, created, pushSupported } = await setLogin(node, secure, handle, password, mfaCode, appName, version, deviceId, deviceToken, pushType, notifications);
|
const { guid, appToken, created, pushSupported } = await setLogin(node, secure, handle, password, mfaCode, appName, version, deviceId, deviceToken, pushType, notifications);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user