Skip to content

Commit

Permalink
feat: 添加对多种登录方式的支持
Browse files Browse the repository at this point in the history
  • Loading branch information
caixw committed Oct 25, 2024
1 parent 98ecc7b commit 31a4228
Show file tree
Hide file tree
Showing 26 changed files with 225 additions and 93 deletions.
4 changes: 2 additions & 2 deletions admin/src/app/context/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ export function buildContext(opt: Required<buildOptions>, f: API) {
* @param account 账号密码信息
* @returns true 表示登录成功,其它情况表示错误信息
*/
async login(account: Account) {
const ret = await f.login(account);
async login(account: Account,type: string = 'password') {
const ret = await f.login(account, type);
if (ret === true) {
uid = account.username;
sessionStorage.setItem(currentKey, uid);
Expand Down
4 changes: 2 additions & 2 deletions admin/src/core/api/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('API token', () => {
});

test('token', async () => {
await writeToken(Object.assign({}, token));
writeToken(Object.assign({}, token));
const f = await API.build('http://localhost', '/login', 'application/json', 'zh-cn');
let t = await f.getToken(); // 过期时间在 1 秒之内,必然未过期。
expect(t).toEqual('access');
Expand All @@ -82,7 +82,7 @@ describe('API token', () => {
test('login', async () => {
const f = await API.build('http://localhost', '/login', 'application/json', 'zh-cn');
fetchMock.mockResponseOnce(JSON.stringify(Object.assign({}, token)));
const ret = await f.login({ username: 'admin', password: '123' });
const ret = await f.login({ username: 'admin', password: '123' }, 'password');
expect(ret).toBeTruthy();

let t = await f.getToken();
Expand Down
4 changes: 2 additions & 2 deletions admin/src/core/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ export class API {
*
* @returns 如果返回 true,表示操作成功,否则表示错误信息。
*/
async login(account: Account): Promise<Problem<never>|undefined|true> {
const token = await this.post<Token>(this.#loginPath, account, false);
async login(account: Account, type: string): Promise<Problem<never>|undefined|true> {
const token = await this.post<Token>(this.#loginPath + '?type='+type, account, false);
if (token.ok) {
this.#token = writeToken(token.body!);
await this.clearCache();
Expand Down
38 changes: 31 additions & 7 deletions admin/src/pages/current/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
// SPDX-License-Identifier: MIT

import { Navigate, useNavigate } from '@solidjs/router';
import { JSX, Match, Switch } from 'solid-js';
import { createSignal, JSX, Match, onMount, Switch } from 'solid-js';

import { useApp, useOptions } from '@/app/context';
import { Button, Icon, ObjectAccessor, Page, Password, TextField } from '@/components';
import { buildEnumsOptions, Button, Choice, FieldAccessor, Icon, ObjectAccessor, Page, Password, TextField } from '@/components';
import { Account } from '@/core';

/**
Expand All @@ -27,23 +27,47 @@ export function Login(): JSX.Element {
const opt = useOptions();
const nav = useNavigate();

const [passports, setPassports] = createSignal<Array<[string,string]>>([]);
const passport = FieldAccessor('passport', 'password');

onMount(async () => {
const r = await ctx.api.get<Array<Passport>>('/passports');
if (!r.ok) {
ctx.outputProblem(r.body);
return;
}
setPassports(r.body!.map((v)=>[v.name,v.desc]));
});

const f = new ObjectAccessor<Account>({ username: '', password: '' });

return <Page title="_i.page.current.login" class="p--login palette--primary">
return <Page title="_i.page.current.login" class="p--login">
<form onReset={()=>f.reset()} onSubmit={async()=>{
const ret = await ctx.login(f.object());
const ret = await ctx.login(f.object(), passport.getValue());
if (ret === true) {
nav(opt.routes.private.home);
} else if (ret) {
await ctx.outputProblem(ret);
}
}}>
<p class="text-lg">{ctx.locale().t('_i.page.current.login')}</p>
<div class="title">
<p class="text-2xl">{ctx.locale().t('_i.page.current.login')}</p>
<Choice accessor={passport} options={buildEnumsOptions(passports(), ctx)}/>
</div>

<TextField prefix={<Icon class="!py-0 !px-1 flex items-center" icon='person' />}
placeholder={ctx.locale().t('_i.page.current.username')} accessor={f.accessor('username', true)} />

<Password icon='password_2' placeholder={ctx.locale().t('_i.page.current.password')} accessor={f.accessor('password', true)} />
<Button disabled={f.accessor('username').getValue() == ''} type="submit">{ctx.locale().t('_i.ok')}</Button>
<Button type="reset">{ ctx.locale().t('_i.reset') }</Button>

<Button palette='primary' disabled={f.accessor('username').getValue() == ''} type="submit">{ctx.locale().t('_i.ok')}</Button>

<Button palette='secondary' type="reset">{ ctx.locale().t('_i.reset') }</Button>
</form>
</Page>;
}

interface Passport {
name: string;
desc: string;
}
7 changes: 6 additions & 1 deletion admin/src/pages/current/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
@apply justify-center h-full items-center;

form {
@apply flex justify-center flex-col px-2 gap-2 w-full xs:w-80;
@apply flex justify-center flex-col gap-2 w-full xs:w-96 p-5;
@apply border border-palette-bg-low rounded-md bg-palette-bg shadow-sm shadow-palette-bg-low;

.title {
@apply flex justify-between mb-8;
}
}
}

Expand Down
1 change: 1 addition & 0 deletions admin/tailwind.preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const config: PresetsConfig = {
outlineColor: colors,
ringColor: colors,
divideColor: colors,
boxShadowColor: colors,

minWidth: breakpoints,
maxWidth: breakpoints,
Expand Down
3 changes: 3 additions & 0 deletions cmfx/initial/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/issue9/cmfx/cmfx/initial"
"github.com/issue9/cmfx/cmfx/modules/admin"
"github.com/issue9/cmfx/cmfx/modules/system"
"github.com/issue9/cmfx/cmfx/user/passport/password"
)

func Exec(name, version string) error {
Expand Down Expand Up @@ -66,6 +67,8 @@ func initServer(name, ver string, o *server.Options, user *Config, action string
switch action {
case "serve":
adminL := admin.Load(adminMod, user.Admin, uploadSaver)
adminL.Passport().Register("password2", password.New(adminL.Module(), "password2", 5), web.Phrase("another password valid"))

system.Load(systemMod, user.System, adminL)
case "install":
adminL := admin.Install(adminMod, user.Admin)
Expand Down
5 changes: 0 additions & 5 deletions cmfx/modules/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,3 @@

// Package admin 管理端的相关操作
package admin

const (
passwordID = "password" // 采用密码登录的
defaultPassword = "123"
)
1 change: 1 addition & 0 deletions cmfx/modules/admin/admintest/admintest.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func NewModule(s *test.Suite) *admin.Module {
AccessExpired: 60 * config.Duration(time.Second),
RefreshExpired: 120 * config.Duration(time.Second),
},
DefaultPassword: "123",
Upload: &admin.Upload{
Size: 1024 * 1024 * 1024,
Exts: []string{".jpg"},
Expand Down
11 changes: 11 additions & 0 deletions cmfx/modules/admin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ type Config struct {
// User 用户相关的配置
User *user.Config `json:"user" xml:"user" yaml:"user"`

// DefaultPassword 默认的密码
//
// 重置密码的操作将会将用户的密码重置为该值。
//
// 如果未设置,该值将被设置为 123
DefaultPassword string `json:"defaultPassword" xml:"defaultPassword" yaml:"defaultPassword"`

// 上传接口的相关配置
Upload *Upload `json:"upload" xml:"upload" yaml:"upload"`
}
Expand All @@ -40,6 +47,10 @@ func (c *Config) SanitizeConfig() *web.FieldError {
return err.AddFieldParent("user")
}

if c.DefaultPassword == "" {
c.DefaultPassword = "123"
}

if c.Upload != nil {
if err := c.Upload.SanitizeConfig(); err != nil {
return err.AddFieldParent("upload")
Expand Down
12 changes: 6 additions & 6 deletions cmfx/modules/admin/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

func Install(mod *cmfx.Module, o *Config) *Module {
user.Install(mod)
password.Install(mod)
password.Install(mod, "passwords")
rbac.Install(mod)

if err := mod.DB().Create(&info{}); err != nil {
Expand Down Expand Up @@ -52,7 +52,7 @@ func Install(mod *cmfx.Module, o *Config) *Module {
},
},
Username: "admin",
Password: defaultPassword,
Password: o.DefaultPassword,
},
{
ctxInfoWithRoleState: ctxInfoWithRoleState{
Expand All @@ -63,7 +63,7 @@ func Install(mod *cmfx.Module, o *Config) *Module {
},
},
Username: "u1",
Password: defaultPassword,
Password: o.DefaultPassword,
},
{
ctxInfoWithRoleState: ctxInfoWithRoleState{
Expand All @@ -74,7 +74,7 @@ func Install(mod *cmfx.Module, o *Config) *Module {
},
},
Username: "u2",
Password: defaultPassword,
Password: o.DefaultPassword,
},
{
ctxInfoWithRoleState: ctxInfoWithRoleState{
Expand All @@ -84,12 +84,12 @@ func Install(mod *cmfx.Module, o *Config) *Module {
},
},
Username: "u3",
Password: defaultPassword,
Password: o.DefaultPassword,
},
}

for _, u := range us {
if err := l.newAdmin(l.password, u, time.Now()); err != nil {
if err := l.newAdmin(u, time.Now()); err != nil {
panic(web.SprintError(mod.Server().Locale().Printer(), true, err))
}
}
Expand Down
1 change: 1 addition & 0 deletions cmfx/modules/admin/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestInstall(t *testing.T) {
AccessExpired: 60,
RefreshExpired: 120,
},
DefaultPassword: "123",
Upload: &Upload{
Size: 1024 * 1024 * 1024,
Exts: []string{".jpg"},
Expand Down
5 changes: 3 additions & 2 deletions cmfx/modules/admin/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ type ctxInfoWithRoleState struct {
Created time.Time `json:"created" xml:"created,attr" cbor:"created"` // 添加时间
}

// 添加新的管理员时,需要提供的数据
type reqInfoWithAccount struct {
ctxInfoWithRoleState
Username string `json:"username" xml:"username" cbor:"username"`
Password string `json:"password" xml:"password" cbor:"password"`
Username string `json:"username" xml:"username" cbor:"username"` // 账号
Password string `json:"password" xml:"password" cbor:"password"` // 密码
}

func (i *info) Filter(v *web.FilterContext) {
Expand Down
19 changes: 10 additions & 9 deletions cmfx/modules/admin/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import (
"github.com/issue9/cmfx/cmfx/user/rbac"
)

const passportTypePassword = "password" // 采用密码登录的

type Module struct {
user *user.Module
user *user.Module
defaultPassword string

password passport.Adapter
roleGroup *rbac.RoleGroup

uploadField string
Expand All @@ -46,12 +48,10 @@ func Load(mod *cmfx.Module, o *Config, saver upload.Saver) *Module {

u := user.Load(mod, o.User)

pass := password.New(mod, 8)
u.Passport().Register(passwordID, pass, web.StringPhrase("password mode"))
u.Passport().Register(passportTypePassword, password.New(mod, "passwords", 8), web.StringPhrase("password mode"))
m := &Module{
user: u,

password: pass,
user: u,
defaultPassword: o.DefaultPassword,

uploadField: o.Upload.Field,

Expand Down Expand Up @@ -86,6 +86,7 @@ func Load(mod *cmfx.Module, o *Config, saver upload.Saver) *Module {
loginRate := ratelimit.New(web.NewCache(mod.ID()+"_rate", mod.Server().Cache()), 20, time.Second, nil, nil)

mod.Router().Prefix(m.URLPrefix()).
Get("/passports", m.getPassports).
Post("/login", m.postLogin, loginRate, initial.Unlimit(mod.Server())).
Delete("/login", m.deleteLogin, m).
Put("/login", m.putToken, m)
Expand Down Expand Up @@ -190,8 +191,8 @@ func (m *Module) Module() *cmfx.Module { return m.user.Module() }
func (m *Module) Passport() *passport.Passport { return m.user.Passport() }

// 手动添加一个新的管理员
func (m *Module) newAdmin(pa passport.Adapter, data *reqInfoWithAccount, now time.Time) error {
uid, err := m.user.NewUser(pa, data.Username, data.Password, now)
func (m *Module) newAdmin(data *reqInfoWithAccount, now time.Time) error {
uid, err := m.user.NewUser(m.Passport().Get(passportTypePassword), data.Username, data.Password, now)
if err != nil {
return err
}
Expand Down
40 changes: 33 additions & 7 deletions cmfx/modules/admin/route_admins.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
package admin

import (
"cmp"
"errors"
"net/http"
"slices"
"time"

"github.com/issue9/orm/v6"
Expand All @@ -20,11 +22,23 @@ import (
"github.com/issue9/cmfx/cmfx/user"
)

type respAdminInfo struct {
ctxInfoWithRoleState

// 当前用户已经开通的验证方式
Passports []*respPassportIdentity `json:"passports" xml:"passports" cbor:"passports"`
}

type respPassportIdentity struct {
Name string `json:"name" xml:"name" cbor:"name"`
Identity string `json:"identity" xml:"identity" cbor:"identity"`
}

// # API GET /admins/{id} 获取指定的管理员账号
//
// @tag admin
// @path id int 管理的 ID
// @resp 200 * ctxInfoWithRoleState
// @resp 200 * respAdminInfo
func (m *Module) getAdmin(ctx *web.Context) web.Responser {
id, resp := ctx.PathID("id", cmfx.BadRequestInvalidPath)
if resp != nil {
Expand All @@ -51,10 +65,22 @@ func (m *Module) getAdmin(ctx *web.Context) web.Responser {
rs = append(rs, r.ID)
}

return web.OK(&ctxInfoWithRoleState{
info: *a,
Roles: rs,
State: u.State,
ps := make([]*respPassportIdentity, 0)
for k, v := range m.Passport().Identities(id) {
ps = append(ps, &respPassportIdentity{
Name: k,
Identity: v,
})
}
slices.SortFunc(ps, func(a, b *respPassportIdentity) int { return cmp.Compare(a.Name, b.Name) }) // 排序,尽量使输出的内容相同

return web.OK(&respAdminInfo{
ctxInfoWithRoleState: ctxInfoWithRoleState{
info: *a,
Roles: rs,
State: u.State,
},
Passports: ps,
})
}

Expand Down Expand Up @@ -204,7 +230,7 @@ func (m *Module) deleteAdminPassword(ctx *web.Context) web.Responser {
}

// 更新数据库
if err := m.password.Set(id, defaultPassword); err != nil {
if err := m.Passport().Get(passportTypePassword).Set(id, m.defaultPassword); err != nil {
return ctx.Error(err, "")
}

Expand All @@ -221,7 +247,7 @@ func (m *Module) postAdmins(ctx *web.Context) web.Responser {
return resp
}

if err := m.newAdmin(m.password, data, ctx.Begin()); err != nil {
if err := m.newAdmin(data, ctx.Begin()); err != nil {
return ctx.Error(err, "")
}
return web.Created(nil, "")
Expand Down
2 changes: 1 addition & 1 deletion cmfx/modules/admin/route_current.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (m *Module) putCurrentPassword(ctx *web.Context) web.Responser {
}

a := m.CurrentUser(ctx)
err := m.password.Change(a.ID, data.Old, data.New)
err := m.Passport().Get(passportTypePassword).Change(a.ID, data.Old, data.New)
if errors.Is(err, passport.ErrUnauthorized()) {
return ctx.Problem(cmfx.Unauthorized)
} else if err != nil {
Expand Down
Loading

0 comments on commit 31a4228

Please sign in to comment.