更新
This commit is contained in:
1 parent
012f8edf8b
commit
36ff960fbd
33 files changed
+1644
-344
No files matched your search
@@ -64,6 +64,12 @@
|
||||
<FaIcon name="chevron-right" color="#94a3b8" :size="12" />
|
||||
<view class="alt-line" />
|
||||
</view>
|
||||
|
||||
<!-- 返回门户:不登录也能先用免登录功能 -->
|
||||
<view class="back-portal" @tap="goPortal">
|
||||
<FaIcon name="chevron-left" color="#94a3b8" :size="12" />
|
||||
<text class="back-portal-text">返回门户,先使用免登录功能</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ============ 账号密码登录(独立视图) ============ -->
|
||||
@@ -133,6 +139,12 @@
|
||||
<view class="back-row">
|
||||
<text class="tip-text">没有账号?请联系企业管理员开通</text>
|
||||
</view>
|
||||
|
||||
<!-- 返回门户:不登录也能先用免登录功能 -->
|
||||
<view class="back-portal" @tap="goPortal">
|
||||
<FaIcon name="chevron-left" color="#94a3b8" :size="12" />
|
||||
<text class="back-portal-text">返回门户,先使用免登录功能</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ============ 选择企业(一人多企业) ============ -->
|
||||
@@ -221,6 +233,8 @@ import {
|
||||
getToken
|
||||
} from '@/utils/auth.js'
|
||||
import { showGeetest4 } from '@/utils/geetest.js'
|
||||
import { getGuestToken, clearGuestToken } from '@/utils/guest.js'
|
||||
import { guestMerge } from '@/api/guest.js'
|
||||
import FaIcon from '@/components/FaIcon.vue'
|
||||
|
||||
/** 运行端检测:h5 / apk(app-plus) / mp-weixin,决定登录方式优先级 */
|
||||
@@ -286,10 +300,10 @@ const brandPadTop = computed(() => {
|
||||
return `${statusBarHeight.value + uni.upx2px(88)}px`
|
||||
})
|
||||
|
||||
/** 是否存在可返回的上级视图(账号密码视图仅在小程序端可退回主视图) */
|
||||
/** 是否显示可返回的上级箭头(账号视图无论能否退回主视图都可见:退不了就回门户) */
|
||||
const canGoBack = computed(() => {
|
||||
if (step.value === 'tenant') return true
|
||||
if (step.value === 'account') return showMiniLogin.value
|
||||
if (step.value === 'account') return true
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -308,13 +322,30 @@ function goMain() {
|
||||
errorMsg.value = ''
|
||||
}
|
||||
|
||||
/** 顶部导航返回 */
|
||||
/** 顶部导航返回:登录内部层级优先逐级退;无可退上级时返回门户(免登录页) */
|
||||
function onNavBack() {
|
||||
if (step.value === 'tenant') {
|
||||
backToAccount()
|
||||
return
|
||||
}
|
||||
goMain()
|
||||
if (step.value === 'account' && showMiniLogin.value) {
|
||||
goMain()
|
||||
return
|
||||
}
|
||||
// 无登录内部上级 → 返回门户,继续用免登录功能
|
||||
goPortal()
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回门户首页(nav)。
|
||||
* 多数从 nav 点「需登录」模块进来,直接 navigateBack 回上一页(保留游客宫格);
|
||||
* 若是被 401 兑回登录页等无栈场景,回落到 reLaunch nav 以游客态重载。
|
||||
*/
|
||||
function goPortal() {
|
||||
if (submitting.value) return
|
||||
const pages = (typeof getCurrentPages === 'function' && getCurrentPages()) || []
|
||||
if (pages.length > 1) uni.navigateBack()
|
||||
else uni.reLaunch({ url: '/pages/nav/nav' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -508,6 +539,17 @@ async function finishLogin(tokens, tenantList, tid, fallbackAccount = '') {
|
||||
// 微信端:登录成功后静默绑定 openid,认证中/无手机号能力时一键登录靠它认人
|
||||
await silentBindMiniapp()
|
||||
|
||||
// 游客数据转正:持游客令牌(手机号授权)时静默并入账号;
|
||||
// 失败不阻断跳转(令牌保留,下次登录自动重试;令牌失效则由 401 自动清理)
|
||||
if (getGuestToken()) {
|
||||
try {
|
||||
await guestMerge()
|
||||
clearGuestToken()
|
||||
} catch (e) {
|
||||
console.warn('[login] guest merge failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
@@ -1082,6 +1124,25 @@ function goForget() {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
/* 返回门户:不登录也能先浏览免登录模块 */
|
||||
.back-portal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
margin-top: 28rpx;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.back-portal:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.back-portal-text {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
|
||||
+241
-13
@@ -7,14 +7,15 @@
|
||||
<text class="head-eyebrow">YUNZE WORKSPACE</text>
|
||||
<text class="head-title">工作台</text>
|
||||
</view>
|
||||
<view class="account-chip"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.account || '我的账号' }}</text></view>
|
||||
<view v-if="!guest" class="account-chip"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.account || '我的账号' }}</text></view>
|
||||
<view v-else class="account-chip" @tap="goLogin"><FaIcon name="right-to-bracket" color="#3478f6" :size="15" /><text>登录</text></view>
|
||||
</view>
|
||||
<view class="tenant-switch" @tap="chooseTenant">
|
||||
<view v-if="!guest" class="tenant-switch" @tap="chooseTenant">
|
||||
<view class="tenant-mark"><FaIcon name="building" color="#ffffff" :size="19" /></view>
|
||||
<view class="tenant-copy"><text class="tenant-label">当前租户</text><text class="tenant-name">{{ user.tenant || '云泽平台' }}</text></view>
|
||||
<view class="tenant-action"><text>切换</text><FaIcon name="chevron-down" color="#3478f6" :size="12" /></view>
|
||||
</view>
|
||||
<text class="head-sub">选择一个业务系统,开始高效工作</text>
|
||||
<text class="head-sub">{{ guest ? '免登录可逛部分功能,登录后使用完整工作台' : '选择一个业务系统,开始高效工作' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="body">
|
||||
@@ -46,12 +47,27 @@
|
||||
<FaIcon :name="card.icon" color="#ffffff" :size="26" />
|
||||
</view>
|
||||
<text class="portal-name">{{ card.name }}</text>
|
||||
<text v-if="guest" class="portal-tag" :class="card.needLogin ? 'need' : 'free'">{{ card.needLogin ? '需登录' : '免登录' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-space" />
|
||||
</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- 游客冷启动授权引导:手机号授权后签发游客令牌,免登录模块数据才能保存 -->
|
||||
<view v-if="guestAuthVisible" class="guest-auth-mask">
|
||||
<view class="guest-auth-card">
|
||||
<text class="guest-auth-title">开启免登录保存</text>
|
||||
<text class="guest-auth-desc">授权手机号后,记事本、日程提醒、密码库等免登录功能的数据即可保存;正式登录后会自动并入账号,不会丢失。</text>
|
||||
<button class="guest-auth-btn" open-type="getPhoneNumber" :disabled="guestAuthLoading" @getphonenumber="onGuestPhone">
|
||||
{{ guestAuthLoading ? '授权中…' : '立即授权' }}
|
||||
</button>
|
||||
<text class="guest-auth-skip" @tap="skipGuestAuth">暂不授权(使用将无法保存数据)</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -63,9 +79,80 @@ const headPad = mpHeadPad()
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { isLoggedIn, getTenants, getTid, setAuthTokens, setTid, setTenantName, getUser, setUser } from '@/utils/auth.js'
|
||||
import { switchTenant } from '@/api/auth.js'
|
||||
import { fetchHome } from '@/api/workspace.js'
|
||||
import { fetchHome, fetchPublicModules, fetchPublicMenus } from '@/api/workspace.js'
|
||||
import { openMenu } from '@/utils/nav.js'
|
||||
import { getSubsystemByModule, goSubsystem } from '@/config/subsystems.js'
|
||||
import { getGuestToken, setGuestToken } from '@/utils/guest.js'
|
||||
import { guestMiniLogin } from '@/api/guest.js'
|
||||
|
||||
/* ==================== 游客手机号授权(免登录保存) ==================== */
|
||||
|
||||
const guestAuthVisible = ref(false)
|
||||
const guestAuthLoading = ref(false)
|
||||
// 本次停留内跳过:关闭后不在 nav 反复弹窗,下次冷启动再引导
|
||||
const guestAuthDismissed = ref(false)
|
||||
|
||||
/** 仅微信小程序可拿手机号动态凭证 */
|
||||
function canRequestGuestPhone() {
|
||||
// #ifdef MP-WEIXIN
|
||||
return true
|
||||
// #endif
|
||||
// eslint-disable-next-line no-unreachable
|
||||
return false
|
||||
}
|
||||
|
||||
function maybeAskGuestAuth() {
|
||||
if (!guest.value || getGuestToken() || guestAuthDismissed.value || !canRequestGuestPhone()) {
|
||||
guestAuthVisible.value = false
|
||||
return
|
||||
}
|
||||
guestAuthVisible.value = true
|
||||
}
|
||||
|
||||
/** 取 uni.login 临时登录凭证(配合手机号动态 code 换游客令牌) */
|
||||
function navUniLoginCode() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (res) => (res && res.code ? resolve(res.code) : reject(new Error('未获取到微信登录凭证'))),
|
||||
fail: () => reject(new Error('获取微信登录凭证失败'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function onGuestPhone(e) {
|
||||
const detail = (e && e.detail) || {}
|
||||
if (String(detail.errMsg || '').indexOf('ok') === -1 || !detail.code) {
|
||||
uni.showToast({ title: '未获取到手机号授权', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (guestAuthLoading.value) return
|
||||
guestAuthLoading.value = true
|
||||
try {
|
||||
const code = await navUniLoginCode()
|
||||
let appid = ''
|
||||
try {
|
||||
appid = (uni.getAccountInfoSync().miniProgram || {}).appId || ''
|
||||
} catch (err) {
|
||||
appid = ''
|
||||
}
|
||||
const res = await guestMiniLogin({ code, phoneCode: detail.code, appid })
|
||||
if (!res || !res.token) throw new Error('未返回游客令牌')
|
||||
setGuestToken(res.token)
|
||||
guestAuthVisible.value = false
|
||||
uni.showToast({ title: '授权成功,数据可保存了', icon: 'none' })
|
||||
} catch (err) {
|
||||
console.error('[nav] guest mini-login failed:', err)
|
||||
uni.showToast({ title: (err && err.message) || '授权失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
guestAuthLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function skipGuestAuth() {
|
||||
guestAuthDismissed.value = true
|
||||
guestAuthVisible.value = false
|
||||
}
|
||||
|
||||
/** 模块 code → 子系统 slug → 进入其概览页;未注册返回 false */
|
||||
function goSubsystemByModule(code) {
|
||||
@@ -91,12 +178,43 @@ const modules = ref([])
|
||||
const menus = ref([])
|
||||
const tenants = ref(getTenants())
|
||||
const switchingTenant = ref(false)
|
||||
// 游客态:未登录时默认落在 nav,渲染公开模块宫格,按菜单级免登录配置分流。
|
||||
const guest = ref(!isLoggedIn())
|
||||
const publicModules = ref([])
|
||||
// 免登录菜单(/api/menus/public,require_login=0):模块宫格是否可免登录进入的唯一依据
|
||||
const publicMenus = ref([])
|
||||
|
||||
/** 含免登录菜单的模块 code 集合(小写) */
|
||||
const guestMenuModuleSet = computed(
|
||||
() => new Set(publicMenus.value.map((m) => String(m.module || '').toLowerCase()).filter(Boolean))
|
||||
)
|
||||
|
||||
/** 叶子菜单(可直接跳转的功能页),已锁定的过滤掉 */
|
||||
const leafMenus = computed(() => menus.value.filter((m) => m.leaf !== false && !m.locked))
|
||||
|
||||
/** 子系统卡片:每个业务模块一张卡 */
|
||||
const cards = computed(() => {
|
||||
// 游客:模块可免登录 = 该模块下存在免登录菜单(菜单级 require_login 控制)
|
||||
if (guest.value) {
|
||||
return publicModules.value.map((mod, index) => {
|
||||
const code = String(mod.code || '').toLowerCase()
|
||||
const needLogin = !guestMenuModuleSet.value.has(code)
|
||||
return {
|
||||
id: mod.id,
|
||||
code: mod.code,
|
||||
path: mod.path || '',
|
||||
name: mod.name || '未命名系统',
|
||||
desc: mod.description || (needLogin ? '需登录使用' : '免登录'),
|
||||
icon: MODULE_ICONS[code] || 'cube',
|
||||
tint: 'tint-' + (index % 8),
|
||||
locked: false,
|
||||
needLogin,
|
||||
items: [],
|
||||
total: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const byCode = {}
|
||||
leafMenus.value.forEach((m) => {
|
||||
if (!m.module) return
|
||||
@@ -132,6 +250,25 @@ const cards = computed(() => {
|
||||
|
||||
async function load(force = false) {
|
||||
loading.value = true
|
||||
guest.value = !isLoggedIn()
|
||||
// 游客:公开模块宫格 + 免登录菜单;模块可点的判定下沉到菜单级
|
||||
// (存在 require_login=0 的菜单即免登录,模块级 require_login 不再作为依据)
|
||||
if (guest.value) {
|
||||
try {
|
||||
const [list, menuList] = await Promise.all([fetchPublicModules(), fetchPublicMenus()])
|
||||
publicModules.value = Array.isArray(list) ? list : []
|
||||
publicMenus.value = Array.isArray(menuList) ? menuList : []
|
||||
loadError.value = ''
|
||||
} catch (e) {
|
||||
console.error('[nav] public modules load failed:', e)
|
||||
loadError.value = (e && e.message) || '接口请求失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
// 进小程序即引导授权:游客无令牌时弹手机号授权窗
|
||||
maybeAskGuestAuth()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await fetchHome(force)
|
||||
user.value = data.user || {}
|
||||
@@ -188,12 +325,17 @@ function chooseTenant() {
|
||||
/**
|
||||
* 点击子系统卡片 → 进入该子系统独立页面组(带自己的 tabbar)。
|
||||
* 未在 config/subsystems.js 注册的模块,降级为直接打开 Web 端该模块首页。
|
||||
* 游客:命中「必须登录」模块时不直接进,先引导登录(登录后回 nav)。
|
||||
*/
|
||||
function enter(card) {
|
||||
if (card.locked) {
|
||||
openMenu({ locked: true, title: card.name })
|
||||
return
|
||||
}
|
||||
if (guest.value && card.needLogin) {
|
||||
goLogin(card.name)
|
||||
return
|
||||
}
|
||||
if (goSubsystemByModule(card.code)) return
|
||||
if (card.items.length) {
|
||||
openMenu(card.items[0], card.name)
|
||||
@@ -206,6 +348,14 @@ function enter(card) {
|
||||
uni.showToast({ title: card.name + '暂未开放', icon: 'none' })
|
||||
}
|
||||
|
||||
/** 引导登录:登录页登录成功后会回 nav(见 login.vue finishLogin) */
|
||||
function goLogin(name = '') {
|
||||
if (name) {
|
||||
uni.showToast({ title: '「' + name + '」需登录后使用', icon: 'none' })
|
||||
}
|
||||
uni.navigateTo({ url: '/pages/login/login' })
|
||||
}
|
||||
|
||||
function openTenantPicker() {
|
||||
if (tenants.value.length < 2) return
|
||||
tenantPickerVisible.value = true
|
||||
@@ -246,19 +396,12 @@ async function changeTenant(item) {
|
||||
/* ==================== 生命周期 ==================== */
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
// 默认落地页:未登录不再强制回登录页,而是以游客态渲染公开模块宫格
|
||||
load()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
// 未登录(含后台切回来时令牌被清)直接回登录页,不再停留在首页
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
// 登录态可能在登录页/其他入口发生变化,每次展示重新判定并加载
|
||||
if (!loading.value) load()
|
||||
})
|
||||
</script>
|
||||
@@ -458,7 +601,92 @@ onShow(() => {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
/* 游客态:卡片左上角标出免登录 / 需登录 */
|
||||
.portal-card {
|
||||
position: relative;
|
||||
}
|
||||
.portal-tag {
|
||||
position: absolute;
|
||||
top: 14rpx;
|
||||
right: 14rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 18rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.portal-tag.free {
|
||||
color: #16a34a;
|
||||
background: rgba(22, 163, 74, 0.12);
|
||||
}
|
||||
.portal-tag.need {
|
||||
color: #94a3b8;
|
||||
background: rgba(148, 163, 184, 0.16);
|
||||
}
|
||||
|
||||
.bottom-space {
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
/* ============ 游客授权引导弹窗 ============ */
|
||||
.guest-auth-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 56rpx;
|
||||
}
|
||||
|
||||
.guest-auth-card {
|
||||
width: 100%;
|
||||
max-width: 560rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 44rpx 36rpx 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 24rpx 64rpx rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
|
||||
.guest-auth-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #17345f;
|
||||
}
|
||||
|
||||
.guest-auth-desc {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
color: #64748b;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.guest-auth-btn {
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
background: linear-gradient(145deg, #69a5ff, #3478f6);
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
line-height: 2.6;
|
||||
border: none;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.guest-auth-skip {
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -69,6 +69,7 @@ import { onShow } from '@dcloudio/uni-app'
|
||||
import { fetchNotes, deleteNote, toggleNotePin } from '@/api/note.js'
|
||||
import { formatRelativeTime } from '@/utils/date.js'
|
||||
import { isLoggedIn } from '@/utils/auth.js'
|
||||
import { getGuestToken } from '@/utils/guest.js'
|
||||
|
||||
const keyword = ref('')
|
||||
const notes = ref([])
|
||||
@@ -78,15 +79,16 @@ const loading = ref(false)
|
||||
const pinnedCount = computed(() => notes.value.filter(item => item.pinned).length)
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
// 免登录页面:登录态或游客令牌均可用;两者皆无回门户触发授权/登录
|
||||
if (!isLoggedIn() && !getGuestToken()) {
|
||||
uni.reLaunch({ url: '/pages/nav/nav' })
|
||||
return
|
||||
}
|
||||
loadNotes()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (isLoggedIn()) loadNotes()
|
||||
if (isLoggedIn() || getGuestToken()) loadNotes()
|
||||
})
|
||||
|
||||
async function loadNotes() {
|
||||
|
||||
@@ -115,6 +115,7 @@ import { onShow } from '@dcloudio/uni-app'
|
||||
import { fetchSchedules, getRemindChannelLabel, getRemindMinutesLabel } from '@/api/schedule.js'
|
||||
import { pad, startOfDay, isToday } from '@/utils/date.js'
|
||||
import { isLoggedIn } from '@/utils/auth.js'
|
||||
import { getGuestToken } from '@/utils/guest.js'
|
||||
|
||||
/** 周一为一周第一天 */
|
||||
const WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日']
|
||||
@@ -282,8 +283,9 @@ function openCreate() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
// 免登录页面:登录态或游客令牌均可用;两者皆无回门户触发授权/登录
|
||||
if (!isLoggedIn() && !getGuestToken()) {
|
||||
uni.reLaunch({ url: '/pages/nav/nav' })
|
||||
return
|
||||
}
|
||||
loadReminders()
|
||||
@@ -291,8 +293,7 @@ onMounted(() => {
|
||||
|
||||
// 新增/编辑返回后刷新列表
|
||||
onShow(() => {
|
||||
if (!isLoggedIn()) return
|
||||
if (schedules.value.length || !loading.value) loadReminders()
|
||||
if (isLoggedIn() || getGuestToken()) loadReminders()
|
||||
})
|
||||
|
||||
async function loadReminders() {
|
||||
|
||||
Reference in new issue
Block a user