Skip to content

Commit 8654772

Browse files
author
yqz
committed
修改配置文件,增加登录注册功能
1 parent 116b429 commit 8654772

11 files changed

Lines changed: 382 additions & 12 deletions

File tree

.cursor/rules/rule.mdc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@ alwaysApply: true
88

99

1010
后端规则:
11+
后端代码修改只修改要求的内容,不要做任何额外修改和处理
1112

13+
14+
项目设计:
15+
后端语言为java
16+
主框架为Springboot
17+
orm框架使用mybatis-plus

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,19 @@ jobs:
4242
with:
4343
node-version: "22"
4444
cache: "npm"
45-
cache-dependency-path: frontend/package-lock.json
45+
cache-dependency-path: vue/package-lock.json
4646

4747
- name: Install frontend dependencies and build
48-
working-directory: frontend
48+
working-directory: vue
4949
run: |
5050
npm ci
5151
npm run build
5252
5353
- name: Prepare release bundle for server
5454
run: |
5555
mkdir -p release
56-
cp -r frontend/dist release/dist
57-
cp frontend/Dockerfile frontend/docker.sh frontend/nginx.conf release/
56+
cp -r vue/dist release/dist
57+
cp vue/Dockerfile vue/docker.sh vue/nginx.conf release/
5858
5959
- name: Deploy files to server
6060
uses: easingthemes/ssh-deploy@v5.1.1

src/main/java/com/yqz/openblog/article/controller/ArticleController.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import jakarta.validation.Valid;
1515

16-
16+
import org.springframework.security.access.prepost.PreAuthorize;
1717
import org.springframework.web.bind.annotation.*;
1818
import jakarta.servlet.http.HttpServletRequest;
1919

