2024-08-17 21:57:14 +02:00
package service
2024-08-12 11:00:25 +02:00
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/base64"
2024-08-17 21:57:14 +02:00
"fmt"
2024-08-12 11:00:25 +02:00
"log"
"os"
2025-01-03 15:08:55 +01:00
"path/filepath"
2024-08-12 11:00:25 +02:00
"time"
2025-02-05 18:08:01 +01:00
"github.com/fxamacker/cbor/v2"
2024-08-12 11:00:25 +02:00
"github.com/go-webauthn/webauthn/protocol"
2025-03-18 13:08:33 -07:00
"github.com/lestrrat-go/jwx/v3/jwk"
"gorm.io/gorm"
2025-02-05 18:08:01 +01:00
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/model"
2025-03-18 13:08:33 -07:00
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
2025-02-05 18:08:01 +01:00
"github.com/pocket-id/pocket-id/backend/internal/utils"
2025-03-18 13:08:33 -07:00
"github.com/pocket-id/pocket-id/backend/resources"
2024-08-12 11:00:25 +02:00
)
2024-08-17 21:57:14 +02:00
type TestService struct {
db * gorm . DB
2025-02-14 17:09:27 +01:00
jwtService * JwtService
2024-08-17 21:57:14 +02:00
appConfigService * AppConfigService
2024-08-12 11:00:25 +02:00
}
2025-02-14 17:09:27 +01:00
func NewTestService ( db * gorm . DB , appConfigService * AppConfigService , jwtService * JwtService ) * TestService {
return & TestService { db : db , appConfigService : appConfigService , jwtService : jwtService }
2024-08-12 11:00:25 +02:00
}
2024-08-17 21:57:14 +02:00
func ( s * TestService ) SeedDatabase ( ) error {
return s . db . Transaction ( func ( tx * gorm . DB ) error {
2024-08-12 11:00:25 +02:00
users := [ ] model . User {
{
Base : model . Base {
ID : "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" ,
} ,
Username : "tim" ,
Email : "tim.cook@test.com" ,
FirstName : "Tim" ,
LastName : "Cook" ,
IsAdmin : true ,
} ,
{
Base : model . Base {
ID : "1cd19686-f9a6-43f4-a41f-14a0bf5b4036" ,
} ,
Username : "craig" ,
Email : "craig.federighi@test.com" ,
FirstName : "Craig" ,
LastName : "Federighi" ,
IsAdmin : false ,
} ,
}
for _ , user := range users {
if err := tx . Create ( & user ) . Error ; err != nil {
return err
}
}
2024-12-13 09:03:52 +01:00
oneTimeAccessTokens := [ ] model . OneTimeAccessToken { {
Base : model . Base {
ID : "bf877753-4ea4-4c9c-bbbd-e198bb201cb8" ,
} ,
Token : "HPe6k6uiDRRVuAQV" ,
ExpiresAt : datatype . DateTime ( time . Now ( ) . Add ( 1 * time . Hour ) ) ,
UserID : users [ 0 ] . ID ,
} ,
{
Base : model . Base {
ID : "d3afae24-fe2d-4a98-abec-cf0b8525096a" ,
} ,
Token : "YCGDtftvsvYWiXd0" ,
ExpiresAt : datatype . DateTime ( time . Now ( ) . Add ( - 1 * time . Second ) ) , // expired
UserID : users [ 0 ] . ID ,
} ,
}
for _ , token := range oneTimeAccessTokens {
if err := tx . Create ( & token ) . Error ; err != nil {
return err
}
}
2024-10-02 09:38:57 +02:00
userGroups := [ ] model . UserGroup {
{
Base : model . Base {
2024-12-12 17:21:28 +01:00
ID : "c7ae7c01-28a3-4f3c-9572-1ee734ea8368" ,
2024-10-02 09:38:57 +02:00
} ,
Name : "developers" ,
FriendlyName : "Developers" ,
Users : [ ] model . User { users [ 0 ] , users [ 1 ] } ,
} ,
{
Base : model . Base {
ID : "adab18bf-f89d-4087-9ee1-70ff15b48211" ,
} ,
Name : "designers" ,
FriendlyName : "Designers" ,
Users : [ ] model . User { users [ 0 ] } ,
} ,
}
for _ , group := range userGroups {
if err := tx . Create ( & group ) . Error ; err != nil {
return err
}
}
2024-08-12 11:00:25 +02:00
oidcClients := [ ] model . OidcClient {
{
Base : model . Base {
ID : "3654a746-35d4-4321-ac61-0bdcff2b4055" ,
} ,
2025-02-14 17:09:27 +01:00
Name : "Nextcloud" ,
Secret : "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC" , // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY
CallbackURLs : model . UrlList { "http://nextcloud/auth/callback" } ,
LogoutCallbackURLs : model . UrlList { "http://nextcloud/auth/logout/callback" } ,
ImageType : utils . StringPointer ( "png" ) ,
CreatedByID : users [ 0 ] . ID ,
2024-08-12 11:00:25 +02:00
} ,
{
Base : model . Base {
ID : "606c7782-f2b1-49e5-8ea9-26eb1b06d018" ,
} ,
2024-08-23 17:04:19 +02:00
Name : "Immich" ,
Secret : "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe" , // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
2025-02-14 17:09:27 +01:00
CallbackURLs : model . UrlList { "http://immich/auth/callback" } ,
2025-02-03 18:41:15 +01:00
CreatedByID : users [ 1 ] . ID ,
AllowedUserGroups : [ ] model . UserGroup {
userGroups [ 1 ] ,
} ,
2024-08-12 11:00:25 +02:00
} ,
}
for _ , client := range oidcClients {
if err := tx . Create ( & client ) . Error ; err != nil {
return err
}
}
authCode := model . OidcAuthorizationCode {
Code : "auth-code" ,
Scope : "openid profile" ,
Nonce : "nonce" ,
2024-10-23 10:02:11 +02:00
ExpiresAt : datatype . DateTime ( time . Now ( ) . Add ( 1 * time . Hour ) ) ,
2024-08-12 11:00:25 +02:00
UserID : users [ 0 ] . ID ,
ClientID : oidcClients [ 0 ] . ID ,
}
if err := tx . Create ( & authCode ) . Error ; err != nil {
return err
}
2025-03-23 15:14:26 -05:00
refreshToken := model . OidcRefreshToken {
Token : "ou87UDg249r1StBLYkMEqy9TXDbV5HmGuDpMcZDo" ,
ExpiresAt : datatype . DateTime ( time . Now ( ) . Add ( 24 * time . Hour ) ) ,
Scope : "openid profile email" ,
UserID : users [ 0 ] . ID ,
ClientID : oidcClients [ 0 ] . ID ,
}
if err := tx . Create ( & refreshToken ) . Error ; err != nil {
return err
}
2024-08-12 11:00:25 +02:00
accessToken := model . OneTimeAccessToken {
Token : "one-time-token" ,
2024-10-23 10:02:11 +02:00
ExpiresAt : datatype . DateTime ( time . Now ( ) . Add ( 1 * time . Hour ) ) ,
2024-08-12 11:00:25 +02:00
UserID : users [ 0 ] . ID ,
}
if err := tx . Create ( & accessToken ) . Error ; err != nil {
return err
}
userAuthorizedClient := model . UserAuthorizedOidcClient {
Scope : "openid profile email" ,
UserID : users [ 0 ] . ID ,
ClientID : oidcClients [ 0 ] . ID ,
}
if err := tx . Create ( & userAuthorizedClient ) . Error ; err != nil {
return err
}
2025-02-03 18:41:15 +01:00
// To generate a new key pair, run the following command:
// openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 | \
// openssl pkcs8 -topk8 -nocrypt | tee >(openssl pkey -pubout)
publicKeyPasskey1 , err := s . getCborPublicKey ( "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwcOo5KV169KR67QEHrcYkeXE3CCxv2BgwnSq4VYTQxyLtdmKxegexa8JdwFKhKXa2BMI9xaN15BoL6wSCRFJhg==" )
publicKeyPasskey2 , err := s . getCborPublicKey ( "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEj4qA0PrZzg8Co1C27nyUbzrp8Ewjr7eOlGI2LfrzmbL5nPhZRAdJ3hEaqrHMSnJBhfMqtQGKwDYpaLIQFAKLhw==" )
2024-08-17 21:57:14 +02:00
if err != nil {
return err
}
2024-08-12 11:00:25 +02:00
webauthnCredentials := [ ] model . WebauthnCredential {
{
Name : "Passkey 1" ,
2025-02-03 18:41:15 +01:00
CredentialID : [ ] byte ( "test-credential-tim" ) ,
PublicKey : publicKeyPasskey1 ,
2024-08-12 11:00:25 +02:00
AttestationType : "none" ,
Transport : model . AuthenticatorTransportList { protocol . Internal } ,
UserID : users [ 0 ] . ID ,
} ,
{
Name : "Passkey 2" ,
2025-02-03 18:41:15 +01:00
CredentialID : [ ] byte ( "test-credential-craig" ) ,
PublicKey : publicKeyPasskey2 ,
2024-08-12 11:00:25 +02:00
AttestationType : "none" ,
Transport : model . AuthenticatorTransportList { protocol . Internal } ,
2025-02-03 18:41:15 +01:00
UserID : users [ 1 ] . ID ,
2024-08-12 11:00:25 +02:00
} ,
}
for _ , credential := range webauthnCredentials {
if err := tx . Create ( & credential ) . Error ; err != nil {
return err
}
}
webauthnSession := model . WebauthnSession {
Challenge : "challenge" ,
2024-12-12 17:21:28 +01:00
ExpiresAt : datatype . DateTime ( time . Now ( ) . Add ( 1 * time . Hour ) ) ,
2024-08-12 11:00:25 +02:00
UserVerification : "preferred" ,
}
if err := tx . Create ( & webauthnSession ) . Error ; err != nil {
2025-03-11 14:16:42 -05:00
return err
}
apiKey := model . ApiKey {
Base : model . Base {
ID : "5f1fa856-c164-4295-961e-175a0d22d725" ,
} ,
Name : "Test API Key" ,
Key : "6c34966f57ef2bb7857649aff0e7ab3ad67af93c846342ced3f5a07be8706c20" ,
UserID : users [ 0 ] . ID ,
}
if err := tx . Create ( & apiKey ) . Error ; err != nil {
2024-08-12 11:00:25 +02:00
return err
}
return nil
} )
}
2024-08-17 21:57:14 +02:00
func ( s * TestService ) ResetDatabase ( ) error {
err := s . db . Transaction ( func ( tx * gorm . DB ) error {
2024-08-12 11:00:25 +02:00
var tables [ ] string
2024-12-12 17:21:28 +01:00
switch common . EnvConfig . DbProvider {
case common . DbProviderSqlite :
// Query to get all tables for SQLite
if err := tx . Raw ( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'schema_migrations';" ) . Scan ( & tables ) . Error ; err != nil {
return err
}
case common . DbProviderPostgres :
// Query to get all tables for PostgreSQL
if err := tx . Raw ( `
SELECT tablename
FROM pg_tables
WHERE schemaname = ' public ' AND tablename != ' schema_migrations ' ;
` ) . Scan ( & tables ) . Error ; err != nil {
return err
}
default :
return fmt . Errorf ( "unsupported database provider: %s" , common . EnvConfig . DbProvider )
2024-08-12 11:00:25 +02:00
}
2024-10-26 00:15:31 +02:00
// Delete all rows from all tables
2024-08-12 11:00:25 +02:00
for _ , table := range tables {
2024-12-12 17:21:28 +01:00
if err := tx . Exec ( fmt . Sprintf ( "DELETE FROM %s;" , table ) ) . Error ; err != nil {
2024-08-12 11:00:25 +02:00
return err
}
}
2024-10-26 00:15:31 +02:00
2024-08-12 11:00:25 +02:00
return nil
} )
2024-10-26 00:15:31 +02:00
2024-08-17 21:57:14 +02:00
return err
2024-08-12 11:00:25 +02:00
}
2024-08-17 21:57:14 +02:00
func ( s * TestService ) ResetApplicationImages ( ) error {
2024-08-12 11:00:25 +02:00
if err := os . RemoveAll ( common . EnvConfig . UploadPath ) ; err != nil {
log . Printf ( "Error removing directory: %v" , err )
return err
}
2025-01-03 15:08:55 +01:00
files , err := resources . FS . ReadDir ( "images" )
if err != nil {
2024-08-12 11:00:25 +02:00
return err
}
2025-01-03 15:08:55 +01:00
for _ , file := range files {
srcFilePath := filepath . Join ( "images" , file . Name ( ) )
destFilePath := filepath . Join ( common . EnvConfig . UploadPath , "application-images" , file . Name ( ) )
err := utils . CopyEmbeddedFileToDisk ( srcFilePath , destFilePath )
if err != nil {
return err
}
}
2024-08-12 11:00:25 +02:00
return nil
}
2024-10-26 00:15:31 +02:00
func ( s * TestService ) ResetAppConfig ( ) error {
// Reseed the config variables
if err := s . appConfigService . InitDbConfig ( ) ; err != nil {
return err
}
// Reset all app config variables to their default values
if err := s . db . Session ( & gorm . Session { AllowGlobalUpdate : true } ) . Model ( & model . AppConfigVariable { } ) . Update ( "value" , "" ) . Error ; err != nil {
return err
}
// Reload the app config from the database after resetting the values
return s . appConfigService . LoadDbConfigFromDb ( )
}
2025-02-14 17:09:27 +01:00
func ( s * TestService ) SetJWTKeys ( ) {
2025-03-18 13:08:33 -07:00
const privateKeyString = ` { "alg":"RS256","d":"mvMDWSdPPvcum0c0iEHE2gbqtV2NKMmLwrl9E6K7g8lTV95SePLnW_bwyMPV7EGp7PQk3l17I5XRhFjze7GqTnFIOgKzMianPs7jv2ELtBMGK0xOPATgu1iGb70xZ6vcvuEfRyY3dJ0zr4jpUdVuXwKmx9rK4IdZn2dFCKfvSuspqIpz11RhF1ALrqDLkxGVv7ZwNh0_VhJZU9hcjG5l6xc7rQEKpPRkZp0IdjkGS8Z0FskoVaiRIWAbZuiVFB9WCW8k1czC4HQTPLpII01bUQx2ludbm0UlXRgVU9ptUUbU7GAImQqTOW8LfPGklEvcgzlIlR_oqw4P9yBxLi-yMQ","dp":"pvNCSnnhbo8Igw9psPR-DicxFnkXlu_ix4gpy6efTrxA-z1VDFDioJ814vKQNioYDzpyAP1gfMPhRkvG_q0hRZsJah3Sb9dfA-WkhSWY7lURQP4yIBTMU0PF_rEATuS7lRciYk1SOx5fqXZd3m_LP0vpBC4Ujlq6NAq6CIjCnms","dq":"TtUVGCCkPNgfOLmkYXu7dxxUCV5kB01-xAEK2OY0n0pG8vfDophH4_D_ZC7nvJ8J9uDhs_3JStexq1lIvaWtG99RNTChIEDzpdn6GH9yaVcb_eB4uJjrNm64FhF8PGCCwxA-xMCZMaARKwhMB2_IOMkxUbWboL3gnhJ2rDO_QO0","e":"AQAB","kid":"8uHDw3M6rf8","kty":"RSA","n":"yaeEL0VKoPBXIAaWXsUgmu05lAvEIIdJn0FX9lHh4JE5UY9B83C5sCNdhs9iSWzpeP11EVjWp8i3Yv2CF7c7u50BXnVBGtxpZpFC-585UXacoJ0chUmarL9GRFJcM1nPHBTFu68aRrn1rIKNHUkNaaxFo0NFGl_4EDDTO8HwawTjwkPoQlRzeByhlvGPVvwgB3Fn93B8QJ_cZhXKxJvjjrC_8Pk76heC_ntEMru71Ix77BoC3j2TuyiN7m9RNBW8BU5q6lKoIdvIeZfTFLzi37iufyfvMrJTixp9zhNB1NxlLCeOZl2MXegtiGqd2H3cbAyqoOiv9ihUWTfXj7SxJw","p":"_Yylc9e07CKdqNRD2EosMC2mrhrEa9j5oY_l00Qyy4-jmCA59Q9viyqvveRo0U7cRvFA5BWgWN6GGLh1DG3X-QBqVr0dnk3uzbobb55RYUXyPLuBZI2q6w2oasbiDwPdY7KpkVv_H-bpITQlyDvO8hhucA6rUV7F6KTQVz8M3Ms","q":"y5p3hch-7jJ21TkAhp_Vk1fLCAuD4tbErwQs2of9ja8sB4iJOs5Wn6HD3P7Mc8Plye7qaLHvzc8I5g0tPKWvC0DPd_FLPXiWwMVAzee3NUX_oGeJNOQp11y1w_KqdO9qZqHSEPZ3NcFL_SZMFgggxhM1uzRiPzsVN0lnD_6prZU","qi":"2Grt6uXHm61ji3xSdkBWNtUnj19vS1-7rFJp5SoYztVQVThf_W52BAiXKBdYZDRVoItC_VS2NvAOjeJjhYO_xQ_q3hK7MdtuXfEPpLnyXKkmWo3lrJ26wbeF6l05LexCkI7ShsOuSt-dsyaTJTszuKDIA6YOfWvfo3aVZmlWRaI","use":"sig"} `
2025-02-14 17:09:27 +01:00
2025-03-18 13:08:33 -07:00
privateKey , _ := jwk . ParseKey ( [ ] byte ( privateKeyString ) )
2025-03-13 09:01:15 -07:00
s . jwtService . SetKey ( privateKey )
2025-02-14 17:09:27 +01:00
}
2024-08-12 11:00:25 +02:00
// getCborPublicKey decodes a Base64 encoded public key and returns the CBOR encoded COSE key
2024-10-26 00:15:31 +02:00
func ( s * TestService ) getCborPublicKey ( base64PublicKey string ) ( [ ] byte , error ) {
2024-08-12 11:00:25 +02:00
decodedKey , err := base64 . StdEncoding . DecodeString ( base64PublicKey )
if err != nil {
2024-08-17 21:57:14 +02:00
return nil , fmt . Errorf ( "failed to decode base64 key: %w" , err )
2024-08-12 11:00:25 +02:00
}
pubKey , err := x509 . ParsePKIXPublicKey ( decodedKey )
if err != nil {
2024-08-17 21:57:14 +02:00
return nil , fmt . Errorf ( "failed to parse public key: %w" , err )
2024-08-12 11:00:25 +02:00
}
ecdsaPubKey , ok := pubKey . ( * ecdsa . PublicKey )
if ! ok {
2024-08-17 21:57:14 +02:00
return nil , fmt . Errorf ( "not an ECDSA public key" )
2024-08-12 11:00:25 +02:00
}
coseKey := map [ int ] interface { } {
1 : 2 , // Key type: EC2
3 : - 7 , // Algorithm: ECDSA with SHA-256
- 1 : 1 , // Curve: P-256
- 2 : ecdsaPubKey . X . Bytes ( ) , // X coordinate
- 3 : ecdsaPubKey . Y . Bytes ( ) , // Y coordinate
}
cborPublicKey , err := cbor . Marshal ( coseKey )
if err != nil {
2024-08-17 21:57:14 +02:00
return nil , fmt . Errorf ( "failed to marshal COSE key: %w" , err )
2024-08-12 11:00:25 +02:00
}
2024-08-17 21:57:14 +02:00
return cborPublicKey , nil
2024-08-12 11:00:25 +02:00
}