mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-07 15:31:00 +08:00
org avatar changing
This commit is contained in:
parent
75e915f23c
commit
4cfa34cbe6
@ -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'
|
||||
|
@ -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:
|
||||
|
@ -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">
|
||||
|
62
frontend-new/src/components/modals/AvatarChangeModal.vue
Normal file
62
frontend-new/src/components/modals/AvatarChangeModal.vue
Normal 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>
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 ...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user