Skip to content

Commit

Permalink
1491: Add api token feature
Browse files Browse the repository at this point in the history
  • Loading branch information
ztefanie committed Aug 29, 2024
1 parent 0f36101 commit 6a595f3
Show file tree
Hide file tree
Showing 20 changed files with 353 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext
import ActivityLogCard from './ActivityLogCard'
import ChangePasswordForm from './ChangePasswordForm'
import NotificationSettings from './NotificationSettings'
import UserUploadApiTokenSettings from './UserUploadApiTokenSettings'

const UserSettingsContainer = styled.div`
display: flex;
Expand All @@ -17,11 +18,12 @@ const UserSettingsContainer = styled.div`
`

const UserSettingsController = () => {
const { applicationFeature, activityLogConfig, projectId } = useContext(ProjectConfigContext)
const { applicationFeature, activityLogConfig, projectId, userUploadApiEnabled } = useContext(ProjectConfigContext)
const { role } = useContext(WhoAmIContext).me!
return (
<UserSettingsContainer>
{applicationFeature && role !== Role.ProjectAdmin && <NotificationSettings projectId={projectId} />}
{userUploadApiEnabled && role == Role.ProjectAdmin && <UserUploadApiTokenSettings />}
<ChangePasswordForm />
{activityLogConfig && <ActivityLogCard activityLogConfig={activityLogConfig} />}
</UserSettingsContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Button, H2, H4, HTMLSelect, HTMLTable } from '@blueprintjs/core'
import Delete from '@mui/icons-material/Delete'
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'

import getMessageFromApolloError from '../../errors/getMessageFromApolloError'
import {
UserUploadApiTokenMetaData,
useCreateUserUploadApiTokenMutation,
useDeleteUserUploadApiTokenMutation,
useGetUserUploadApiTokenMetaDataQuery,
} from '../../generated/graphql'
import { formatDate } from '../../util/formatDate'
import { useAppToaster } from '../AppToaster'
import getQueryResult from '../util/getQueryResult'
import SettingsCard from './SettingsCard'

const Container = styled.div`
background: ghostwhite;
padding: 20px;
border-radius: 8px;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05);
`

const Row = styled.div`
width: 80%;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
`

const NewTokenText = styled.p`
font-size: 18px;
color: #007bff;
background: #e9f7ff;
padding: 10px;
border-radius: 6px;
margin-top: 15px;
word-break: break-all;
`

const UserUploadApiTokenSetting = () => {
const metaDataQuery = useGetUserUploadApiTokenMetaDataQuery({})

const appToaster = useAppToaster()

const [tokenMetaData, setTokenMetadata] = useState<Array<UserUploadApiTokenMetaData>>([])
const [createdToken, setCreatedToken] = useState<string>('')
const [expiresIn, setExpiresIn] = useState<number>(1)

useEffect(() => {
const metaDataQueryResult = getQueryResult(metaDataQuery)
if (metaDataQueryResult.successful) {
const { tokenMetaData } = metaDataQueryResult.data
setTokenMetadata(tokenMetaData)
}
}, [metaDataQuery, tokenMetaData])

const [createToken] = useCreateUserUploadApiTokenMutation({
onCompleted: result => {
appToaster?.show({ intent: 'success', message: 'Token wurde erfolgreich erzeugt.' })
setCreatedToken(result.createUserUploadApiTokenPayload)
metaDataQuery.refetch()
},
onError: error => {
const { title } = getMessageFromApolloError(error)
appToaster?.show({
intent: 'danger',
message: title,
})
},
})

const [deleteToken] = useDeleteUserUploadApiTokenMutation({
onCompleted: result => {
appToaster?.show({ intent: 'success', message: 'Token wurde erfolgreich gelöscht.' })
metaDataQuery.refetch()
},
onError: error => {
const { title } = getMessageFromApolloError(error)
appToaster?.show({
intent: 'danger',
message: title,
})
},
})

return (
<SettingsCard>
<H2>Api Token</H2>

<Container>
<H4>Neues Token erstellen</H4>
<p>Ein neu erzeugtes Token wir nur einmalig angezeigt und kann danach nicht wieder abgerufen werden.</p>
<Row>
<label htmlFor='expiresIn'>Gültigkeitsdauer:</label>
<HTMLSelect
name='expiresIn'
id='expiresIn'
value={expiresIn}
onChange={e => setExpiresIn(parseInt(e.target.value))}>
<option value='1'>1 Monat</option>
<option value='3'>3 Monate</option>
<option value='12'>1 Jahr</option>
<option value='36'>3 Jahre</option>
</HTMLSelect>

<Button intent='primary' onClick={() => createToken({ variables: { expiresIn: expiresIn } })}>
Erstellen
</Button>
</Row>
{createdToken && <NewTokenText>New Token: {createdToken}</NewTokenText>}
</Container>

{tokenMetaData.length > 0 && (
<HTMLTable>
<thead>
<tr>
<th>E-Mail des Erstellers</th>
<th>Ablaufdatum</th>
<th></th>
</tr>
</thead>
<tbody>
{tokenMetaData.map((item, index) => (
<tr key={index}>
<td>{item.creatorEmail}</td>
<td>{formatDate(item.expirationDate)}</td>
<td>
<Delete color='error' onClick={() => deleteToken({ variables: { id: item.id } })} />
</td>
</tr>
))}
</tbody>
</HTMLTable>
)}
</SettingsCard>
)
}

export default UserUploadApiTokenSetting
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation createUserUploadApiToken($expiresIn: Int!) {
createUserUploadApiTokenPayload: createUserUploadApiToken(expiresIn: $expiresIn)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation deleteUserUploadApiToken($id: Int!) {
deleteUserUploadApiToken(id: $id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query getUserUploadApiTokenMetaData {
tokenMetaData: getUserUploadApiTokenMetaData {
id
creatorEmail
expirationDate
}
}
1 change: 1 addition & 0 deletions administration/src/project-configs/bayern/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const config: ProjectConfig = {
storeManagement: {
enabled: false,
},
userUploadApiEnabled: false,
}

export default config
1 change: 1 addition & 0 deletions administration/src/project-configs/getProjectConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export interface ProjectConfig {
freinetCSVImportEnabled: boolean
cardCreation: boolean
storeManagement: StoresManagement
userUploadApiEnabled: boolean
}

export const setProjectConfigOverride = (hostname: string) => {
Expand Down
1 change: 1 addition & 0 deletions administration/src/project-configs/koblenz/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const config: ProjectConfig = {
storeManagement: {
enabled: false,
},
userUploadApiEnabled: true,
}

export default config
1 change: 1 addition & 0 deletions administration/src/project-configs/nuernberg/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const config: ProjectConfig = {
freinetCSVImportEnabled: false,
cardCreation: true,
storeManagement: storeConfig,
userUploadApiEnabled: false,
}

export default config
1 change: 1 addition & 0 deletions administration/src/project-configs/showcase/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const config: ProjectConfig = {
storeManagement: {
enabled: false,
},
apiUpload: false,
}

export default config
4 changes: 4 additions & 0 deletions administration/src/util/formatDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ const formatDateWithTimezone = (dateString: string, timezone: string): string =>
new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium', timeStyle: 'short', timeZone: timezone }).format(
new Date(dateString)
)

export const formatDate = (dateString: string): string =>
new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium' }).format(new Date(dateString))

export default formatDateWithTimezone
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.javatime.date
import org.jetbrains.exposed.sql.javatime.timestamp
import org.jetbrains.exposed.sql.lowerCase
import org.jetbrains.exposed.sql.or
Expand Down Expand Up @@ -54,3 +55,21 @@ class AdministratorEntity(id: EntityID<Int>) : IntEntity(id) {
var notificationOnVerification by Administrators.notificationOnVerification
var deleted by Administrators.deleted
}

val TOKEN_LENGTH = 60

object UserUploadApiTokens : IntIdTable() {
val token = binary("token")
val creatorId = reference("creatorId", Administrators)
val projectId = reference("projectId", Projects)
val expirationDate = date("expirationDate")
}

class UserUploadApiTokenEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<UserUploadApiTokenEntity>(UserUploadApiTokens)

var token by UserUploadApiTokens.token
var creator by UserUploadApiTokens.creatorId
var projectId by UserUploadApiTokens.projectId
var expirationDate by UserUploadApiTokens.expirationDate
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package app.ehrenamtskarte.backend.auth.database.repos

import app.ehrenamtskarte.backend.auth.database.UserUploadApiTokenEntity
import org.jetbrains.exposed.dao.id.EntityID
import java.time.LocalDate

object UserUploadApiTokensRepository {
fun insert(
token: ByteArray,
adminId: EntityID<Int>,
expirationDate: LocalDate,
projectId: EntityID<Int>
): UserUploadApiTokenEntity {
return UserUploadApiTokenEntity.new {
this.token = token
this.creator = adminId
this.expirationDate = expirationDate
this.projectId = projectId
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import app.ehrenamtskarte.backend.auth.webservice.schema.NotificationSettingsQue
import app.ehrenamtskarte.backend.auth.webservice.schema.ResetPasswordMutationService
import app.ehrenamtskarte.backend.auth.webservice.schema.ResetPasswordQueryService
import app.ehrenamtskarte.backend.auth.webservice.schema.SignInMutationService
import app.ehrenamtskarte.backend.auth.webservice.schema.UserUploadApiTokenQueryService
import app.ehrenamtskarte.backend.auth.webservice.schema.UserUploadApiTokenService
import app.ehrenamtskarte.backend.auth.webservice.schema.ViewAdministratorsQueryService
import app.ehrenamtskarte.backend.common.webservice.GraphQLParams
import app.ehrenamtskarte.backend.common.webservice.createRegistryFromNamedDataLoaders
Expand All @@ -22,11 +24,13 @@ val authGraphQlParams = GraphQLParams(
TopLevelObject(ChangePasswordMutationService()),
TopLevelObject(ResetPasswordMutationService()),
TopLevelObject(ManageUsersMutationService()),
TopLevelObject(NotificationSettingsMutationService())
TopLevelObject(NotificationSettingsMutationService()),
TopLevelObject(UserUploadApiTokenService())
),
queries = listOf(
TopLevelObject(ViewAdministratorsQueryService()),
TopLevelObject(ResetPasswordQueryService()),
TopLevelObject(NotificationSettingsQueryService())
TopLevelObject(NotificationSettingsQueryService()),
TopLevelObject(UserUploadApiTokenQueryService())
)
)
Loading

0 comments on commit 6a595f3

Please sign in to comment.