Skip to content

Commit

Permalink
feat: supprime user.admin
Browse files Browse the repository at this point in the history
Tout ce qui a trait à 'admin' concerne désormais uniquement le token.
  • Loading branch information
thom4parisot committed Feb 12, 2025
1 parent b716315 commit 965c551
Show file tree
Hide file tree
Showing 14 changed files with 294 additions and 219 deletions.
3 changes: 0 additions & 3 deletions front/src/components/Credentials.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ query getUserDetails($user: ID!) {
displayName
_id
email
admin
createdAt
updatedAt
firstName
Expand All @@ -23,7 +22,6 @@ query getFullUserProfile {
lastName
institution
email
admin
createdAt
updatedAt
apiToken
Expand Down Expand Up @@ -64,7 +62,6 @@ mutation updateUser($user: ID!, $details: UserProfileInput!) {
displayName
_id
email
admin
createdAt
updatedAt
firstName
Expand Down
1 change: 0 additions & 1 deletion front/src/components/UserInfos.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ export default function UserInfos() {
<Field label={t('user.account.id')}>
<code>{activeUser._id}</code>
</Field>
{activeUser.admin && <Field label="Admin">✔️</Field>}
<Field label={t('user.account.createdAt')}>
<TimeAgo date={activeUser.createdAt} />
</Field>
Expand Down
10 changes: 7 additions & 3 deletions graphql/helpers/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@ const jwt = require('jsonwebtoken')
const User = require('../models/user')
const Sentry = require('@sentry/node')

module.exports.createJWTToken = async function createJWTToken ({ email, jwtSecret}) {
module.exports.createJWTToken = async function createJWTToken({
email,
jwtSecret,
}) {
const user = await User.findOne({ email })

// generate a JWT token
const payload = {
email,
_id: user._id,
authType: user.authType,
admin: Boolean(user.admin),
session: true,
}

return jwt.sign(payload, jwtSecret)
}

module.exports.populateUserFromJWT = function populateUserFromJWT ({ jwtSecret }) {
module.exports.populateUserFromJWT = function populateUserFromJWT({
jwtSecret,
}) {
return async function populateUserFromJWTMiddleware(req, res, next) {
const jwtToken = req.headers.authorization?.replace(/^Bearer\s+/, '')

Expand Down
17 changes: 17 additions & 0 deletions graphql/migrations/20250212122400-user-delete-admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
exports.up = async function (db) {
const mongo = await db._run('getDbInstance', true)
try {
await mongo.collection('users').updateMany({}, { $unset: { admin: '' } })
} finally {
await mongo.close()
}
}

exports.down = async function (db) {
const mongo = await db._run('getDbInstance', true)
try {
await mongo.collection('users').updateMany({}, { $set: { admin: false } })
} finally {
await mongo.close()
}
}
150 changes: 75 additions & 75 deletions graphql/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,66 @@ const { article: defaultArticle } = require('../data/defaultsData')

const Schema = mongoose.Schema

const userSchema = new Schema({
email: {
type: String,
unique: true,
required: true
},
username: {
type: String,
// unique but not required, we need to create a sparse index manually
},
// TODO remove this link
tags: [
{
type: Schema.Types.ObjectId,
ref: 'Tag'
}
],
acquintances: [
{
type: Schema.Types.ObjectId,
ref: 'User'
}
],
articles: [
{
type: Schema.Types.ObjectId,
ref: 'Article'
}
],
authType: {
type: String,
default: 'local',
enum: ['local', 'oidc']
},
password: {
type: String,
default: null,
set: (password) => {
return bcrypt.hashSync(password, 10)
}
},
displayName: {
type: String,
},
admin: {
type: Boolean,
default: false
},
firstName: {
type: String
},
lastName: {
type: String
},
institution: {
type: String
const userSchema = new Schema(
{
email: {
type: String,
unique: true,
required: true,
},
username: {
type: String,
// unique but not required, we need to create a sparse index manually
},
// TODO remove this link
tags: [
{
type: Schema.Types.ObjectId,
ref: 'Tag',
},
],
acquintances: [
{
type: Schema.Types.ObjectId,
ref: 'User',
},
],
articles: [
{
type: Schema.Types.ObjectId,
ref: 'Article',
},
],
authType: {
type: String,
default: 'local',
enum: ['local', 'oidc'],
},
password: {
type: String,
default: null,
set: (password) => {
return bcrypt.hashSync(password, 10)
},
},
displayName: {
type: String,
},
firstName: {
type: String,
},
lastName: {
type: String,
},
institution: {
type: String,
},
zoteroToken: {
type: String,
},
},
zoteroToken: {
type: String
}
}, { timestamps: true })
{ timestamps: true }
)

/**
* Compare an existing password against a user input one.
Expand All @@ -81,23 +80,24 @@ userSchema.methods.comparePassword = async function (password) {
return bcrypt.compare(password, oldPassword)
}

userSchema.methods.createDefaultArticle = async function createDefaultArticle () {
const newArticle = await this.model('Article').create({
title: defaultArticle.title,
zoteroLink: defaultArticle.zoteroLink,
owner: this,
workingVersion: {
metadata: defaultArticle.metadata,
bib: defaultArticle.bib,
md: defaultArticle.md
},
})
userSchema.methods.createDefaultArticle =
async function createDefaultArticle() {
const newArticle = await this.model('Article').create({
title: defaultArticle.title,
zoteroLink: defaultArticle.zoteroLink,
owner: this,
workingVersion: {
metadata: defaultArticle.metadata,
bib: defaultArticle.bib,
md: defaultArticle.md,
},
})

await newArticle.createNewVersion({ mode: 'MINOR', user: this })
await newArticle.createNewVersion({ mode: 'MINOR', user: this })

this.articles.push(newArticle)
return this.save()
}
this.articles.push(newArticle)
return this.save()
}

userSchema.virtual('authTypes').get(function () {
const types = new Set()
Expand Down
27 changes: 20 additions & 7 deletions graphql/policies/isUser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ const UserModel = require('../models/user.js')

const user = '63977de2f83aa77c5f92cb1c'
const sameUserObject = new UserModel({ _id: user })
const sameUserToken = { _id: user, email: '[email protected]', admin: false, session: true, authType: 'oidc' }
const sameUserToken = {
_id: user,
email: '[email protected]',
session: true,
authType: 'oidc',
}

const differentUserObject = new UserModel({ _id: '00000de2f83aa77c5f92dc2f'})
const differentUserObject = new UserModel({ _id: '00000de2f83aa77c5f92dc2f' })

const adminToken = { admin: true, roles: ['read'], readonly: true }


describe('isUser', () => {
test('without token, no args.user', () => {
expect(() => isUser({ }, { token: {} })).toThrow(/Unauthorized/)
expect(() => isUser({}, { token: {} })).toThrow(/Unauthorized/)
})

test('without token, explicit args.user', () => {
Expand All @@ -28,14 +32,23 @@ describe('isUser', () => {
})

test('with token, implicit user is token user', () => {
expect(isUser({}, { token: sameUserToken, user: sameUserObject })).toEqual({ userId: sameUserToken._id })
expect(isUser({}, { token: sameUserToken, user: sameUserObject })).toEqual({
userId: sameUserToken._id,
})
})

test('with token, explicit user is same as user token', () => {
expect(isUser({ user }, { token: sameUserToken, user: sameUserObject })).toEqual({ userId: user })
expect(
isUser({ user }, { token: sameUserToken, user: sameUserObject })
).toEqual({ userId: user })
})

test('with token, explicit user is different than user token', () => {
expect(() => isUser({ user: differentUserObject.id }, { token: sameUserToken, user: sameUserObject })).toThrow(/Forbidden/)
expect(() =>
isUser(
{ user: differentUserObject.id },
{ token: sameUserToken, user: sameUserObject }
)
).toThrow(/Forbidden/)
})
})
4 changes: 2 additions & 2 deletions graphql/resolvers/articleResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,8 @@ module.exports = {
},

Article: {
async workspaces(article, _, { user }) {
if (user.admin) {
async workspaces(article, _, { user, token }) {
if (token.admin) {
return Workspace.find({ articles: article._id })
}
return Workspace.find({
Expand Down
37 changes: 22 additions & 15 deletions graphql/resolvers/articleResolver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,33 @@ describe('article resolver', () => {
const context = {
user: {
email: '[email protected]',
admin: false,
id: userId.toString(),
_id: userId._id
}
_id: userId._id,
},
token: {},
}
const article = await Article.create({
title: 'My thesis',
owner: [userId],
contributors: [],
versions: [],
tags: []
tags: [],
})
await Workspace.create({
name: 'Workspace A',
color: '#f4a261',
members: [
{
user: new ObjectId(),
role: 'editor'
role: 'editor',
},
{
user: new ObjectId(),
role: 'translator'
role: 'translator',
},
{
user: userId,
role: 'contributor'
role: 'contributor',
},
],
articles: [article._id],
Expand All @@ -47,7 +47,7 @@ describe('article resolver', () => {
members: [
{
user: new ObjectId(),
role: 'editor'
role: 'editor',
},
],
articles: [article._id],
Expand All @@ -59,24 +59,31 @@ describe('article resolver', () => {
members: [
{
user: userId,
role: 'editor'
role: 'editor',
},
],
articles: [article._id],
creator: new ObjectId(),
})
let workspaces = await ArticleMutation.workspaces(article, {}, context)
expect(workspaces.map(w => w.toObject())).toMatchObject([
expect(workspaces.map((w) => w.toObject())).toMatchObject([
{ name: 'Workspace A' },
// should not contain Workspace B because user is not invited in this workspace
{ name: 'Workspace C' }
{ name: 'Workspace C' },
])
const contextWithAdminUser = { user: { ...context.user, admin: true } }
workspaces = await ArticleMutation.workspaces(article, {}, contextWithAdminUser)
expect(workspaces.map(w => w.toObject())).toMatchObject([
const contextWithAdminUser = {
user: { ...context.user },
token: { admin: true },
}
workspaces = await ArticleMutation.workspaces(
article,
{},
contextWithAdminUser
)
expect(workspaces.map((w) => w.toObject())).toMatchObject([
{ name: 'Workspace A' },
{ name: 'Workspace B' }, // admin user can see all workspaces that includes a given article
{ name: 'Workspace C' }
{ name: 'Workspace C' },
])
})
})
Loading

0 comments on commit 965c551

Please sign in to comment.