org avatar changing

This commit is contained in:
MiniDigger | Martin 2022-06-15 09:34:56 +02:00
parent 75e915f23c
commit 4cfa34cbe6
9 changed files with 143 additions and 43 deletions

View File

@ -61,6 +61,7 @@ services:
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
SSO_CLIENT_ID: "${SSO_CLIENT_ID}"
TOKEN_SECRET: "${TOKEN_SECRET}"
SSO_API_KEY: "changeme"
depends_on:
- 'db'
- 'mail'

View File

@ -21,11 +21,12 @@ hangar:
auth-url: "https://hangar-auth.benndorf.dev"
oauth-url: "https://hangar-auth.benndorf.dev/hydra"
client-id: "${SSO_CLIENT_ID}"
api-key: "${SSO_API_KEY"
security:
api:
url: "https://hangar-auth.benndorf.dev"
avatar-url: "https://hangar-auth.benndorf.dev/avatar/%s?size=120x120"
avatar-url: "https://hangar-auth.benndorf.dev/avatar/%s"
token-secret: "${TOKEN_SECRET}"
logging:

View File

@ -6,11 +6,12 @@ import { avatarUrl, forumUserUrl } from "~/composables/useUrlHelper";
import { useI18n } from "vue-i18n";
import Card from "~/components/design/Card.vue";
import TaglineModal from "~/components/modals/TaglineModal.vue";
import { computed, FunctionalComponent } from "vue";
import { computed } from "vue";
import { NamedPermission } from "~/types/enums";
import { hasPerms } from "~/composables/usePerm";
import { useAuthStore } from "~/store/auth";
import Tag from "~/components/Tag.vue";
import AvatarChangeModal from "~/components/modals/AvatarChangeModal.vue";
const props = defineProps<{
user: User;
@ -32,9 +33,9 @@ const canEditCurrentUser = computed<boolean>(() => {
<template>
<Card accent>
<div class="flex mb-4">
<div>
<div class="relative">
<UserAvatar :username="user.name" :avatar-url="avatarUrl(user.name)" />
<!-- todo org avatar changing -->
<AvatarChangeModal v-if="user.isOrganization && hasPerms(NamedPermission.EDIT_SUBJECT_SETTINGS)" :user="user" />
</div>
<div class="ml-2 overflow-clip">

View File

@ -0,0 +1,62 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import Button from "~/components/design/Button.vue";
import Modal from "~/components/modals/Modal.vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { AxiosError } from "axios";
import Tooltip from "~/components/design/Tooltip.vue";
import { useContext } from "vite-ssr/vue";
import InputFile from "~/components/ui/InputFile.vue";
import { User } from "hangar-api";
import { ref } from "vue";
import { useRouter } from "vue-router";
const props = defineProps<{
user: User;
}>();
const i18n = useI18n();
const ctx = useContext();
const router = useRouter();
const file = ref();
async function changeOrgAvatar() {
if (!props.user.isOrganization) {
return;
}
try {
await useInternalApi(
`organizations/org/${props.user.name}/settings/avatar`,
true,
"POST",
{ avatar: file.value },
{ "Content-Type": "multipart/form-data" }
);
router.go(0);
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
}
}
</script>
<template>
<Modal :title="i18n.t('author.org.editAvatar')">
<template #default="{ on }">
<InputFile v-model="file"></InputFile>
<Button button-type="secondary" class="mt-2" @click="on.click">{{ i18n.t("general.close") }}</Button>
<Button button-type="primary" class="mt-2 ml-2" @click="changeOrgAvatar()">{{ i18n.t("general.submit") }}</Button>
</template>
<template #activator="{ on }">
<Tooltip class="absolute -bottom-3 -right-3">
<template #content>
{{ i18n.t("author.org.editAvatar") }}
</template>
<Button v-on="on"><IconMdiPencil /></Button>
</Tooltip>
</template>
</Modal>
</template>

View File

@ -38,8 +38,8 @@ public class HangarSecurityConfig {
@ConfigurationProperties(prefix = "hangar.security.api")
public static class SecurityApiConfig {
private String url = "http://localhost:8000";
private String avatarUrl = "http://localhost:8000/avatar/%s?size=120x120";
private String url = "http://localhost:8081";
private String avatarUrl = "http://localhost:8081/avatar/%s";
private long timeout = 10000;
public String getUrl() {

View File

@ -16,6 +16,7 @@ public class SSOConfig {
private String authUrl = "http://localhost:3001";
private String signupUrl = "/account/signup";
private String apiKey = "secret";
public boolean isEnabled() {
return enabled;
@ -80,4 +81,12 @@ public class SSOConfig {
public void setOauthUrl(String oauthUrl) {
this.oauthUrl = oauthUrl;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
}

View File

@ -42,9 +42,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Optional;
@ -151,15 +154,9 @@ public class OrganizationController extends HangarComponent {
@Unlocked
@ResponseBody
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.EDIT_SUBJECT_SETTINGS, args = "{#name}")
@GetMapping(value = "/org/{name}/settings/avatar", produces = MediaType.TEXT_PLAIN_VALUE)
public String getUpdateAvatarUri(@PathVariable String name) {
try {
URI uri = authenticationService.changeAvatarUri(getHangarPrincipal().getName(), name);
return uri.toString();
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new HangarApiException(HttpStatus.INTERNAL_SERVER_ERROR);
}
@PostMapping(value = "/org/{name}/settings/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void changeAvatar(@PathVariable String name, @RequestParam MultipartFile avatar) throws IOException {
authenticationService.changeAvatar(name, avatar);
}
@Anyone

View File

@ -1,28 +1,37 @@
package io.papermc.hangar.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.model.common.roles.GlobalRole;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.internal.ChangeAvatarToken;
import io.papermc.hangar.service.internal.perms.roles.GlobalRoleService;
import io.papermc.hangar.service.internal.users.UserService;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.model.common.roles.GlobalRole;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.service.internal.perms.roles.GlobalRoleService;
import io.papermc.hangar.service.internal.users.UserService;
@Service
public class AuthenticationService extends HangarComponent {
@ -61,23 +70,42 @@ public class AuthenticationService extends HangarComponent {
return userTable;
}
public URI changeAvatarUri(String requester, String organization) throws JsonProcessingException {
ChangeAvatarToken token = getChangeAvatarToken(requester, organization);
UriComponentsBuilder uriComponents = UriComponentsBuilder.fromHttpUrl(config.sso.getAuthUrl());
uriComponents.path("/accounts/user/{organization}/change-avatar/").queryParam("key", token.getSignedData());
return uriComponents.build(organization);
public void changeAvatar(String org, MultipartFile avatar) throws IOException {
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("avatar", new MultipartInputStreamFileResource(avatar.getInputStream(), avatar.getOriginalFilename()));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
try {
ResponseEntity<Void> response = restTemplate.postForEntity(config.security.api.getUrl() + "/avatar/org/" + org + "?apiKey=" + config.sso.getApiKey(), requestEntity, Void.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new ResponseStatusException(response.getStatusCode(), "Error from auth api");
}
} catch (HttpStatusCodeException ex) {
throw new ResponseStatusException(ex.getStatusCode(), "Error from auth api: " + ex.getMessage(), ex);
}
}
public ChangeAvatarToken getChangeAvatarToken(String requester, String organization) throws JsonProcessingException {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> bodyMap = new LinkedMultiValueMap<>();
// TODO allow changing org avatars in SSO
if (true) throw new RuntimeException("disabled");
// bodyMap.add("api-key", config.sso.getApiKey());
bodyMap.add("request_username", requester);
ChangeAvatarToken token;
token = mapper.treeToValue(restTemplate.postForObject(config.security.api.getUrl() + "/api/users/" + organization + "/change-avatar-token/", new HttpEntity<>(bodyMap, headers), ObjectNode.class), ChangeAvatarToken.class);
return token;
static class MultipartInputStreamFileResource extends InputStreamResource {
private final String filename;
MultipartInputStreamFileResource(InputStream inputStream, String filename) {
super(inputStream);
this.filename = filename;
}
@Override
public String getFilename() {
return this.filename;
}
@Override
public long contentLength() throws IOException {
return -1; // we do not want to generally read the whole stream into memory ...
}
}
}

View File

@ -143,6 +143,7 @@ hangar:
auth-url: "http://localhost:3001"
signup-url: "/account/signup/"
api-key: "supersecret"
security:
secure: false
@ -152,9 +153,9 @@ hangar:
token-expiry: 300 # seconds
refresh-token-expiry: 30 # days
api:
url: "http://localhost:8000"
# avatar-url: "http://localhost:8000/avatar/%s?size=120x120" # only comment in if you run auth locally
avatar-url: "https://docs.papermc.io/img/paper.png"
url: "http://localhost:8081"
avatar-url: "http://localhost:8081/avatar/%s" # only comment in if you run auth locally
# avatar-url: "https://docs.papermc.io/img/paper.png"
timeout: 10000
safe-download-hosts:
- "github.com"