Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1491: Add api token feature #1607

Merged
merged 12 commits into from
Sep 24, 2024
142 changes: 142 additions & 0 deletions administration/src/bp-modules/user-settings/ApiTokenSettings.tsx
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 {
ApiTokenMetaData,
useCreateApiTokenMutation,
useDeleteApiTokenMutation,
useGetApiTokenMetaDataQuery,
} 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;
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
padding: 10px;
border-radius: 6px;
margin-top: 15px;
word-break: break-all;
`

const ApiTokenSetting = () => {
const metaDataQuery = useGetApiTokenMetaDataQuery({})

const appToaster = useAppToaster()

const [tokenMetaData, setTokenMetadata] = useState<Array<ApiTokenMetaData>>([])
const [createdToken, setCreatedToken] = useState<string>('')
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
const [expiresIn, setExpiresIn] = useState<number>(1)

useEffect(() => {
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
const metaDataQueryResult = getQueryResult(metaDataQuery)
if (metaDataQueryResult.successful) {
const { tokenMetaData } = metaDataQueryResult.data
setTokenMetadata(tokenMetaData)
}
}, [metaDataQuery, tokenMetaData])

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

const [deleteToken] = useDeleteApiTokenMutation({
onCompleted: result => {
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
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>

ztefanie marked this conversation as resolved.
Show resolved Hide resolved
<Container>
<H4>Neues Token erstellen</H4>
<p>Ein neu erzeugtes Token wir nur einmalig angezeigt und kann danach nicht wieder abgerufen werden.</p>
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
<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>

ztefanie marked this conversation as resolved.
Show resolved Hide resolved
<Button intent='primary' onClick={() => createToken({ variables: { expiresIn: expiresIn } })}>
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
Erstellen
</Button>
</Row>
{createdToken && <NewTokenText>New Token: {createdToken}</NewTokenText>}
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
</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) => (
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
<tr key={index}>
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
<td>{item.creatorEmail}</td>
<td>{formatDate(item.expirationDate)}</td>
<td>
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
<Delete color='error' onClick={() => deleteToken({ variables: { id: item.id } })} />
</td>
</tr>
))}
</tbody>
</HTMLTable>
)}
</SettingsCard>
)
}

export default ApiTokenSetting
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { WhoAmIContext } from '../../WhoAmIProvider'
import { Role } from '../../generated/graphql'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import ActivityLogCard from './ActivityLogCard'
import ApiTokenSettings from './ApiTokenSettings'
import ChangePasswordForm from './ChangePasswordForm'
import NotificationSettings from './NotificationSettings'

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 && <ApiTokenSettings />}
<ChangePasswordForm />
{activityLogConfig && <ActivityLogCard activityLogConfig={activityLogConfig} />}
</UserSettingsContainer>
Expand Down
3 changes: 3 additions & 0 deletions administration/src/graphql/auth/createApiToken.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation createApiToken($expiresIn: Int!) {
createApiTokenPayload: createApiToken(expiresIn: $expiresIn)
}
3 changes: 3 additions & 0 deletions administration/src/graphql/auth/deleteApiToken.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation deleteApiToken($id: Int!) {
deleteApiToken(id: $id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query getApiTokenMetaData {
tokenMetaData: getApiTokenMetaData {
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
ztefanie marked this conversation as resolved.
Show resolved Hide resolved
}

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,
},
userUploadApiEnabled: 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
ztefanie marked this conversation as resolved.
Show resolved Hide resolved

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

class ApiTokenEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<ApiTokenEntity>(ApiTokens)

var token by ApiTokens.token
var creator by ApiTokens.creatorId
var projectId by ApiTokens.projectId
var expirationDate by ApiTokens.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.ApiTokenEntity
import org.jetbrains.exposed.dao.id.EntityID
import java.time.LocalDate

object ApiTokensRepository {
fun insert(
token: ByteArray,
adminId: EntityID<Int>,
expirationDate: LocalDate,
projectId: EntityID<Int>
): ApiTokenEntity {
return ApiTokenEntity.new {
this.token = token
this.creator = adminId
this.expirationDate = expirationDate
this.projectId = projectId
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package app.ehrenamtskarte.backend.auth.webservice

import app.ehrenamtskarte.backend.auth.webservice.dataloader.administratorLoader
import app.ehrenamtskarte.backend.auth.webservice.schema.ApiTokenQueryService
import app.ehrenamtskarte.backend.auth.webservice.schema.ApiTokenService
import app.ehrenamtskarte.backend.auth.webservice.schema.ChangePasswordMutationService
import app.ehrenamtskarte.backend.auth.webservice.schema.ManageUsersMutationService
import app.ehrenamtskarte.backend.auth.webservice.schema.NotificationSettingsMutationService
Expand All @@ -22,11 +24,13 @@ val authGraphQlParams = GraphQLParams(
TopLevelObject(ChangePasswordMutationService()),
TopLevelObject(ResetPasswordMutationService()),
TopLevelObject(ManageUsersMutationService()),
TopLevelObject(NotificationSettingsMutationService())
TopLevelObject(NotificationSettingsMutationService()),
TopLevelObject(ApiTokenService())
),
queries = listOf(
TopLevelObject(ViewAdministratorsQueryService()),
TopLevelObject(ResetPasswordQueryService()),
TopLevelObject(NotificationSettingsQueryService())
TopLevelObject(NotificationSettingsQueryService()),
TopLevelObject(ApiTokenQueryService())
)
)
Loading