@@ -45,40 +45,46 @@ public ApiResponse<ArticleDetailResponse> detailPublished(@PathVariable("article
4545
}
4646

4747
@PostMapping("/articles")
48+
@PreAuthorize("hasAnyRole('ADMIN','AUTHOR')")
4849
public ApiResponse<ArticleListItemResponse> createDraft(@RequestBody @Valid ArticleCreateRequest req) {
4950
Long uid = currentUser.userId();
5051
return ApiResponse.ok(articleService.createDraft(uid, req));
5152
}
5253

5354
@PutMapping("/articles/{articleId}")
55+
@PreAuthorize("hasAnyRole('ADMIN','AUTHOR')")
5456
public ApiResponse<ArticleListItemResponse> updateArticle(@PathVariable("articleId") Long articleId,
5557
@RequestBody @Valid ArticleUpdateRequest req) {
5658
Long uid = currentUser.userId();
5759
return ApiResponse.ok(articleService.updateArticle(uid, articleId, req));
5860
}
5961

6062
@PostMapping("/articles/{articleId}/publish")
63+
@PreAuthorize("hasAnyRole('ADMIN','AUTHOR')")
6164
public ApiResponse<ArticleListItemResponse> submitForReview(@PathVariable("articleId") Long articleId,
6265
@RequestBody(required = false) ArticlePublishRequest req) {
6366
Long uid = currentUser.userId();
6467
return ApiResponse.ok(articleService.publish(uid, articleId, req == null ? null : req.getPublishedAt()));
6568
}
6669

6770
@PostMapping("/articles/{articleId}/unpublish")
71+
@PreAuthorize("hasAnyRole('ADMIN','AUTHOR')")
6872
public ApiResponse<Void> unpublish(@PathVariable("articleId") Long articleId) {
6973
Long uid = currentUser.userId();
7074
articleService.unpublishOrDelete(uid, articleId);
7175
return ApiResponse.ok();
7276
}
7377

7478
@DeleteMapping("/articles/{articleId}")
79+
@PreAuthorize("hasAnyRole('ADMIN','AUTHOR')")
7580
public ApiResponse<Void> deleteArticle(@PathVariable("articleId") Long articleId) {
7681
Long uid = currentUser.userId();
7782
articleService.unpublishOrDelete(uid, articleId);
7883
return ApiResponse.ok();
7984
}
8085

8186
@GetMapping("/users/me/articles")
87+
@PreAuthorize("hasAnyRole('ADMIN','AUTHOR')")
8288
public ApiResponse<PageResult<ArticleListItemResponse>> listMine(
8389
@RequestParam(defaultValue = "0") int page,
8490
@RequestParam(defaultValue = "20") int size) {
@@ -87,6 +93,7 @@ public ApiResponse<PageResult<ArticleListItemResponse>> listMine(
8793
}
8894

8995
@GetMapping("/users/me/articles/{articleId}")
96+
@PreAuthorize("hasAnyRole('ADMIN','AUTHOR')")
9097
public ApiResponse<ArticleDetailResponse> detailMine(@PathVariable("articleId") Long articleId,
9198
HttpServletRequest request) {
9299
Long uid = currentUser.userId();

src/main/java/com/yqz/openblog/user/service/AuthService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ public void register(RegisterRequest req) {
7676
user.setEmail(req.getEmail());
7777
user.setPasswordHash(passwordEncoder.encode(req.getPassword()));
7878
user.setNickname(req.getNickname());
79-
// 个人博客模式:默认作者角色(无审核/无管理员审核流程
80-
user.setRole(UserRole.AUTHOR);
79+
// 前台自助注册:读者账号(与管理员/作者在库中共存;控制台与发文权限见接口鉴权
80+
user.setRole(UserRole.READER);
8181
user.setStatus("ACTIVE");
8282
userMapper.insert(user);
8383
}

vue/src/api/admin.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ export function login(account, password) {
77
})
88
}
99

10+
/** 前台会员注册(与控制台管理员登录入口分离) */
11+
export function register(payload) {
12+
const { username, email, password, nickname } = payload
13+
const body = { username, email, password }
14+
const nick = typeof nickname === 'string' ? nickname.trim() : ''
15+
if (nick) body.nickname = nick
16+
return request('/api/v1/auth/register', {
17+
method: 'POST',
18+
body: JSON.stringify(body)
19+
})
20+
}
21+
1022
export function fetchMe() {
1123
return request('/api/v1/users/me', { method: 'GET', withAuth: true })
1224
}

vue/src/assets/blog.css

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1880,6 +1880,93 @@ body {
18801880
font: inherit;
18811881
}
18821882

1883+
.site-nav-auth {
1884+
display: inline-flex;
1885+
align-items: center;
1886+
gap: 10px;
1887+
margin-left: 6px;
1888+
padding-left: 14px;
1889+
border-left: 1px solid var(--border);
1890+
}
1891+
1892+
.site-nav-user-name {
1893+
font-size: 14px;
1894+
font-weight: 650;
1895+
color: var(--text);
1896+
max-width: 112px;
1897+
overflow: hidden;
1898+
text-overflow: ellipsis;
1899+
white-space: nowrap;
1900+
}
1901+
1902+
.site-nav-pill-outline,
1903+
.site-nav-pill-solid {
1904+
border-radius: 999px;
1905+
padding: 6px 14px;
1906+
text-decoration: none;
1907+
}
1908+
1909+
.site-nav-pill-outline {
1910+
border: 1px solid var(--border);
1911+
color: var(--muted);
1912+
}
1913+
1914+
.site-nav-pill-outline:hover {
1915+
color: var(--text);
1916+
border-color: rgba(0, 0, 0, 0.14);
1917+
}
1918+
1919+
[data-theme='dark'] .site-nav-pill-outline:hover {
1920+
border-color: rgba(255, 255, 255, 0.16);
1921+
}
1922+
1923+
.site-nav-pill-solid {
1924+
border: 1px solid rgba(170, 59, 255, 0.45);
1925+
background: rgba(170, 59, 255, 0.1);
1926+
color: var(--text);
1927+
font-weight: 650;
1928+
}
1929+
1930+
.site-nav-pill-solid:hover {
1931+
border-color: rgba(170, 59, 255, 0.65);
1932+
background: rgba(170, 59, 255, 0.14);
1933+
}
1934+
1935+
.site-auth-card .site-auth-title {
1936+
font-weight: 1000;
1937+
font-size: 22px;
1938+
margin-bottom: 8px;
1939+
}
1940+
1941+
.site-auth-lead {
1942+
color: var(--muted);
1943+
font-size: 13px;
1944+
line-height: 1.55;
1945+
margin-bottom: 18px;
1946+
}
1947+
1948+
.site-auth-footer {
1949+
margin-top: 18px;
1950+
text-align: center;
1951+
font-size: 13px;
1952+
color: var(--muted);
1953+
}
1954+
1955+
.site-auth-link {
1956+
color: var(--muted);
1957+
text-decoration: none;
1958+
font-weight: 650;
1959+
}
1960+
1961+
.site-auth-link:hover {
1962+
color: var(--text);
1963+
}
1964+
1965+
.site-auth-dot {
1966+
margin: 0 6px;
1967+
opacity: 0.55;
1968+
}
1969+
18831970
.site-nav-widgets-btn {
18841971
display: inline-flex;
18851972
align-items: center;
@@ -2083,7 +2170,7 @@ body {
20832170
.site-nav {
20842171
gap: 12px;
20852172
}
2086-
.site-nav-link:nth-child(4) {
2173+
.site-nav-link-sm-hide {
20872174
display: none;
20882175
}
20892176
}

vue/src/auth/session.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,18 @@ export function isConsoleSessionValid() {
4747
if (!token || !isLikelyJwt(token) || isJwtExpired(token)) return false
4848
return true
4949
}
50+
51+
/** JWT payload 中的 role(ADMIN / AUTHOR / READER) */
52+
export function getAccessTokenRole() {
53+
const token = getStoredAccessToken()
54+
if (!token) return null
55+
const payload = parseJwtPayload(token)
56+
const r = payload?.role
57+
return typeof r === 'string' ? r : null
58+
}
59+
60+
/** 是否可进入 /console(与后台发文能力一致) */
61+
export function canAccessConsole() {
62+
const r = getAccessTokenRole()
63+
return r === 'ADMIN' || r === 'AUTHOR'
64+
}

vue/src/components/BlogHeader.vue

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,25 @@
3535
问题反馈
3636
</router-link>
3737

38-
<a class="site-nav-link" href="https://github.com/yyyCode/OpenBlog.git" target="_blank" rel="noopener noreferrer">
38+
<div class="site-nav-auth" aria-label="账户">
39+
<template v-if="me">
40+
<span class="site-nav-user-name" :title="displayName">{{ displayName }}</span>
41+
<button type="button" class="site-nav-link site-nav-link-btn site-nav-pill-outline" @click="logout">
42+
退出
43+
</button>
44+
</template>
45+
<template v-else>
46+
<router-link to="/login" class="site-nav-link site-nav-pill-outline">登录</router-link>
47+
<router-link to="/register" class="site-nav-link site-nav-pill-solid">注册</router-link>
48+
</template>
49+
</div>
50+
51+
<a
52+
class="site-nav-link site-nav-link-sm-hide"
53+
href="https://github.com/yyyCode/OpenBlog.git"
54+
target="_blank"
55+
rel="noopener noreferrer"
56+
>
3957
<span class="site-nav-ico" aria-hidden="true">
4058
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
4159
<path
@@ -61,20 +79,69 @@
6179
</template>
6280

6381
<script setup>
64-
import { onMounted, ref } from 'vue'
82+
import { computed, onMounted, ref, watch } from 'vue'
83+
import { useRoute, useRouter } from 'vue-router'
6584
import { toggleTheme as applyToggle } from '../theme'
85+
import { fetchMe } from '../api/admin'
86+
import { clearAuth, getStoredAccessToken, isJwtExpired, isLikelyJwt } from '../auth/session'
87+
6688
defineEmits(['toggle-widgets'])
6789
90+
const route = useRoute()
91+
const router = useRouter()
6892
const isDark = ref(false)
93+
const me = ref(null)
94+
95+
const displayName = computed(() => {
96+
const m = me.value
97+
if (!m) return ''
98+
const n = (m.nickname || '').trim()
99+
return n || m.username || '用户'
100+
})
101+
102+
function tokenLooksValid() {
103+
const t = getStoredAccessToken()
104+
return Boolean(t && isLikelyJwt(t) && !isJwtExpired(t))
105+
}
106+
107+
async function loadMe() {
108+
if (!tokenLooksValid()) {
109+
me.value = null
110+
return
111+
}
112+
try {
113+
me.value = await fetchMe()
114+
} catch {
115+
me.value = null
116+
}
117+
}
118+
119+
watch(
120+
() => route.fullPath,
121+
() => {
122+
loadMe()
123+
}
124+
)
69125
70126
function sync() {
71127
isDark.value = document.documentElement.getAttribute('data-theme') === 'dark'
72128
}
73129
74130
onMounted(() => {
75131
sync()
132+
loadMe()
76133
})
77134
135+
function logout() {
136+
clearAuth()
137+
me.value = null
138+
if (route.path.startsWith('/console')) {
139+
router.push('/console/login')
140+
} else {
141+
router.push('/')
142+
}
143+
}
144+
78145
function onToggle() {
79146
applyToggle()
80147
sync()

0 commit comments

Comments
 (0)